diff --git a/src/backend/database/routes/host-import-order.ts b/src/backend/database/routes/host-import-order.ts index 1c9f2d1dc..0a7d16c7c 100644 --- a/src/backend/database/routes/host-import-order.ts +++ b/src/backend/database/routes/host-import-order.ts @@ -1,5 +1,11 @@ import { normalizeImportedHost } from "./host-normalizers.js"; +function jumpHostId(value: unknown): unknown { + return typeof value === "string" && /^\d+$/.test(value) + ? Number(value) + : value; +} + export function prepareHostImports(input: Record[]) { const hosts = input.map((value, index) => ({ host: normalizeImportedHost(value), @@ -27,7 +33,7 @@ export function prepareHostImports(input: Record[]) { if (!Array.isArray(jumps)) throw new Error(`Host ${entry.index + 1}: jumpHosts must be an array`); for (const jump of jumps) { - const dependency = byId.get(jump?.hostId); + const dependency = byId.get(jumpHostId(jump?.hostId)); if (!dependency) throw new Error( `Host ${entry.index + 1}: jump host ${jump?.hostId} is missing from the export. Re-export and include all jump hosts.`, @@ -48,7 +54,7 @@ export function remapImportedJumpHosts( ) { if (!Array.isArray(jumps)) return null; return jumps.map((jump) => { - const hostId = importedIds.get(jump.hostId); + const hostId = importedIds.get(jumpHostId(jump.hostId)); if (hostId === undefined) throw new Error(`Jump host ${jump.hostId} failed to import`); return { hostId }; diff --git a/src/backend/tests/database/routes/host-import-terminal-settings.test.ts b/src/backend/tests/database/routes/host-import-terminal-settings.test.ts index c0fb0f261..e9d2c9a93 100644 --- a/src/backend/tests/database/routes/host-import-terminal-settings.test.ts +++ b/src/backend/tests/database/routes/host-import-terminal-settings.test.ts @@ -103,49 +103,66 @@ async function importHosts( return res; } -it("imports forward jump references first and replaces source IDs with destination IDs", async () => { - mocks.create - .mockResolvedValueOnce({ id: 501 }) - .mockResolvedValueOnce({ id: 502 }); - const raw = { - hosts: [ - { ...host, exportId: 10, jumpHosts: [{ hostId: 20 }] }, - { ...host, ip: "192.0.2.2", exportId: 20 }, - ], - }; - const payload = buildExportPayload(raw, null, new Set(["jumpHosts"]), false); - const res = await importHosts(payload.hosts); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ success: 2, failed: 0 }), - ); - expect(mocks.create.mock.calls[0][1].ip).toBe("192.0.2.2"); - expect(JSON.parse(mocks.create.mock.calls[1][1].jumpHosts)).toEqual([ - { hostId: 501 }, - ]); - expect(mocks.create.mock.calls[1][1]).not.toHaveProperty("exportId"); - expect(mocks.create.mock.calls[1][1]).not.toHaveProperty("id"); -}); +it.each([20, "20"])( + "imports forward jump reference %j and replaces source IDs with destination IDs", + async (jumpId) => { + mocks.create + .mockResolvedValueOnce({ id: 501 }) + .mockResolvedValueOnce({ id: 502 }); + const raw = { + hosts: [ + { ...host, exportId: 10, jumpHosts: [{ hostId: jumpId }] }, + { ...host, ip: "192.0.2.2", exportId: 20 }, + ], + }; + const payload = buildExportPayload( + raw, + null, + new Set(["jumpHosts"]), + false, + ); + const res = await importHosts(payload.hosts); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ success: 2, failed: 0 }), + ); + expect(mocks.create.mock.calls[0][1].ip).toBe("192.0.2.2"); + expect(JSON.parse(mocks.create.mock.calls[1][1].jumpHosts)).toEqual([ + { hostId: 501 }, + ]); + expect(mocks.create.mock.calls[1][1]).not.toHaveProperty("exportId"); + expect(mocks.create.mock.calls[1][1]).not.toHaveProperty("id"); + }, +); -it("maps overwritten jump hosts to their 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 }, - ], - true, - ); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ success: 1, updated: 1, failed: 0 }), - ); - expect(JSON.parse(mocks.create.mock.calls[0][1].jumpHosts)).toEqual([ - { hostId: 801 }, - ]); -}); +it.each([20, "20"])( + "maps overwritten jump reference %j to its existing destination ID", + async (jumpId) => { + mocks.list.mockResolvedValue([{ ...host, ip: "192.0.2.2", id: 801 }]); + const res = await importHosts( + [ + { ...host, exportId: 10, jumpHosts: [{ hostId: jumpId }] }, + { ...host, ip: "192.0.2.2", exportId: 20 }, + ], + true, + ); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ success: 1, updated: 1, failed: 0 }), + ); + expect(JSON.parse(mocks.create.mock.calls[0][1].jumpHosts)).toEqual([ + { hostId: 801 }, + ]); + }, +); it.each([ [{ ...host, jumpHosts: [{ hostId: 20 }] }], + [{ ...host, jumpHosts: [{ hostId: "20" }] }], [{ ...host, exportId: 10, jumpHosts: [{ hostId: 10 }] }], + [{ ...host, exportId: 10, jumpHosts: [{ hostId: "10" }] }], + [ + { ...host, exportId: 10, jumpHosts: [{ hostId: "20oops" }] }, + { ...host, exportId: 20 }, + ], [ { ...host, exportId: 10 }, { ...host, exportId: 10 },