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