mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix: allow remote browsing and connection checks without a project
This commit is contained in:
@@ -51,6 +51,7 @@ uuid.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ide-ssh = { workspace = true, features = ["testing"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "test-util"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -91,6 +91,7 @@ mod tests {
|
||||
let host = Host::new(HostConfig {
|
||||
config_dir: base.join("config"),
|
||||
data_dir: base.join("data"),
|
||||
local_data_dir: base.join("data"),
|
||||
languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()),
|
||||
syntax: Arc::default(),
|
||||
sink_for: Box::new(|_| Arc::new(|_| {})),
|
||||
|
||||
@@ -222,6 +222,7 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
|
||||
| "index_status"
|
||||
| "jvm_file_presentations"
|
||||
| "deploy_coverage"
|
||||
| "deploy_test_connection"
|
||||
| "find_files"
|
||||
| "find_symbols"
|
||||
| "mark_recent"
|
||||
@@ -243,6 +244,7 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
|
||||
| "remote_current"
|
||||
| "remote_disconnect"
|
||||
| "remote_connect"
|
||||
| "remote_bootstrap_list"
|
||||
| "remote_list"
|
||||
| "remote_open"
|
||||
| "remote_of"
|
||||
|
||||
@@ -56,6 +56,8 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
|
||||
"db_console_path",
|
||||
"db_export_file",
|
||||
"vcs_write_patch",
|
||||
"remote_bootstrap_list",
|
||||
"deploy_test_connection",
|
||||
] {
|
||||
assert!(ide_host::trust::restricted_command_allowed(command), "{command}");
|
||||
for window in ["main", "welcome"] {
|
||||
@@ -75,6 +77,15 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
|
||||
"vcs_git_version",
|
||||
"docker_compose_op",
|
||||
"db_execute",
|
||||
"remote_delete",
|
||||
"remote_mkdir",
|
||||
"remote_rename",
|
||||
"remote_import_local",
|
||||
"remote_bootstrap_exec",
|
||||
"deploy_upload",
|
||||
"deploy_download",
|
||||
"deploy_delete_remote",
|
||||
"deploy_sync",
|
||||
"unknown_command",
|
||||
] {
|
||||
assert!(!ide_host::trust::restricted_command_allowed(command), "{command}");
|
||||
@@ -109,6 +120,55 @@ async fn application_commands_work_in_restricted_projects_and_without_a_project(
|
||||
host.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn welcome_window_browses_selected_ssh_host_without_executing_commands() {
|
||||
use ide_core::remote_settings::{SshAuth, SshConfiguration};
|
||||
use ide_ssh::{FsConnection, SshFiles, SshManager};
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let server_root = tmp.path().join("server");
|
||||
std::fs::create_dir_all(server_root.join("projects/demo")).unwrap();
|
||||
std::fs::write(server_root.join("projects/README.md"), "remote file").unwrap();
|
||||
let port = ide_ssh::testing::sshd(server_root, Arc::new(|cmd| panic!("unexpected SSH exec: {cmd}"))).await;
|
||||
let ssh = Arc::new(SshManager::with_files(
|
||||
tmp.path().join("secrets.json"),
|
||||
SshFiles { known_hosts: tmp.path().join("known_hosts"), config: None },
|
||||
));
|
||||
ssh.set_configs(vec![SshConfiguration {
|
||||
id: "selected-host".into(),
|
||||
host: "127.0.0.1".into(),
|
||||
port: Some(port),
|
||||
user: "dev".into(),
|
||||
auth: SshAuth::Password,
|
||||
..Default::default()
|
||||
}]);
|
||||
ssh.set_secret("selected-host", Some("secret".into()), false);
|
||||
assert!(ssh.session("selected-host").await.is_err());
|
||||
ssh.trust_host("selected-host").await.unwrap();
|
||||
|
||||
let mut host = host(&tmp.path().join("app"), false);
|
||||
let state = Arc::get_mut(&mut host).unwrap();
|
||||
state.ssh = ssh;
|
||||
state.remote = Arc::new(ide_remote::RemoteService::new(state.ssh.clone(), tmp.path().join("cache")));
|
||||
assert!(host.project("welcome").is_err());
|
||||
host.authorize_command("welcome", "remote_bootstrap_list").unwrap();
|
||||
let connection = FsConnection::Sftp { ssh: "selected-host".into() };
|
||||
let entries = host.remote.list(&connection, "/projects").await.unwrap();
|
||||
assert!(entries.iter().any(|e| e.path == "/projects/demo" && e.is_dir));
|
||||
assert!(entries.iter().any(|e| e.name == "README.md" && !e.is_dir));
|
||||
assert!(host.remote.list(&connection, "/projects/demo").await.unwrap().is_empty());
|
||||
host.authorize_command("welcome", "deploy_test_connection").unwrap();
|
||||
host.remote.test_connection(&connection, "/projects").await.unwrap();
|
||||
let local = tmp.path().join("local-deployment");
|
||||
std::fs::create_dir(&local).unwrap();
|
||||
ide_host::ops::deploy::deploy_test_connection(&host, FsConnection::Local, local.to_str().unwrap().into())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(std::fs::read_dir(local).unwrap().count(), 0, "write probe is cleaned up");
|
||||
assert!(host.project("welcome").is_err());
|
||||
host.shutdown().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_user_decisions_locations_revocation_and_one_time_migration() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import type { RemoteEntry, Settings } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { ConnectHost } from "./ConnectDialog";
|
||||
import { closeConnectDialog, openConnectDialog, useConnectDialog } from "./model";
|
||||
|
||||
const { list, openRemote, executeAction } = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
openRemote: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return { ...real, commands: { ...real.commands, remoteBootstrapList: list } };
|
||||
});
|
||||
vi.mock("@/workbench/projectWindow", () => ({ openRemoteProject: openRemote }));
|
||||
vi.mock("@/platform/actions", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/platform/actions")>()),
|
||||
executeAction,
|
||||
}));
|
||||
|
||||
const entry = (path: string, isDir = true): RemoteEntry => ({
|
||||
name: path.slice(path.lastIndexOf("/") + 1),
|
||||
path,
|
||||
isDir,
|
||||
isSymlink: false,
|
||||
size: 0,
|
||||
mtime: null,
|
||||
permissions: "rwxr-xr-x",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
useProject.setState({
|
||||
project: null,
|
||||
opening: false,
|
||||
settings: {
|
||||
ssh: {
|
||||
configs: [
|
||||
{ id: "first", name: "First host", host: "first.example", user: "dev" },
|
||||
{ id: "selected", name: "Selected host", host: "selected.example", user: "dev" },
|
||||
],
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
useRemoteProgress.setState({ progress: null, location: null });
|
||||
openConnectDialog();
|
||||
});
|
||||
|
||||
afterEach(() => closeConnectDialog());
|
||||
|
||||
it("browses the selected SSH host, navigates folders and opens one without a current project", async () => {
|
||||
let finish!: (result: { status: "ok"; data: RemoteEntry[] }) => void;
|
||||
list.mockImplementationOnce(() => new Promise((resolve) => (finish = resolve)));
|
||||
openRemote.mockResolvedValue(true);
|
||||
render(<ConnectHost />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "SSH configuration" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: /Selected host/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Browse" }));
|
||||
expect(list).toHaveBeenCalledWith("selected", "~");
|
||||
expect(screen.getByRole("button", { name: "Browse" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Open" })).toBeDisabled();
|
||||
finish({
|
||||
status: "ok",
|
||||
data: [entry("/home/dev/.cache"), entry("/home/dev/projects"), entry("/home/dev/README.md", false)],
|
||||
});
|
||||
const folders = await screen.findByRole("list", { name: "Remote folders" });
|
||||
expect(
|
||||
within(folders)
|
||||
.getAllByRole("button")
|
||||
.map((button) => button.textContent),
|
||||
).toEqual(["..", "projects", ".cache"]);
|
||||
expect(screen.getByLabelText("Project directory:")).toHaveValue("/home/dev");
|
||||
list.mockResolvedValueOnce({ status: "ok", data: [entry("/home/dev/projects/demo")] });
|
||||
fireEvent.click(within(folders).getByRole("button", { name: "projects" }));
|
||||
await screen.findByRole("button", { name: "demo" });
|
||||
expect(list).toHaveBeenLastCalledWith("selected", "/home/dev/projects");
|
||||
list.mockResolvedValueOnce({ status: "ok", data: [entry("/home/dev/projects")] });
|
||||
fireEvent.click(within(folders).getByRole("button", { name: ".." }));
|
||||
await waitFor(() => expect(screen.getByLabelText("Project directory:")).toHaveValue("/home/dev"));
|
||||
expect(list).toHaveBeenLastCalledWith("selected", "/home/dev");
|
||||
list.mockResolvedValueOnce({ status: "ok", data: [] });
|
||||
fireEvent.click(within(folders).getByRole("button", { name: "projects" }));
|
||||
await waitFor(() => expect(screen.getByLabelText("Project directory:")).toHaveValue("/home/dev/projects"));
|
||||
expect(within(folders).getAllByRole("button")).toHaveLength(1);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open" }));
|
||||
await waitFor(() => expect(openRemote).toHaveBeenCalledWith("selected", "/home/dev/projects"));
|
||||
expect(useConnectDialog.getState().open).toBe(false);
|
||||
expect(useProject.getState().project).toBeNull();
|
||||
});
|
||||
|
||||
it("shows a connection error and lets the welcome window retry Browse", async () => {
|
||||
list.mockRejectedValueOnce(new Error("Connection refused"));
|
||||
render(<ConnectHost />);
|
||||
fireEvent.change(screen.getByLabelText("Project directory:"), { target: { value: " /srv/projects " } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Browse" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Connection refused");
|
||||
expect(list).toHaveBeenCalledWith("first", "/srv/projects");
|
||||
expect(screen.getByRole("button", { name: "Browse" })).toBeEnabled();
|
||||
list.mockResolvedValueOnce({ status: "ok", data: [] });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Browse" }));
|
||||
await screen.findByRole("list", { name: "Remote folders" });
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
expect(openRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("offers SSH configuration from the welcome window and disables Browse until one exists", () => {
|
||||
useProject.setState({ settings: { ssh: { configs: [] } } as unknown as Settings });
|
||||
render(<ConnectHost />);
|
||||
expect(screen.getByRole("button", { name: "Browse" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Open" })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Configure…" }));
|
||||
expect(executeAction).toHaveBeenCalledWith("file.settings", "tools.sshConfigurations");
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in New Issue
Block a user