mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
186 lines
4.8 KiB
TypeScript
186 lines
4.8 KiB
TypeScript
/**
|
|
* The tunnel host-columns migration.
|
|
*
|
|
* An upgrade must be lossless: a host with tunnels on and a saved tunnel list
|
|
* must keep both once the ssh_data columns are dropped. Running it twice
|
|
* must not duplicate or clobber what it moved.
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
interface HostRow {
|
|
id: number;
|
|
enable_tunnel: boolean;
|
|
tunnel_connections: string | null;
|
|
}
|
|
|
|
interface SettingsRow {
|
|
pluginId: string;
|
|
scope: string;
|
|
scopeId: string | null;
|
|
key: string;
|
|
value: string | null;
|
|
}
|
|
|
|
const hostRows: HostRow[] = [];
|
|
const settingsRows: SettingsRow[] = [];
|
|
|
|
const find = (
|
|
pluginId: string,
|
|
scope: string,
|
|
scopeId: string | null,
|
|
key: string,
|
|
) =>
|
|
settingsRows.find(
|
|
(row) =>
|
|
row.pluginId === pluginId &&
|
|
row.scope === scope &&
|
|
row.scopeId === scopeId &&
|
|
row.key === key,
|
|
);
|
|
|
|
const pluginSettingsRepository = {
|
|
get: async (
|
|
pluginId: string,
|
|
scope: string,
|
|
scopeId: string | null,
|
|
key: string,
|
|
) => find(pluginId, scope, scopeId, key) ?? null,
|
|
set: async (
|
|
pluginId: string,
|
|
scope: string,
|
|
scopeId: string | null,
|
|
key: string,
|
|
value: string | null,
|
|
) => {
|
|
const existing = find(pluginId, scope, scopeId, key);
|
|
if (existing) {
|
|
existing.value = value;
|
|
return;
|
|
}
|
|
settingsRows.push({ pluginId, scope, scopeId, key, value });
|
|
},
|
|
};
|
|
|
|
vi.mock("../../database/repositories/factory.js", () => ({
|
|
createCurrentPluginSettingsRepository: () => pluginSettingsRepository,
|
|
}));
|
|
|
|
vi.mock("../../database/db/index.js", () => ({
|
|
getDb: () => ({
|
|
all: async () =>
|
|
hostRows.filter(
|
|
(row) => row.enable_tunnel || row.tunnel_connections !== null,
|
|
),
|
|
}),
|
|
}));
|
|
|
|
const { runTunnelsSettingsMigration } =
|
|
await import("../../utils/crypto-migration/tunnels-settings-migration.js");
|
|
|
|
function storedValue(hostId: number, key: string): unknown {
|
|
const row = settingsRows.find(
|
|
(entry) => entry.scopeId === String(hostId) && entry.key === key,
|
|
);
|
|
return row?.value === null || row?.value === undefined
|
|
? undefined
|
|
: JSON.parse(row.value);
|
|
}
|
|
|
|
const tunnel = {
|
|
scope: "s2s",
|
|
mode: "local",
|
|
sourcePort: 8080,
|
|
endpointHost: "db",
|
|
endpointPort: 5432,
|
|
maxRetries: 3,
|
|
retryInterval: 10,
|
|
autoStart: true,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
hostRows.length = 0;
|
|
settingsRows.length = 0;
|
|
});
|
|
|
|
describe("runTunnelsSettingsMigration", () => {
|
|
it("moves a host's switch and saved tunnels into plugin settings", async () => {
|
|
hostRows.push({
|
|
id: 1,
|
|
enable_tunnel: true,
|
|
tunnel_connections: JSON.stringify([tunnel]),
|
|
});
|
|
|
|
const result = await runTunnelsSettingsMigration();
|
|
|
|
expect(result.moved).toBe(1);
|
|
expect(storedValue(1, "enableTunnel")).toBe(true);
|
|
expect(storedValue(1, "tunnelConnections")).toEqual([tunnel]);
|
|
expect(settingsRows.every((row) => row.pluginId === "tunnels")).toBe(true);
|
|
expect(settingsRows.every((row) => row.scope === "host")).toBe(true);
|
|
});
|
|
|
|
it("keeps a disabled host's saved tunnels and its disabled switch", async () => {
|
|
hostRows.push({
|
|
id: 3,
|
|
enable_tunnel: false,
|
|
tunnel_connections: JSON.stringify([tunnel]),
|
|
});
|
|
|
|
await runTunnelsSettingsMigration();
|
|
|
|
expect(storedValue(3, "enableTunnel")).toBe(false);
|
|
expect(storedValue(3, "tunnelConnections")).toEqual([tunnel]);
|
|
});
|
|
|
|
it("stores an unreadable tunnel list as empty rather than failing", async () => {
|
|
hostRows.push({ id: 4, enable_tunnel: true, tunnel_connections: "{nope" });
|
|
|
|
const result = await runTunnelsSettingsMigration();
|
|
|
|
expect(result.moved).toBe(1);
|
|
expect(storedValue(4, "tunnelConnections")).toEqual([]);
|
|
});
|
|
|
|
it("does nothing on a fresh install", async () => {
|
|
const result = await runTunnelsSettingsMigration();
|
|
|
|
expect(result.moved).toBe(0);
|
|
expect(settingsRows).toEqual([]);
|
|
});
|
|
|
|
it("is idempotent: a second run changes nothing", async () => {
|
|
hostRows.push({ id: 2, enable_tunnel: true, tunnel_connections: null });
|
|
|
|
await runTunnelsSettingsMigration();
|
|
const afterFirst = JSON.parse(JSON.stringify(settingsRows));
|
|
|
|
const second = await runTunnelsSettingsMigration();
|
|
|
|
expect(second.moved).toBe(0);
|
|
expect(second.skipped).toBe(1);
|
|
expect(settingsRows).toEqual(afterFirst);
|
|
});
|
|
|
|
it("does not overwrite a value the plugin already saved", async () => {
|
|
hostRows.push({
|
|
id: 5,
|
|
enable_tunnel: true,
|
|
tunnel_connections: JSON.stringify([tunnel]),
|
|
});
|
|
await pluginSettingsRepository.set(
|
|
"tunnels",
|
|
"host",
|
|
"5",
|
|
"enableTunnel",
|
|
"false",
|
|
);
|
|
|
|
const result = await runTunnelsSettingsMigration();
|
|
|
|
expect(result.skipped).toBe(1);
|
|
expect(storedValue(5, "enableTunnel")).toBe(false);
|
|
expect(storedValue(5, "tunnelConnections")).toBeUndefined();
|
|
});
|
|
});
|