mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: skip existing hosts on JSON and SSH config import (#1607)
* fix: skip existing hosts on JSON and SSH config import Import (skip existing) only loaded existing hosts when overwriting, so every host was created again. Always load them, and skip a match unless overwrite is set. A skipped host still resolves as a jump host for the hosts imported after it. Fixes #1606 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: use .js extension for test import Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2fe7530cd4
commit
8d9c83b0f6
2 files changed
+78
-12
No files matched your search
@@ -568,9 +568,9 @@ export function registerHostBulkRoutes(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loaded for both modes: overwrite updates a match, skip leaves it alone.
|
||||||
let existingHostMap: Map<string, { id: number }> | undefined;
|
let existingHostMap: Map<string, { id: number }> | undefined;
|
||||||
const hostRepository = createCurrentHostRepository();
|
const hostRepository = createCurrentHostRepository();
|
||||||
if (overwrite) {
|
|
||||||
try {
|
try {
|
||||||
const allHosts =
|
const allHosts =
|
||||||
await createCurrentHostResolutionRepository().findHostsByUserId(
|
await createCurrentHostResolutionRepository().findHostsByUserId(
|
||||||
@@ -584,7 +584,6 @@ export function registerHostBulkRoutes(
|
|||||||
} catch {
|
} catch {
|
||||||
existingHostMap = undefined;
|
existingHostMap = undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const knownAuthTypes = listKnownAuthTypes();
|
const knownAuthTypes = listKnownAuthTypes();
|
||||||
const writtenHostIds: number[] = [];
|
const writtenHostIds: number[] = [];
|
||||||
@@ -624,6 +623,15 @@ export function registerHostBulkRoutes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||||
|
const existing = existingHostMap?.get(lookupKey);
|
||||||
|
if (existing && !overwrite) {
|
||||||
|
// Hosts that jump through this one still resolve to the kept host.
|
||||||
|
if (exportId !== undefined) importedIds.set(exportId, existing.id);
|
||||||
|
results.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
effectiveConnectionType === "ssh" &&
|
effectiveConnectionType === "ssh" &&
|
||||||
hostData.authType &&
|
hostData.authType &&
|
||||||
@@ -769,8 +777,6 @@ export function registerHostBulkRoutes(
|
|||||||
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
|
||||||
const existing = existingHostMap?.get(lookupKey);
|
|
||||||
await withHostDefaults(
|
await withHostDefaults(
|
||||||
userId,
|
userId,
|
||||||
existing?.id ?? null,
|
existing?.id ?? null,
|
||||||
@@ -831,7 +837,7 @@ export function registerHostBulkRoutes(
|
|||||||
notifyImportedHosts(userId, writtenHostIds);
|
notifyImportedHosts(userId, writtenHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.skipped} skipped, ${results.failed} failed`,
|
||||||
success: results.success,
|
success: results.success,
|
||||||
updated: results.updated,
|
updated: results.updated,
|
||||||
skipped: results.skipped,
|
skipped: results.skipped,
|
||||||
@@ -931,7 +937,6 @@ export function registerHostBulkRoutes(
|
|||||||
|
|
||||||
let existingHostMap: Map<string, { id: number }> | undefined;
|
let existingHostMap: Map<string, { id: number }> | undefined;
|
||||||
const hostRepository = createCurrentHostRepository();
|
const hostRepository = createCurrentHostRepository();
|
||||||
if (overwrite) {
|
|
||||||
try {
|
try {
|
||||||
const allHosts =
|
const allHosts =
|
||||||
await createCurrentHostResolutionRepository().findHostsByUserId(
|
await createCurrentHostResolutionRepository().findHostsByUserId(
|
||||||
@@ -945,7 +950,6 @@ export function registerHostBulkRoutes(
|
|||||||
} catch {
|
} catch {
|
||||||
existingHostMap = undefined;
|
existingHostMap = undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const writtenHostIds: number[] = [];
|
const writtenHostIds: number[] = [];
|
||||||
for (let i = 0; i < hostsToImport.length; i++) {
|
for (let i = 0; i < hostsToImport.length; i++) {
|
||||||
@@ -962,6 +966,13 @@ export function registerHostBulkRoutes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||||
|
const existing = existingHostMap?.get(lookupKey);
|
||||||
|
if (existing && !overwrite) {
|
||||||
|
results.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
userId,
|
userId,
|
||||||
connectionType: "ssh",
|
connectionType: "ssh",
|
||||||
@@ -999,8 +1010,6 @@ export function registerHostBulkRoutes(
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
|
||||||
const existing = existingHostMap?.get(lookupKey);
|
|
||||||
await withHostDefaults(
|
await withHostDefaults(
|
||||||
userId,
|
userId,
|
||||||
existing?.id ?? null,
|
existing?.id ?? null,
|
||||||
@@ -1037,7 +1046,7 @@ export function registerHostBulkRoutes(
|
|||||||
notifyImportedHosts(userId, writtenHostIds);
|
notifyImportedHosts(userId, writtenHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.skipped} skipped, ${results.failed} failed`,
|
||||||
success: results.success,
|
success: results.success,
|
||||||
updated: results.updated,
|
updated: results.updated,
|
||||||
skipped: results.skipped,
|
skipped: results.skipped,
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
import { registerHostBulkRoutes } from "../../../database/routes/host-bulk-routes.js";
|
import { registerHostBulkRoutes } from "../../../database/routes/host-bulk-routes.js";
|
||||||
import { buildExportPayload } from "../../../../ui/sidebar/host-export-payload";
|
import { buildExportPayload } from "../../../../ui/sidebar/host-export-payload.js";
|
||||||
|
|
||||||
let handler: RequestHandler;
|
let handler: RequestHandler;
|
||||||
|
let sshConfigHandler: RequestHandler;
|
||||||
const router = {
|
const router = {
|
||||||
post: (path: string, ...handlers: RequestHandler[]) => {
|
post: (path: string, ...handlers: RequestHandler[]) => {
|
||||||
if (path === "/bulk-import") handler = handlers.at(-1)!;
|
if (path === "/bulk-import") handler = handlers.at(-1)!;
|
||||||
|
if (path === "/ssh-config-import") sshConfigHandler = handlers.at(-1)!;
|
||||||
},
|
},
|
||||||
put: () => {},
|
put: () => {},
|
||||||
patch: () => {},
|
patch: () => {},
|
||||||
@@ -66,12 +68,13 @@ beforeEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.create.mockResolvedValue({ id: 71 });
|
mocks.create.mockResolvedValue({ id: 71 });
|
||||||
mocks.update.mockResolvedValue({ id: 19 });
|
mocks.update.mockResolvedValue({ id: 19 });
|
||||||
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
mocks.list.mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([false, true])(
|
it.each([false, true])(
|
||||||
"carries plugin host settings through export selection and import (overwrite=%s)",
|
"carries plugin host settings through export selection and import (overwrite=%s)",
|
||||||
async (overwrite) => {
|
async (overwrite) => {
|
||||||
|
if (overwrite) mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
const payload = buildExportPayload(
|
const payload = buildExportPayload(
|
||||||
{ hosts: [host] },
|
{ hosts: [host] },
|
||||||
null,
|
null,
|
||||||
@@ -168,6 +171,60 @@ it.each([20, "20"])(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("skips hosts that already exist when not overwriting", async () => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
|
const res = await importHosts([host]);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ success: 0, updated: 0, skipped: 1, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.applyPluginSettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a skipped jump host reference to its existing destination ID", async () => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, ip: "192.0.2.2", id: 801 }]);
|
||||||
|
const res = await importHosts([
|
||||||
|
{ ...host, exportId: 10, jumpHosts: [{ hostId: 20 }] },
|
||||||
|
{ ...host, ip: "192.0.2.2", exportId: 20 },
|
||||||
|
]);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ success: 1, skipped: 1, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(JSON.parse(mocks.create.mock.calls[0][1].jumpHosts)).toEqual([
|
||||||
|
{ hostId: 801 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, { success: 0, updated: 0, skipped: 1 }],
|
||||||
|
[true, { success: 0, updated: 1, skipped: 0 }],
|
||||||
|
])(
|
||||||
|
"SSH config import with an existing host (overwrite=%s)",
|
||||||
|
async (overwrite, counts) => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
|
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() };
|
||||||
|
await sshConfigHandler(
|
||||||
|
{
|
||||||
|
userId: "user-1",
|
||||||
|
body: {
|
||||||
|
content: "Host web\n HostName 192.0.2.1\n User alice\n Port 22\n",
|
||||||
|
overwrite,
|
||||||
|
},
|
||||||
|
} as unknown as Request,
|
||||||
|
res as unknown as Response,
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ ...counts, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).toHaveBeenCalledTimes(overwrite ? 1 : 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ ...host, jumpHosts: [{ hostId: 20 }] }],
|
[{ ...host, jumpHosts: [{ hostId: 20 }] }],
|
||||||
[{ ...host, jumpHosts: [{ hostId: "20" }] }],
|
[{ ...host, jumpHosts: [{ hostId: "20" }] }],
|
||||||
|
|||||||
Reference in new issue
Block a user