fix(hosts): normalize imported jump host references (#1470)

* fix(hosts): normalize imported jump host references

* fix(ci): build plugin SDK before dependent checks

* fix(mysql): keep plugin settings scope indexes within limits

* test(mysql): refresh reviewed plugin upgrade SQL
This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-09-26 18:18:21 -05:00
1 parent 710f545e34
commit ec982aadb5
2 files changed
+63 -40

No files matched your search

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