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].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}