mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
A mapping's local path may now be an absolute path inside the project (the form IntelliJ writes), and a deployment path of . means the server root; absolute paths outside the project are kept as they are. The project-tree Deployment submenu names the default server (Upload to <server>, Download from <server>, Sync with Deployed to <server>…) and gains Sync with Deployed to Default Server. Remote Host downloads use the backend-resolved mapping, which fixes downloads under symlinked or case-different paths. Fixes #877 Co-authored-by: florentvdry <[email protected]>
176 lines
7.7 KiB
TypeScript
176 lines
7.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { SyncEntry } from "@/platform/ipc";
|
|
import { DEFAULT_DEPLOYMENT, DEFAULT_DEPLOYMENT_SERVER } from "@/platform/remoteSettings";
|
|
import {
|
|
defaultSyncAction,
|
|
initialSyncChoices,
|
|
joinRemote,
|
|
localPathForRemote,
|
|
pathsFromArg,
|
|
remoteName,
|
|
remoteParent,
|
|
resolveServer,
|
|
sshRemedy,
|
|
summarizeReport,
|
|
transferMenuEntries,
|
|
syncChoices,
|
|
syncSteps,
|
|
targetPaths,
|
|
} from "./model";
|
|
|
|
const ROOT = "/work/app";
|
|
|
|
describe("targetPaths", () => {
|
|
const ctx = { part: null, selection: [], editorPath: null, projectRoot: ROOT } as const;
|
|
|
|
it("reads paths passed by menus", () => {
|
|
expect(pathsFromArg(`${ROOT}/a.php`)).toEqual([`${ROOT}/a.php`]);
|
|
expect(pathsFromArg([`${ROOT}/a`, 3, ""])).toEqual([`${ROOT}/a`]);
|
|
expect(pathsFromArg({ paths: [`${ROOT}/b`] })).toEqual([`${ROOT}/b`]);
|
|
expect(pathsFromArg(undefined)).toBeNull();
|
|
expect(pathsFromArg({ paths: [] })).toBeNull();
|
|
});
|
|
|
|
it("prefers the passed paths over the selection and the editor", () => {
|
|
expect(
|
|
targetPaths(`${ROOT}/x`, { ...ctx, part: "project", selection: [`${ROOT}/y`], editorPath: `${ROOT}/z` }),
|
|
).toEqual([`${ROOT}/x`]);
|
|
});
|
|
|
|
it("uses the project view selection when it had focus last, else the editor file", () => {
|
|
const both = { ...ctx, selection: [`${ROOT}/src`], editorPath: `${ROOT}/index.php` };
|
|
expect(targetPaths(undefined, { ...both, part: "project" })).toEqual([`${ROOT}/src`]);
|
|
expect(targetPaths(undefined, { ...both, part: "editor" })).toEqual([`${ROOT}/index.php`]);
|
|
expect(targetPaths(undefined, { ...ctx, part: "editor", selection: [`${ROOT}/src`] })).toEqual([]);
|
|
});
|
|
|
|
it("drops paths outside the project and paths inside selected folders", () => {
|
|
expect(targetPaths([`${ROOT}/src`, `${ROOT}/src/a.php`, "/elsewhere/b", `${ROOT}/srcx`], ctx)).toEqual([
|
|
`${ROOT}/src`,
|
|
`${ROOT}/srcx`,
|
|
]);
|
|
expect(targetPaths(`${ROOT}/a`, { ...ctx, projectRoot: null })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("sync defaults", () => {
|
|
it("picks the action from the status", () => {
|
|
expect(defaultSyncAction("localOnly")).toBe("upload");
|
|
expect(defaultSyncAction("localNewer")).toBe("upload");
|
|
expect(defaultSyncAction("different")).toBe("upload");
|
|
expect(defaultSyncAction("remoteOnly")).toBe("download");
|
|
expect(defaultSyncAction("remoteNewer")).toBe("download");
|
|
expect(defaultSyncAction("same")).toBe("skip");
|
|
});
|
|
|
|
const entry = (path: string, status: SyncEntry["status"], local: boolean, remote: boolean): SyncEntry => ({
|
|
path,
|
|
remotePath: `/srv/${path}`,
|
|
local: local ? { size: 1, mtime: 1 } : null,
|
|
remote: remote ? { size: 2, mtime: 2 } : null,
|
|
status,
|
|
});
|
|
|
|
it("offers only actions possible for the row", () => {
|
|
expect(syncChoices(entry("a", "localOnly", true, false))).toEqual(["skip", "upload", "deleteLocal"]);
|
|
expect(syncChoices(entry("b", "remoteOnly", false, true))).toEqual(["skip", "download", "deleteRemote"]);
|
|
});
|
|
|
|
it("turns choices into steps without the skipped rows", () => {
|
|
const entries = [
|
|
entry("a", "localOnly", true, false),
|
|
entry("b", "same", true, true),
|
|
entry("c", "remoteNewer", true, true),
|
|
];
|
|
const choices = initialSyncChoices(entries);
|
|
expect(choices).toEqual({ a: "upload", b: "skip", c: "download" });
|
|
expect(syncSteps({ ...choices, b: "deleteRemote" })).toEqual([
|
|
{ path: "a", action: "upload" },
|
|
{ path: "b", action: "deleteRemote" },
|
|
{ path: "c", action: "download" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("servers and reports", () => {
|
|
const a = { ...DEFAULT_DEPLOYMENT_SERVER, id: "a", name: "Staging" };
|
|
const b = { ...DEFAULT_DEPLOYMENT_SERVER, id: "b", name: "" };
|
|
|
|
it("resolves the default server", () => {
|
|
expect(resolveServer({ ...DEFAULT_DEPLOYMENT, servers: [a, b], defaultServer: "b" }, null)?.id).toBe("b");
|
|
expect(resolveServer({ ...DEFAULT_DEPLOYMENT, servers: [a, b], defaultServer: "" }, null)).toBeNull();
|
|
expect(resolveServer({ ...DEFAULT_DEPLOYMENT, servers: [a], defaultServer: "" }, null)?.id).toBe("a");
|
|
expect(resolveServer({ ...DEFAULT_DEPLOYMENT, servers: [a, b], defaultServer: "b" }, "a")?.id).toBe("a");
|
|
});
|
|
|
|
it("names the menu's transfers after the default server", () => {
|
|
const actions = (servers: (typeof a)[], defaultServer: string) =>
|
|
transferMenuEntries({ ...DEFAULT_DEPLOYMENT, servers, defaultServer }).map((e) => e.label ?? e.action);
|
|
const named = ["Upload to Staging", "Download from Staging", "Sync with Deployed to Staging…"];
|
|
const choosers = ["deployment.uploadTo", "deployment.downloadFrom", "deployment.syncWith"];
|
|
expect(actions([a], "")).toEqual(named);
|
|
expect(actions([a, b], "a")).toEqual([...named, ...choosers]);
|
|
expect(actions([a, b], "")).toEqual(choosers);
|
|
});
|
|
|
|
it("summarizes transfer reports", () => {
|
|
expect(summarizeReport({ files: 3, skipped: 1, errors: [] }, "upload", "Staging")).toEqual({
|
|
level: "info",
|
|
title: "Uploaded {n} files to {server}",
|
|
params: { n: 3, server: "Staging", skipped: 1 },
|
|
});
|
|
expect(
|
|
summarizeReport({ files: 2, skipped: 0, errors: [{ path: "x", message: "denied" }] }, "download", "S").level,
|
|
).toBe("warning");
|
|
expect(
|
|
summarizeReport({ files: 0, skipped: 0, errors: [{ path: "x", message: "denied" }] }, "sync", "S").level,
|
|
).toBe("error");
|
|
});
|
|
|
|
it("classifies SSH failures", () => {
|
|
expect(sshRemedy("protocol", "Unknown SSH host key for example.com", "openSsh")).toBe("trustHostKey");
|
|
expect(sshRemedy("permissionDenied", "SSH password required for [email protected]", "password")).toBe("secret");
|
|
expect(sshRemedy("permissionDenied", "SSH key passphrase required for ~/.ssh/id", "keyPair")).toBe("secret");
|
|
expect(sshRemedy("permissionDenied", "SSH host key of box:22 has changed", "password")).toBeNull();
|
|
expect(sshRemedy("permissionDenied", "SSH authentication failed", "openSsh")).toBeNull();
|
|
expect(sshRemedy("permissionDenied", "SSH authentication failed", "password")).toBe("secret");
|
|
expect(sshRemedy("io", "Connection refused", "password")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("remote paths", () => {
|
|
it("joins and splits remote paths", () => {
|
|
expect(joinRemote("/var/www", "app/")).toBe("/var/www/app");
|
|
expect(joinRemote("/", "etc")).toBe("/etc");
|
|
expect(joinRemote("/var/www", "")).toBe("/var/www");
|
|
expect(remoteParent("/var/www/app")).toBe("/var/www");
|
|
expect(remoteParent("/var")).toBe("/");
|
|
expect(remoteParent("/")).toBe("/");
|
|
expect(remoteName("/var/www/app/")).toBe("app");
|
|
});
|
|
|
|
it("maps remote paths back to project paths through the most specific mapping", () => {
|
|
const mappings = [
|
|
{ local: ROOT, deployment: "/var/www" },
|
|
{ local: `${ROOT}/public`, deployment: "/var/www/htdocs" },
|
|
{ local: `${ROOT}/shared`, deployment: "/var/www/opt/shared" },
|
|
];
|
|
const map = (p: string) => localPathForRemote(p, mappings);
|
|
expect(map("/var/www/index.php")).toBe(`${ROOT}/index.php`);
|
|
expect(map("/var/www/htdocs/css/a.css")).toBe(`${ROOT}/public/css/a.css`);
|
|
expect(map("/var/www/htdocs")).toBe(`${ROOT}/public`);
|
|
expect(map("/var/www/opt/shared/lib")).toBe(`${ROOT}/shared/lib`);
|
|
expect(map("/opt/shared/lib")).toBeNull();
|
|
expect(map("/var/wwwx/a")).toBeNull();
|
|
expect(map("/etc/passwd")).toBeNull();
|
|
});
|
|
|
|
it("uses the backend folder for aliases and does not invent mappings", () => {
|
|
const mappings = [{ local: `${ROOT}/src`, deployment: "/mnt/deploy" }];
|
|
expect(localPathForRemote("/mnt/deploy/a.txt", mappings)).toBe(`${ROOT}/src/a.txt`);
|
|
expect(localPathForRemote("/mnt/deploy/a.txt", [])).toBeNull();
|
|
expect(localPathForRemote("/mnt/deploy/../a.txt", mappings)).toBeNull();
|
|
expect(localPathForRemote("/a.txt", [{ local: ROOT, deployment: "/" }])).toBe(`${ROOT}/a.txt`);
|
|
});
|
|
});
|