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| remote_projects::Store::global(cx).clone());
 20
 21    let resp = store
 22        .update(cx, |store, cx| {
 23            store.create_dev_server("server-1".to_string(), 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_remote_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.remote_projects();
 68            assert_eq!(projects.len(), 1);
 69            assert_eq!(projects[0].path, "/remote");
 70            workspace::join_remote_project(
 71                projects[0].project_id.unwrap(),
 72                client.app_state.clone(),
 73                cx,
 74            )
 75        })
 76        .await
 77        .unwrap();
 78
 79    cx.executor().run_until_parked();
 80
 81    let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
 82    cx.simulate_keystrokes("cmd-p 1 enter");
 83
 84    let editor = remote_workspace
 85        .update(cx, |ws, cx| {
 86            ws.active_item_as::<Editor>(cx).unwrap().clone()
 87        })
 88        .unwrap();
 89    editor.update(cx, |ed, cx| {
 90        assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
 91    });
 92    cx.simulate_input("wow!");
 93    cx.simulate_keystrokes("cmd-s");
 94
 95    let content = dev_server
 96        .fs()
 97        .load(&Path::new("/remote/1.txt"))
 98        .await
 99        .unwrap();
100    assert_eq!(content, "wow!remote\nremote\nremote\n");
101}
102
103#[gpui::test]
104async fn test_dev_server_env_files(
105    cx1: &mut gpui::TestAppContext,
106    cx2: &mut gpui::TestAppContext,
107    cx3: &mut gpui::TestAppContext,
108) {
109    let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
110
111    let (_dev_server, remote_workspace) =
112        create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
113
114    cx1.executor().run_until_parked();
115
116    let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
117    cx1.simulate_keystrokes("cmd-p . e enter");
118
119    let editor = remote_workspace
120        .update(cx1, |ws, cx| {
121            ws.active_item_as::<Editor>(cx).unwrap().clone()
122        })
123        .unwrap();
124    editor.update(cx1, |ed, cx| {
125        assert_eq!(ed.text(cx).to_string(), "SECRET");
126    });
127
128    cx1.update(|cx| {
129        workspace::join_channel(
130            channel_id,
131            client1.app_state.clone(),
132            Some(remote_workspace),
133            cx,
134        )
135    })
136    .await
137    .unwrap();
138    cx1.executor().run_until_parked();
139
140    remote_workspace
141        .update(cx1, |ws, cx| {
142            assert!(ws.project().read(cx).is_shared());
143        })
144        .unwrap();
145
146    join_channel(channel_id, &client2, cx2).await.unwrap();
147    cx2.executor().run_until_parked();
148
149    let (workspace2, cx2) = client2.active_workspace(cx2);
150    let editor = workspace2.update(cx2, |ws, cx| {
151        ws.active_item_as::<Editor>(cx).unwrap().clone()
152    });
153    // TODO: it'd be nice to hide .env files from other people
154    editor.update(cx2, |ed, cx| {
155        assert_eq!(ed.text(cx).to_string(), "SECRET");
156    });
157}
158
159async fn create_remote_project(
160    server: &TestServer,
161    client_app_state: Arc<AppState>,
162    cx: &mut TestAppContext,
163    cx_devserver: &mut TestAppContext,
164) -> (TestClient, WindowHandle<Workspace>) {
165    let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
166
167    let resp = store
168        .update(cx, |store, cx| {
169            store.create_dev_server("server-1".to_string(), cx)
170        })
171        .await
172        .unwrap();
173    let dev_server = server
174        .create_dev_server(resp.access_token, cx_devserver)
175        .await;
176
177    cx.executor().run_until_parked();
178
179    dev_server
180        .fs()
181        .insert_tree(
182            "/remote",
183            json!({
184                "1.txt": "remote\nremote\nremote",
185                ".env": "SECRET",
186            }),
187        )
188        .await;
189
190    store
191        .update(cx, |store, cx| {
192            store.create_remote_project(
193                client::DevServerId(resp.dev_server_id),
194                "/remote".to_string(),
195                cx,
196            )
197        })
198        .await
199        .unwrap();
200
201    cx.executor().run_until_parked();
202
203    let workspace = store
204        .update(cx, |store, cx| {
205            let projects = store.remote_projects();
206            assert_eq!(projects.len(), 1);
207            assert_eq!(projects[0].path, "/remote");
208            workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
209        })
210        .await
211        .unwrap();
212
213    cx.executor().run_until_parked();
214
215    (dev_server, workspace)
216}
217
218#[gpui::test]
219async fn test_dev_server_leave_room(
220    cx1: &mut gpui::TestAppContext,
221    cx2: &mut gpui::TestAppContext,
222    cx3: &mut gpui::TestAppContext,
223) {
224    let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
225
226    let (_dev_server, remote_workspace) =
227        create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
228
229    cx1.update(|cx| {
230        workspace::join_channel(
231            channel_id,
232            client1.app_state.clone(),
233            Some(remote_workspace),
234            cx,
235        )
236    })
237    .await
238    .unwrap();
239    cx1.executor().run_until_parked();
240
241    remote_workspace
242        .update(cx1, |ws, cx| {
243            assert!(ws.project().read(cx).is_shared());
244        })
245        .unwrap();
246
247    join_channel(channel_id, &client2, cx2).await.unwrap();
248    cx2.executor().run_until_parked();
249
250    cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx)))
251        .await
252        .unwrap();
253
254    cx1.executor().run_until_parked();
255
256    let (workspace, cx2) = client2.active_workspace(cx2);
257    cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
258}
259
260#[gpui::test]
261async fn test_dev_server_reconnect(
262    cx1: &mut gpui::TestAppContext,
263    cx2: &mut gpui::TestAppContext,
264    cx3: &mut gpui::TestAppContext,
265) {
266    let (mut server, client1) = TestServer::start1(cx1).await;
267    let channel_id = server
268        .make_channel("test", None, (&client1, cx1), &mut [])
269        .await;
270
271    let (_dev_server, remote_workspace) =
272        create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
273
274    cx1.update(|cx| {
275        workspace::join_channel(
276            channel_id,
277            client1.app_state.clone(),
278            Some(remote_workspace),
279            cx,
280        )
281    })
282    .await
283    .unwrap();
284    cx1.executor().run_until_parked();
285
286    remote_workspace
287        .update(cx1, |ws, cx| {
288            assert!(ws.project().read(cx).is_shared());
289        })
290        .unwrap();
291
292    drop(client1);
293
294    let client2 = server.create_client(cx2, "user_a").await;
295
296    let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
297
298    store
299        .update(cx2, |store, cx| {
300            let projects = store.remote_projects();
301            workspace::join_remote_project(
302                projects[0].project_id.unwrap(),
303                client2.app_state.clone(),
304                cx,
305            )
306        })
307        .await
308        .unwrap();
309}
310
311#[gpui::test]
312async fn test_create_remote_project_path_validation(
313    cx1: &mut gpui::TestAppContext,
314    cx2: &mut gpui::TestAppContext,
315    cx3: &mut gpui::TestAppContext,
316) {
317    let (server, client1) = TestServer::start1(cx1).await;
318    let _channel_id = server
319        .make_channel("test", None, (&client1, cx1), &mut [])
320        .await;
321
322    // Creating a project with a path that does exist should not fail
323    let (_dev_server, _) =
324        create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
325
326    cx1.executor().run_until_parked();
327
328    let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
329
330    let resp = store
331        .update(cx1, |store, cx| {
332            store.create_dev_server("server-2".to_string(), cx)
333        })
334        .await
335        .unwrap();
336
337    cx1.executor().run_until_parked();
338
339    let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
340
341    cx1.executor().run_until_parked();
342
343    // Creating a remote project with a path that does not exist should fail
344    let result = store
345        .update(cx1, |store, cx| {
346            store.create_remote_project(
347                client::DevServerId(resp.dev_server_id),
348                "/notfound".to_string(),
349                cx,
350            )
351        })
352        .await;
353
354    cx1.executor().run_until_parked();
355
356    let error = result.unwrap_err();
357    assert!(matches!(
358        error.error_code(),
359        ErrorCode::RemoteProjectPathDoesNotExist
360    ));
361}