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