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].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].path, "/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_create_dev_server_project_path_validation(
509 cx1: &mut gpui::TestAppContext,
510 cx2: &mut gpui::TestAppContext,
511 cx3: &mut gpui::TestAppContext,
512) {
513 let (server, client1) = TestServer::start1(cx1).await;
514 let _channel_id = server
515 .make_channel("test", None, (&client1, cx1), &mut [])
516 .await;
517
518 // Creating a project with a path that does exist should not fail
519 let (_dev_server, _) =
520 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
521
522 cx1.executor().run_until_parked();
523
524 let store = cx1.update(|cx| dev_server_projects::Store::global(cx).clone());
525
526 let resp = store
527 .update(cx1, |store, cx| {
528 store.create_dev_server("server-2".to_string(), None, cx)
529 })
530 .await
531 .unwrap();
532
533 cx1.executor().run_until_parked();
534
535 let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
536
537 cx1.executor().run_until_parked();
538
539 // Creating a remote project with a path that does not exist should fail
540 let result = store
541 .update(cx1, |store, cx| {
542 store.create_dev_server_project(
543 client::DevServerId(resp.dev_server_id),
544 "/notfound".to_string(),
545 cx,
546 )
547 })
548 .await;
549
550 cx1.executor().run_until_parked();
551
552 let error = result.unwrap_err();
553 assert!(matches!(
554 error.error_code(),
555 ErrorCode::DevServerProjectPathDoesNotExist
556 ));
557}
558
559#[gpui::test]
560async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
561 let (server, client1) = TestServer::start1(cx1).await;
562
563 // Creating a project with a path that does exist should not fail
564 let (dev_server, remote_workspace) =
565 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
566
567 let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
568
569 cx.simulate_keystrokes("cmd-p 1 enter");
570 cx.simulate_keystrokes("cmd-shift-s");
571 cx.simulate_input("2.txt");
572 cx.simulate_keystrokes("enter");
573
574 cx.executor().run_until_parked();
575
576 let title = remote_workspace
577 .update(&mut cx, |ws, cx| {
578 let active_item = ws.active_item(cx).unwrap();
579 active_item.tab_description(0, &cx).unwrap()
580 })
581 .unwrap();
582
583 assert_eq!(title, "2.txt");
584
585 let path = Path::new("/remote/2.txt");
586 assert_eq!(
587 dev_server.fs().load(&path).await.unwrap(),
588 "remote\nremote\nremote"
589 );
590}
591
592#[gpui::test]
593async fn test_new_file_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
594 let (server, client1) = TestServer::start1(cx1).await;
595
596 // Creating a project with a path that does exist should not fail
597 let (dev_server, remote_workspace) =
598 create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
599
600 let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
601
602 cx.simulate_keystrokes("cmd-n");
603 cx.simulate_input("new!");
604 cx.simulate_keystrokes("cmd-shift-s");
605 cx.simulate_input("2.txt");
606 cx.simulate_keystrokes("enter");
607
608 cx.executor().run_until_parked();
609
610 let title = remote_workspace
611 .update(&mut cx, |ws, cx| {
612 ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
613 })
614 .unwrap();
615
616 assert_eq!(title, "2.txt");
617
618 let path = Path::new("/remote/2.txt");
619 assert_eq!(dev_server.fs().load(&path).await.unwrap(), "new!");
620}