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