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| dev_server_projects::Store::global(cx).clone());
20
21 let resp = store
22 .update(cx, |store, cx| {
23 store.create_dev_server("server-1".to_string(), None, 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_dev_server_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.dev_server_projects();
68 assert_eq!(projects.len(), 1);
69 assert_eq!(projects[0].path, "/remote");
70 workspace::join_dev_server_project(
71 projects[0].project_id.unwrap(),
72 client.app_state.clone(),
73 None,
74 cx,
75 )
76 })
77 .await
78 .unwrap();
79
80 cx.executor().run_until_parked();
81
82 let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
83 cx.simulate_keystrokes("cmd-p 1 enter");
84
85 let editor = remote_workspace
86 .update(cx, |ws, cx| {
87 ws.active_item_as::<Editor>(cx).unwrap().clone()
88 })
89 .unwrap();
90 editor.update(cx, |ed, cx| {
91 assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
92 });
93 cx.simulate_input("wow!");
94 cx.simulate_keystrokes("cmd-s");
95
96 let content = dev_server
97 .fs()
98 .load(&Path::new("/remote/1.txt"))
99 .await
100 .unwrap();
101 assert_eq!(content, "wow!remote\nremote\nremote\n");
102}
103
104#[gpui::test]
105async fn test_dev_server_env_files(
106 cx1: &mut gpui::TestAppContext,
107 cx2: &mut gpui::TestAppContext,
108 cx3: &mut gpui::TestAppContext,
109) {
110 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
111
112 let (_dev_server, remote_workspace) =
113 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
114
115 cx1.executor().run_until_parked();
116
117 let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
118 cx1.simulate_keystrokes("cmd-p . e enter");
119
120 let editor = remote_workspace
121 .update(cx1, |ws, cx| {
122 ws.active_item_as::<Editor>(cx).unwrap().clone()
123 })
124 .unwrap();
125 editor.update(cx1, |ed, cx| {
126 assert_eq!(ed.text(cx).to_string(), "SECRET");
127 });
128
129 cx1.update(|cx| {
130 workspace::join_channel(
131 channel_id,
132 client1.app_state.clone(),
133 Some(remote_workspace),
134 cx,
135 )
136 })
137 .await
138 .unwrap();
139 cx1.executor().run_until_parked();
140
141 remote_workspace
142 .update(cx1, |ws, cx| {
143 assert!(ws.project().read(cx).is_shared());
144 })
145 .unwrap();
146
147 join_channel(channel_id, &client2, cx2).await.unwrap();
148 cx2.executor().run_until_parked();
149
150 let (workspace2, cx2) = client2.active_workspace(cx2);
151 let editor = workspace2.update(cx2, |ws, cx| {
152 ws.active_item_as::<Editor>(cx).unwrap().clone()
153 });
154 // TODO: it'd be nice to hide .env files from other people
155 editor.update(cx2, |ed, cx| {
156 assert_eq!(ed.text(cx).to_string(), "SECRET");
157 });
158}
159
160async fn create_dev_server_project(
161 server: &TestServer,
162 client_app_state: Arc<AppState>,
163 cx: &mut TestAppContext,
164 cx_devserver: &mut TestAppContext,
165) -> (TestClient, WindowHandle<Workspace>) {
166 let store = cx.update(|cx| dev_server_projects::Store::global(cx).clone());
167
168 let resp = store
169 .update(cx, |store, cx| {
170 store.create_dev_server("server-1".to_string(), None, cx)
171 })
172 .await
173 .unwrap();
174 let dev_server = server
175 .create_dev_server(resp.access_token, cx_devserver)
176 .await;
177
178 cx.executor().run_until_parked();
179
180 dev_server
181 .fs()
182 .insert_tree(
183 "/remote",
184 json!({
185 "1.txt": "remote\nremote\nremote",
186 ".env": "SECRET",
187 }),
188 )
189 .await;
190
191 store
192 .update(cx, |store, cx| {
193 store.create_dev_server_project(
194 client::DevServerId(resp.dev_server_id),
195 "/remote".to_string(),
196 cx,
197 )
198 })
199 .await
200 .unwrap();
201
202 cx.executor().run_until_parked();
203
204 let workspace = store
205 .update(cx, |store, cx| {
206 let projects = store.dev_server_projects();
207 assert_eq!(projects.len(), 1);
208 assert_eq!(projects[0].path, "/remote");
209 workspace::join_dev_server_project(
210 projects[0].project_id.unwrap(),
211 client_app_state,
212 None,
213 cx,
214 )
215 })
216 .await
217 .unwrap();
218
219 cx.executor().run_until_parked();
220
221 (dev_server, workspace)
222}
223
224#[gpui::test]
225async fn test_dev_server_leave_room(
226 cx1: &mut gpui::TestAppContext,
227 cx2: &mut gpui::TestAppContext,
228 cx3: &mut gpui::TestAppContext,
229) {
230 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
231
232 let (_dev_server, remote_workspace) =
233 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
234
235 cx1.update(|cx| {
236 workspace::join_channel(
237 channel_id,
238 client1.app_state.clone(),
239 Some(remote_workspace),
240 cx,
241 )
242 })
243 .await
244 .unwrap();
245 cx1.executor().run_until_parked();
246
247 remote_workspace
248 .update(cx1, |ws, cx| {
249 assert!(ws.project().read(cx).is_shared());
250 })
251 .unwrap();
252
253 join_channel(channel_id, &client2, cx2).await.unwrap();
254 cx2.executor().run_until_parked();
255
256 cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx)))
257 .await
258 .unwrap();
259
260 cx1.executor().run_until_parked();
261
262 let (workspace, cx2) = client2.active_workspace(cx2);
263 cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
264}
265
266#[gpui::test]
267async fn test_dev_server_delete(
268 cx1: &mut gpui::TestAppContext,
269 cx2: &mut gpui::TestAppContext,
270 cx3: &mut gpui::TestAppContext,
271) {
272 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
273
274 let (_dev_server, remote_workspace) =
275 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
276
277 cx1.update(|cx| {
278 workspace::join_channel(
279 channel_id,
280 client1.app_state.clone(),
281 Some(remote_workspace),
282 cx,
283 )
284 })
285 .await
286 .unwrap();
287 cx1.executor().run_until_parked();
288
289 remote_workspace
290 .update(cx1, |ws, cx| {
291 assert!(ws.project().read(cx).is_shared());
292 })
293 .unwrap();
294
295 join_channel(channel_id, &client2, cx2).await.unwrap();
296 cx2.executor().run_until_parked();
297
298 cx1.update(|cx| {
299 dev_server_projects::Store::global(cx).update(cx, |store, cx| {
300 store.delete_dev_server_project(store.dev_server_projects().first().unwrap().id, cx)
301 })
302 })
303 .await
304 .unwrap();
305
306 cx1.executor().run_until_parked();
307
308 let (workspace, cx2) = client2.active_workspace(cx2);
309 cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
310
311 cx1.update(|cx| {
312 dev_server_projects::Store::global(cx).update(cx, |store, _| {
313 assert_eq!(store.dev_server_projects().len(), 0);
314 })
315 })
316}
317
318#[gpui::test]
319async fn test_dev_server_rename(
320 cx1: &mut gpui::TestAppContext,
321 cx2: &mut gpui::TestAppContext,
322 cx3: &mut gpui::TestAppContext,
323) {
324 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
325
326 let (_dev_server, remote_workspace) =
327 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
328
329 cx1.update(|cx| {
330 workspace::join_channel(
331 channel_id,
332 client1.app_state.clone(),
333 Some(remote_workspace),
334 cx,
335 )
336 })
337 .await
338 .unwrap();
339 cx1.executor().run_until_parked();
340
341 remote_workspace
342 .update(cx1, |ws, cx| {
343 assert!(ws.project().read(cx).is_shared());
344 })
345 .unwrap();
346
347 join_channel(channel_id, &client2, cx2).await.unwrap();
348 cx2.executor().run_until_parked();
349
350 cx1.update(|cx| {
351 dev_server_projects::Store::global(cx).update(cx, |store, cx| {
352 store.rename_dev_server(
353 store.dev_servers().first().unwrap().id,
354 "name-edited".to_string(),
355 None,
356 cx,
357 )
358 })
359 })
360 .await
361 .unwrap();
362
363 cx1.executor().run_until_parked();
364
365 cx1.update(|cx| {
366 dev_server_projects::Store::global(cx).update(cx, |store, _| {
367 assert_eq!(store.dev_servers().first().unwrap().name, "name-edited");
368 })
369 })
370}
371
372#[gpui::test]
373async fn test_dev_server_refresh_access_token(
374 cx1: &mut gpui::TestAppContext,
375 cx2: &mut gpui::TestAppContext,
376 cx3: &mut gpui::TestAppContext,
377 cx4: &mut gpui::TestAppContext,
378) {
379 let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
380
381 let (_dev_server, remote_workspace) =
382 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
383
384 cx1.update(|cx| {
385 workspace::join_channel(
386 channel_id,
387 client1.app_state.clone(),
388 Some(remote_workspace),
389 cx,
390 )
391 })
392 .await
393 .unwrap();
394 cx1.executor().run_until_parked();
395
396 remote_workspace
397 .update(cx1, |ws, cx| {
398 assert!(ws.project().read(cx).is_shared());
399 })
400 .unwrap();
401
402 join_channel(channel_id, &client2, cx2).await.unwrap();
403 cx2.executor().run_until_parked();
404
405 // Regenerate the access token
406 let new_token_response = cx1
407 .update(|cx| {
408 dev_server_projects::Store::global(cx).update(cx, |store, cx| {
409 store.regenerate_dev_server_token(store.dev_servers().first().unwrap().id, cx)
410 })
411 })
412 .await
413 .unwrap();
414
415 cx1.executor().run_until_parked();
416
417 // Assert that the other client was disconnected
418 let (workspace, cx2) = client2.active_workspace(cx2);
419 cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
420
421 // Assert that the owner of the dev server does not see the dev server as online anymore
422 let (workspace, cx1) = client1.active_workspace(cx1);
423 cx1.update(|cx| {
424 assert!(workspace.read(cx).project().read(cx).is_disconnected());
425 dev_server_projects::Store::global(cx).update(cx, |store, _| {
426 assert_eq!(
427 store.dev_servers().first().unwrap().status,
428 DevServerStatus::Offline
429 );
430 })
431 });
432
433 // Reconnect the dev server with the new token
434 let _dev_server = server
435 .create_dev_server(new_token_response.access_token, cx4)
436 .await;
437
438 cx1.executor().run_until_parked();
439
440 // Assert that the dev server is online again
441 cx1.update(|cx| {
442 dev_server_projects::Store::global(cx).update(cx, |store, _| {
443 assert_eq!(store.dev_servers().len(), 1);
444 assert_eq!(
445 store.dev_servers().first().unwrap().status,
446 DevServerStatus::Online
447 );
448 })
449 });
450}
451
452#[gpui::test]
453async fn test_dev_server_reconnect(
454 cx1: &mut gpui::TestAppContext,
455 cx2: &mut gpui::TestAppContext,
456 cx3: &mut gpui::TestAppContext,
457) {
458 let (mut server, client1) = TestServer::start1(cx1).await;
459 let channel_id = server
460 .make_channel("test", None, (&client1, cx1), &mut [])
461 .await;
462
463 let (_dev_server, remote_workspace) =
464 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
465
466 cx1.update(|cx| {
467 workspace::join_channel(
468 channel_id,
469 client1.app_state.clone(),
470 Some(remote_workspace),
471 cx,
472 )
473 })
474 .await
475 .unwrap();
476 cx1.executor().run_until_parked();
477
478 remote_workspace
479 .update(cx1, |ws, cx| {
480 assert!(ws.project().read(cx).is_shared());
481 })
482 .unwrap();
483
484 drop(client1);
485
486 let client2 = server.create_client(cx2, "user_a").await;
487
488 let store = cx2.update(|cx| dev_server_projects::Store::global(cx).clone());
489
490 store
491 .update(cx2, |store, cx| {
492 let projects = store.dev_server_projects();
493 workspace::join_dev_server_project(
494 projects[0].project_id.unwrap(),
495 client2.app_state.clone(),
496 None,
497 cx,
498 )
499 })
500 .await
501 .unwrap();
502}
503
504#[gpui::test]
505async fn test_create_dev_server_project_path_validation(
506 cx1: &mut gpui::TestAppContext,
507 cx2: &mut gpui::TestAppContext,
508 cx3: &mut gpui::TestAppContext,
509) {
510 let (server, client1) = TestServer::start1(cx1).await;
511 let _channel_id = server
512 .make_channel("test", None, (&client1, cx1), &mut [])
513 .await;
514
515 // Creating a project with a path that does exist should not fail
516 let (_dev_server, _) =
517 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
518
519 cx1.executor().run_until_parked();
520
521 let store = cx1.update(|cx| dev_server_projects::Store::global(cx).clone());
522
523 let resp = store
524 .update(cx1, |store, cx| {
525 store.create_dev_server("server-2".to_string(), None, cx)
526 })
527 .await
528 .unwrap();
529
530 cx1.executor().run_until_parked();
531
532 let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
533
534 cx1.executor().run_until_parked();
535
536 // Creating a remote project with a path that does not exist should fail
537 let result = store
538 .update(cx1, |store, cx| {
539 store.create_dev_server_project(
540 client::DevServerId(resp.dev_server_id),
541 "/notfound".to_string(),
542 cx,
543 )
544 })
545 .await;
546
547 cx1.executor().run_until_parked();
548
549 let error = result.unwrap_err();
550 assert!(matches!(
551 error.error_code(),
552 ErrorCode::DevServerProjectPathDoesNotExist
553 ));
554}
555
556#[gpui::test]
557async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
558 let (server, client1) = TestServer::start1(cx1).await;
559
560 // Creating a project with a path that does exist should not fail
561 let (dev_server, remote_workspace) =
562 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
563
564 let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
565
566 cx.simulate_keystrokes("cmd-p 1 enter");
567 cx.simulate_keystrokes("cmd-shift-s");
568 cx.simulate_input("2.txt");
569 cx.simulate_keystrokes("enter");
570
571 cx.executor().run_until_parked();
572
573 let title = remote_workspace
574 .update(&mut cx, |ws, cx| {
575 ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
576 })
577 .unwrap();
578
579 assert_eq!(title, "2.txt");
580
581 let path = Path::new("/remote/2.txt");
582 assert_eq!(
583 dev_server.fs().load(&path).await.unwrap(),
584 "remote\nremote\nremote"
585 );
586}
587
588#[gpui::test]
589async fn test_new_file_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
590 let (server, client1) = TestServer::start1(cx1).await;
591
592 // Creating a project with a path that does exist should not fail
593 let (dev_server, remote_workspace) =
594 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
595
596 let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
597
598 cx.simulate_keystrokes("cmd-n");
599 cx.simulate_input("new!");
600 cx.simulate_keystrokes("cmd-shift-s");
601 cx.simulate_input("2.txt");
602 cx.simulate_keystrokes("enter");
603
604 cx.executor().run_until_parked();
605
606 let title = remote_workspace
607 .update(&mut cx, |ws, cx| {
608 ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
609 })
610 .unwrap();
611
612 assert_eq!(title, "2.txt");
613
614 let path = Path::new("/remote/2.txt");
615 assert_eq!(dev_server.fs().load(&path).await.unwrap(), "new!");
616}