dev_server_tests.rs

  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}