1use crate::{db::ChannelId, tests::TestServer};
2use call::ActiveCall;
3use chrono::Utc;
4use editor::Editor;
5use gpui::{BackgroundExecutor, TestAppContext};
6use rpc::proto;
7
8#[gpui::test]
9async fn test_channel_guests(
10 executor: BackgroundExecutor,
11 cx_a: &mut TestAppContext,
12 cx_b: &mut TestAppContext,
13) {
14 let mut server = TestServer::start(executor.clone()).await;
15 let client_a = server.create_client(cx_a, "user_a").await;
16 let client_b = server.create_client(cx_b, "user_b").await;
17 let active_call_a = cx_a.read(ActiveCall::global);
18
19 let channel_id = server
20 .make_public_channel("the-channel", &client_a, cx_a)
21 .await;
22
23 // Client A shares a project in the channel
24 let project_a = client_a.build_test_project(cx_a).await;
25 active_call_a
26 .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
27 .await
28 .unwrap();
29 let project_id = active_call_a
30 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
31 .await
32 .unwrap();
33 cx_a.executor().run_until_parked();
34
35 // Client B joins channel A as a guest
36 cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
37 .await
38 .unwrap();
39
40 // b should be following a in the shared project.
41 // B is a guest,
42 executor.run_until_parked();
43
44 let active_call_b = cx_b.read(ActiveCall::global);
45 let project_b =
46 active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
47 let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone());
48
49 assert_eq!(
50 project_b.read_with(cx_b, |project, _| project.remote_id()),
51 Some(project_id),
52 );
53 assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
54 assert!(
55 project_b
56 .update(cx_b, |project, cx| {
57 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
58 project.create_entry((worktree_id, "b.txt"), false, cx)
59 })
60 .await
61 .is_err()
62 );
63 assert!(room_b.read_with(cx_b, |room, _| room.is_muted()));
64}
65
66#[gpui::test]
67async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
68 let mut server = TestServer::start(cx_a.executor()).await;
69 let client_a = server.create_client(cx_a, "user_a").await;
70 let client_b = server.create_client(cx_b, "user_b").await;
71 let active_call_a = cx_a.read(ActiveCall::global);
72
73 let channel_id = server
74 .make_public_channel("the-channel", &client_a, cx_a)
75 .await;
76
77 let project_a = client_a.build_test_project(cx_a).await;
78 cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
79 .await
80 .unwrap();
81
82 // Client A shares a project in the channel
83 active_call_a
84 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
85 .await
86 .unwrap();
87 cx_a.run_until_parked();
88
89 // Client B joins channel A as a guest
90 cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
91 .await
92 .unwrap();
93 cx_a.run_until_parked();
94
95 // client B opens 1.txt as a guest
96 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
97 let room_b = cx_b
98 .read(ActiveCall::global)
99 .update(cx_b, |call, _| call.room().unwrap().clone());
100 cx_b.simulate_keystrokes("cmd-p");
101 cx_a.run_until_parked();
102 cx_b.simulate_keystrokes("1 enter");
103
104 let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| {
105 (
106 workspace.project().clone(),
107 workspace.active_item_as::<Editor>(cx).unwrap(),
108 )
109 });
110 assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
111 assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
112 cx_b.update(|_window, cx_b| {
113 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
114 });
115 assert!(
116 room_b
117 .update(cx_b, |room, cx| room.share_microphone(cx))
118 .await
119 .is_err()
120 );
121
122 // B is promoted
123 active_call_a
124 .update(cx_a, |call, cx| {
125 call.room().unwrap().update(cx, |room, cx| {
126 room.set_participant_role(
127 client_b.user_id().unwrap(),
128 proto::ChannelRole::Member,
129 cx,
130 )
131 })
132 })
133 .await
134 .unwrap();
135 cx_a.run_until_parked();
136
137 // project and buffers are now editable
138 assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
139 assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
140
141 // B sees themselves as muted, and can unmute.
142 cx_b.update(|_window, cx_b| {
143 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
144 });
145 room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
146 room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
147 cx_a.run_until_parked();
148 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
149
150 // B is demoted
151 active_call_a
152 .update(cx_a, |call, cx| {
153 call.room().unwrap().update(cx, |room, cx| {
154 room.set_participant_role(
155 client_b.user_id().unwrap(),
156 proto::ChannelRole::Guest,
157 cx,
158 )
159 })
160 })
161 .await
162 .unwrap();
163 cx_a.run_until_parked();
164
165 // project and buffers are no longer editable
166 assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
167 assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
168 assert!(
169 room_b
170 .update(cx_b, |room, cx| room.share_microphone(cx))
171 .await
172 .is_err()
173 );
174}
175
176#[gpui::test]
177async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
178 let mut server = TestServer::start(cx_a.executor()).await;
179
180 server
181 .app_state
182 .db
183 .update_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None)
184 .await
185 .unwrap();
186
187 let client_a = server.create_client(cx_a, "user_a").await;
188 let client_b = server.create_client(cx_b, "user_b").await;
189 let active_call_a = cx_a.read(ActiveCall::global);
190 let active_call_b = cx_b.read(ActiveCall::global);
191
192 // Create a parent channel that requires the Zed CLA
193 let parent_channel_id = server
194 .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
195 .await;
196 server
197 .app_state
198 .db
199 .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id.0), true)
200 .await
201 .unwrap();
202
203 // Create a public channel that is a child of the parent channel.
204 let channel_id = client_a
205 .channel_store()
206 .update(cx_a, |store, cx| {
207 store.create_channel("the-sub-channel", Some(parent_channel_id), cx)
208 })
209 .await
210 .unwrap();
211 client_a
212 .channel_store()
213 .update(cx_a, |store, cx| {
214 store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
215 })
216 .await
217 .unwrap();
218 client_a
219 .channel_store()
220 .update(cx_a, |store, cx| {
221 store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
222 })
223 .await
224 .unwrap();
225
226 // Users A and B join the channel. B is a guest.
227 active_call_a
228 .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
229 .await
230 .unwrap();
231 active_call_b
232 .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
233 .await
234 .unwrap();
235 cx_a.run_until_parked();
236 let room_b = cx_b
237 .read(ActiveCall::global)
238 .update(cx_b, |call, _| call.room().unwrap().clone());
239 cx_b.update(|cx_b| {
240 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
241 });
242
243 // A tries to grant write access to B, but cannot because B has not
244 // yet signed the zed CLA.
245 active_call_a
246 .update(cx_a, |call, cx| {
247 call.room().unwrap().update(cx, |room, cx| {
248 room.set_participant_role(
249 client_b.user_id().unwrap(),
250 proto::ChannelRole::Member,
251 cx,
252 )
253 })
254 })
255 .await
256 .unwrap_err();
257 cx_a.run_until_parked();
258 assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
259 cx_b.update(|cx_b| {
260 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
261 });
262
263 // A tries to grant write access to B, but cannot because B has not
264 // yet signed the zed CLA.
265 active_call_a
266 .update(cx_a, |call, cx| {
267 call.room().unwrap().update(cx, |room, cx| {
268 room.set_participant_role(
269 client_b.user_id().unwrap(),
270 proto::ChannelRole::Talker,
271 cx,
272 )
273 })
274 })
275 .await
276 .unwrap();
277 cx_a.run_until_parked();
278 assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
279 cx_b.update(|cx_b| {
280 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
281 });
282
283 // User B signs the zed CLA.
284 server
285 .app_state
286 .db
287 .add_contributor("user_b", 100, None, None, Utc::now(), None)
288 .await
289 .unwrap();
290
291 // A can now grant write access to B.
292 active_call_a
293 .update(cx_a, |call, cx| {
294 call.room().unwrap().update(cx, |room, cx| {
295 room.set_participant_role(
296 client_b.user_id().unwrap(),
297 proto::ChannelRole::Member,
298 cx,
299 )
300 })
301 })
302 .await
303 .unwrap();
304 cx_a.run_until_parked();
305 assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
306 cx_b.update(|cx_b| {
307 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
308 });
309}