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 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
111 assert!(room_b
112 .update(cx_b, |room, cx| room.share_microphone(cx))
113 .await
114 .is_err());
115
116 // B is promoted
117 active_call_a
118 .update(cx_a, |call, cx| {
119 call.room().unwrap().update(cx, |room, cx| {
120 room.set_participant_role(
121 client_b.user_id().unwrap(),
122 proto::ChannelRole::Member,
123 cx,
124 )
125 })
126 })
127 .await
128 .unwrap();
129 cx_a.run_until_parked();
130
131 // project and buffers are now editable
132 assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
133 assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
134
135 // B sees themselves as muted, and can unmute.
136 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
137 room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
138 room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
139 cx_a.run_until_parked();
140 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
141
142 // B is demoted
143 active_call_a
144 .update(cx_a, |call, cx| {
145 call.room().unwrap().update(cx, |room, cx| {
146 room.set_participant_role(
147 client_b.user_id().unwrap(),
148 proto::ChannelRole::Guest,
149 cx,
150 )
151 })
152 })
153 .await
154 .unwrap();
155 cx_a.run_until_parked();
156
157 // project and buffers are no longer editable
158 assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
159 assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
160 assert!(room_b
161 .update(cx_b, |room, cx| room.share_microphone(cx))
162 .await
163 .is_err());
164}
165
166#[gpui::test]
167async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
168 let mut server = TestServer::start(cx_a.executor()).await;
169
170 server
171 .app_state
172 .db
173 .get_or_create_user_by_github_account("user_b", 100, None, Utc::now(), None)
174 .await
175 .unwrap();
176
177 let client_a = server.create_client(cx_a, "user_a").await;
178 let client_b = server.create_client(cx_b, "user_b").await;
179 let active_call_a = cx_a.read(ActiveCall::global);
180 let active_call_b = cx_b.read(ActiveCall::global);
181
182 // Create a parent channel that requires the Zed CLA
183 let parent_channel_id = server
184 .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
185 .await;
186 server
187 .app_state
188 .db
189 .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id.0), true)
190 .await
191 .unwrap();
192
193 // Create a public channel that is a child of the parent channel.
194 let channel_id = client_a
195 .channel_store()
196 .update(cx_a, |store, cx| {
197 store.create_channel("the-sub-channel", Some(parent_channel_id), cx)
198 })
199 .await
200 .unwrap();
201 client_a
202 .channel_store()
203 .update(cx_a, |store, cx| {
204 store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
205 })
206 .await
207 .unwrap();
208 client_a
209 .channel_store()
210 .update(cx_a, |store, cx| {
211 store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
212 })
213 .await
214 .unwrap();
215
216 // Users A and B join the channel. B is a guest.
217 active_call_a
218 .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
219 .await
220 .unwrap();
221 active_call_b
222 .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
223 .await
224 .unwrap();
225 cx_a.run_until_parked();
226 let room_b = cx_b
227 .read(ActiveCall::global)
228 .update(cx_b, |call, _| call.room().unwrap().clone());
229 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
230
231 // A tries to grant write access to B, but cannot because B has not
232 // yet signed the zed CLA.
233 active_call_a
234 .update(cx_a, |call, cx| {
235 call.room().unwrap().update(cx, |room, cx| {
236 room.set_participant_role(
237 client_b.user_id().unwrap(),
238 proto::ChannelRole::Member,
239 cx,
240 )
241 })
242 })
243 .await
244 .unwrap_err();
245 cx_a.run_until_parked();
246 assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
247 assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
248
249 // A tries to grant write access to B, but cannot because B has not
250 // yet signed the zed CLA.
251 active_call_a
252 .update(cx_a, |call, cx| {
253 call.room().unwrap().update(cx, |room, cx| {
254 room.set_participant_role(
255 client_b.user_id().unwrap(),
256 proto::ChannelRole::Talker,
257 cx,
258 )
259 })
260 })
261 .await
262 .unwrap();
263 cx_a.run_until_parked();
264 assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
265 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
266
267 // User B signs the zed CLA.
268 server
269 .app_state
270 .db
271 .add_contributor("user_b", 100, None, Utc::now(), None)
272 .await
273 .unwrap();
274
275 // A can now grant write access to B.
276 active_call_a
277 .update(cx_a, |call, cx| {
278 call.room().unwrap().update(cx, |room, cx| {
279 room.set_participant_role(
280 client_b.user_id().unwrap(),
281 proto::ChannelRole::Member,
282 cx,
283 )
284 })
285 })
286 .await
287 .unwrap();
288 cx_a.run_until_parked();
289 assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
290 assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
291}