1use std::{path::Path, sync::Arc};
2
3use call::ActiveCall;
4use editor::Editor;
5use fs::Fs;
6use gpui::{TestAppContext, VisualTestContext, WindowHandle};
7use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
8use serde_json::json;
9use workspace::{AppState, Workspace};
10
11use crate::tests::{following_tests::join_channel, TestServer};
12
13use super::TestClient;
14
15#[gpui::test]
16async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
17 let (server, client) = TestServer::start1(cx).await;
18
19 let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
20
21 let resp = store
22 .update(cx, |store, cx| {
23 store.create_dev_server("server-1".to_string(), cx)
24 })
25 .await
26 .unwrap();
27
28 store.update(cx, |store, _| {
29 assert_eq!(store.dev_servers().len(), 1);
30 assert_eq!(store.dev_servers()[0].name, "server-1");
31 assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline);
32 });
33
34 let dev_server = server.create_dev_server(resp.access_token, cx2).await;
35 cx.executor().run_until_parked();
36 store.update(cx, |store, _| {
37 assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online);
38 });
39
40 dev_server
41 .fs()
42 .insert_tree(
43 "/remote",
44 json!({
45 "1.txt": "remote\nremote\nremote",
46 "2.js": "function two() { return 2; }",
47 "3.rs": "mod test",
48 }),
49 )
50 .await;
51
52 store
53 .update(cx, |store, cx| {
54 store.create_remote_project(
55 client::DevServerId(resp.dev_server_id),
56 "/remote".to_string(),
57 cx,
58 )
59 })
60 .await
61 .unwrap();
62
63 cx.executor().run_until_parked();
64
65 let remote_workspace = store
66 .update(cx, |store, cx| {
67 let projects = store.remote_projects();
68 assert_eq!(projects.len(), 1);
69 assert_eq!(projects[0].path, "/remote");
70 workspace::join_remote_project(
71 projects[0].project_id.unwrap(),
72 client.app_state.clone(),
73 cx,
74 )
75 })
76 .await
77 .unwrap();
78
79 cx.executor().run_until_parked();
80
81 let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
82 cx.simulate_keystrokes("cmd-p 1 enter");
83
84 let editor = remote_workspace
85 .update(cx, |ws, cx| {
86 ws.active_item_as::<Editor>(cx).unwrap().clone()
87 })
88 .unwrap();
89 editor.update(cx, |ed, cx| {
90 assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
91 });
92 cx.simulate_input("wow!");
93 cx.simulate_keystrokes("cmd-s");
94
95 let content = dev_server
96 .fs()
97 .load(&Path::new("/remote/1.txt"))
98 .await
99 .unwrap();
100 assert_eq!(content, "wow!remote\nremote\nremote\n");
101}
102
103#[gpui::test]
104async fn test_dev_server_env_files(
105 cx1: &mut gpui::TestAppContext,
106 cx2: &mut gpui::TestAppContext,
107 cx3: &mut gpui::TestAppContext,
108) {
109 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
110
111 let (_dev_server, remote_workspace) =
112 create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
113
114 cx1.executor().run_until_parked();
115
116 let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
117 cx1.simulate_keystrokes("cmd-p . e enter");
118
119 let editor = remote_workspace
120 .update(cx1, |ws, cx| {
121 ws.active_item_as::<Editor>(cx).unwrap().clone()
122 })
123 .unwrap();
124 editor.update(cx1, |ed, cx| {
125 assert_eq!(ed.text(cx).to_string(), "SECRET");
126 });
127
128 cx1.update(|cx| {
129 workspace::join_channel(
130 channel_id,
131 client1.app_state.clone(),
132 Some(remote_workspace),
133 cx,
134 )
135 })
136 .await
137 .unwrap();
138 cx1.executor().run_until_parked();
139
140 remote_workspace
141 .update(cx1, |ws, cx| {
142 assert!(ws.project().read(cx).is_shared());
143 })
144 .unwrap();
145
146 join_channel(channel_id, &client2, cx2).await.unwrap();
147 cx2.executor().run_until_parked();
148
149 let (workspace2, cx2) = client2.active_workspace(cx2);
150 let editor = workspace2.update(cx2, |ws, cx| {
151 ws.active_item_as::<Editor>(cx).unwrap().clone()
152 });
153 // TODO: it'd be nice to hide .env files from other people
154 editor.update(cx2, |ed, cx| {
155 assert_eq!(ed.text(cx).to_string(), "SECRET");
156 });
157}
158
159async fn create_remote_project(
160 server: &TestServer,
161 client_app_state: Arc<AppState>,
162 cx: &mut TestAppContext,
163 cx_devserver: &mut TestAppContext,
164) -> (TestClient, WindowHandle<Workspace>) {
165 let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
166
167 let resp = store
168 .update(cx, |store, cx| {
169 store.create_dev_server("server-1".to_string(), cx)
170 })
171 .await
172 .unwrap();
173 let dev_server = server
174 .create_dev_server(resp.access_token, cx_devserver)
175 .await;
176
177 cx.executor().run_until_parked();
178
179 dev_server
180 .fs()
181 .insert_tree(
182 "/remote",
183 json!({
184 "1.txt": "remote\nremote\nremote",
185 ".env": "SECRET",
186 }),
187 )
188 .await;
189
190 store
191 .update(cx, |store, cx| {
192 store.create_remote_project(
193 client::DevServerId(resp.dev_server_id),
194 "/remote".to_string(),
195 cx,
196 )
197 })
198 .await
199 .unwrap();
200
201 cx.executor().run_until_parked();
202
203 let workspace = store
204 .update(cx, |store, cx| {
205 let projects = store.remote_projects();
206 assert_eq!(projects.len(), 1);
207 assert_eq!(projects[0].path, "/remote");
208 workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
209 })
210 .await
211 .unwrap();
212
213 cx.executor().run_until_parked();
214
215 (dev_server, workspace)
216}
217
218#[gpui::test]
219async fn test_dev_server_leave_room(
220 cx1: &mut gpui::TestAppContext,
221 cx2: &mut gpui::TestAppContext,
222 cx3: &mut gpui::TestAppContext,
223) {
224 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
225
226 let (_dev_server, remote_workspace) =
227 create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
228
229 cx1.update(|cx| {
230 workspace::join_channel(
231 channel_id,
232 client1.app_state.clone(),
233 Some(remote_workspace),
234 cx,
235 )
236 })
237 .await
238 .unwrap();
239 cx1.executor().run_until_parked();
240
241 remote_workspace
242 .update(cx1, |ws, cx| {
243 assert!(ws.project().read(cx).is_shared());
244 })
245 .unwrap();
246
247 join_channel(channel_id, &client2, cx2).await.unwrap();
248 cx2.executor().run_until_parked();
249
250 cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx)))
251 .await
252 .unwrap();
253
254 cx1.executor().run_until_parked();
255
256 let (workspace, cx2) = client2.active_workspace(cx2);
257 cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
258}
259
260#[gpui::test]
261async fn test_dev_server_reconnect(
262 cx1: &mut gpui::TestAppContext,
263 cx2: &mut gpui::TestAppContext,
264 cx3: &mut gpui::TestAppContext,
265) {
266 let (mut server, client1) = TestServer::start1(cx1).await;
267 let channel_id = server
268 .make_channel("test", None, (&client1, cx1), &mut [])
269 .await;
270
271 let (_dev_server, remote_workspace) =
272 create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
273
274 cx1.update(|cx| {
275 workspace::join_channel(
276 channel_id,
277 client1.app_state.clone(),
278 Some(remote_workspace),
279 cx,
280 )
281 })
282 .await
283 .unwrap();
284 cx1.executor().run_until_parked();
285
286 remote_workspace
287 .update(cx1, |ws, cx| {
288 assert!(ws.project().read(cx).is_shared());
289 })
290 .unwrap();
291
292 drop(client1);
293
294 let client2 = server.create_client(cx2, "user_a").await;
295
296 let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
297
298 store
299 .update(cx2, |store, cx| {
300 let projects = store.remote_projects();
301 workspace::join_remote_project(
302 projects[0].project_id.unwrap(),
303 client2.app_state.clone(),
304 cx,
305 )
306 })
307 .await
308 .unwrap();
309}
310
311#[gpui::test]
312async fn test_create_remote_project_path_validation(
313 cx1: &mut gpui::TestAppContext,
314 cx2: &mut gpui::TestAppContext,
315 cx3: &mut gpui::TestAppContext,
316) {
317 let (server, client1) = TestServer::start1(cx1).await;
318 let _channel_id = server
319 .make_channel("test", None, (&client1, cx1), &mut [])
320 .await;
321
322 // Creating a project with a path that does exist should not fail
323 let (_dev_server, _) =
324 create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
325
326 cx1.executor().run_until_parked();
327
328 let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
329
330 let resp = store
331 .update(cx1, |store, cx| {
332 store.create_dev_server("server-2".to_string(), cx)
333 })
334 .await
335 .unwrap();
336
337 cx1.executor().run_until_parked();
338
339 let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
340
341 cx1.executor().run_until_parked();
342
343 // Creating a remote project with a path that does not exist should fail
344 let result = store
345 .update(cx1, |store, cx| {
346 store.create_remote_project(
347 client::DevServerId(resp.dev_server_id),
348 "/notfound".to_string(),
349 cx,
350 )
351 })
352 .await;
353
354 cx1.executor().run_until_parked();
355
356 let error = result.unwrap_err();
357 assert!(matches!(
358 error.error_code(),
359 ErrorCode::RemoteProjectPathDoesNotExist
360 ));
361}