"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { ProfileService } = require("../src/main/profiles/profileService"); const { VaultStore } = require("../src/main/security/vaultStore"); const { ProfileTransfer, serializeProfiles, parseProfiles, identity, MAX_BYTES } = require("../src/main/profiles/profileTransfer"); const document = (servers, version = 1) => JSON.stringify({ format: "chj-server-profiles", version, servers }); const server = { name: 'Žluťoučký, "server"', host: "NGINX.lan", username: "root", port: 22 }; async function setup(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-transfer-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const vault = new VaultStore(root); await vault.create("transfer-test-password"); const profiles = new ProfileService(vault); return { vault, profiles, transfer: new ProfileTransfer(profiles) }; } test("JSON and CSV round trip UTF-8, original DNS/IPv4/IPv6 and never serialize secrets or paths", () => { const servers = [server, { ...server, host: "192.168.1.10" }, { ...server, host: "2001:db8::10", port: 2222 }, { ...server, name: "=FORMULA()", username: "@user" }, { ...server, name: "'literal" }]; const profiles = servers.map((item) => ({ ...item, label: item.name, password: "do-not-export", privateKeyPath: "/secret/key", token: "secret-token", resolvedIp: "1.2.3.4", hasStoredPassword: true })); for (const format of ["json", "csv"]) { const exported = serializeProfiles(profiles, format); assert.doesNotMatch(exported, /do-not-export|\/secret\/key|secret-token|1\.2\.3\.4|hasStoredPassword/); assert.deepEqual(parseProfiles(exported).servers, servers); } assert.match(serializeProfiles(profiles, "csv"), /'=FORMULA/); assert.deepEqual(Object.keys(JSON.parse(serializeProfiles(profiles)).servers[0]), ["name", "host", "username", "port"]); }); test("import validates the entire document, version, fields, CSV structure and limits", () => { for (const input of [document([server], 2), document([{ ...server, host: "bad host" }]), document([{ ...server, port: 0 }]), document([{ ...server, port: 65536 }]), document([{ ...server, port: null }]), document([{ ...server, username: "bad\nuser" }]), document([]), "name,ip,username,port\nx,1.2.3.4,root,22", 'name,host,username,port\n"bad,host,root,22', "name,host,username,port\nx,host,root,22,extra", "x".repeat(MAX_BYTES + 1)]) assert.throws(() => parseProfiles(input)); assert.deepEqual(parseProfiles('port,username,host,name\r\n22,root,nginx.lan,NGINX\r\n').servers[0], { name: "NGINX", host: "nginx.lan", username: "root", port: 22 }); assert.equal(parseProfiles(document([{ ...server, port: undefined }])).servers[0].port, 22); assert.equal(identity({ ...server, host: "2001:db8::1" }), identity({ ...server, host: "2001:0db8:0:0:0:0:0:1" })); }); test("preview is read-only; skip, update and create handle vault and file duplicates without importing authentication", async (t) => { for (const policy of ["skip", "update", "create"]) { const { profiles, vault, transfer } = await setup(t); const saved = profiles.save({ label: "Old", host: "nginx.lan", username: "root", storePassword: true, password: "old-password" }); const preview = transfer.preview(document([{ ...server, password: "incoming-password", privateKeyPath: "/incoming/key" }, { ...server, name: "Second" }])); assert.deepEqual(preview.servers.map((item) => item.duplicate), [true, true]); assert.equal(profiles.get(saved.id).label, "Old"); assert.equal(profiles.getStoredPassword(saved.id), "old-password"); assert.deepEqual(transfer.commit(preview.token, policy), policy === "skip" ? { created: 0, updated: 0, skipped: 2 } : policy === "update" ? { created: 0, updated: 2, skipped: 0 } : { created: 2, updated: 0, skipped: 0 }); if (policy === "update") { assert.equal(profiles.getStoredPassword(saved.id), null); assert.equal(profiles.get(saved.id).label, "Second"); } if (policy === "create") { assert.equal(profiles.list().length, 3); for (const profile of profiles.list().filter((item) => item.id !== saved.id)) { assert.equal(profile.hasStoredPassword, false); assert.equal(profile.privateKeyPath, ""); } } assert.doesNotMatch(JSON.stringify(vault.getData()), /incoming-password|\/incoming\/key/); assert.throws(() => transfer.commit(preview.token, policy), /expired/); } }); test("updating a key-authenticated duplicate removes key paths and imported rows require new authentication", async (t) => { const { profiles, transfer } = await setup(t); const saved = profiles.save({ label: "Key server", host: server.host, username: server.username, tags: ["production"], authMethod: "privateKey", privateKeyPath: "/existing/key" }); const preview = transfer.preview(document([server, { ...server, host: "other.lan" }])); assert.deepEqual(transfer.commit(preview.token, "update"), { created: 1, updated: 1, skipped: 0 }); assert.equal(profiles.get(saved.id).authMethod, "password"); assert.equal(profiles.get(saved.id).privateKeyPath, ""); assert.deepEqual(profiles.get(saved.id).tags, ["production"]); }); test("cancel, lock and stale previews cannot be committed; invalid import leaves the vault intact", async (t) => { const { profiles, vault, transfer } = await setup(t); let preview = transfer.preview(document([server])); transfer.cancel(); assert.throws(() => transfer.commit(preview.token, "create"), /expired/); preview = transfer.preview(document([server])); vault.lock(); await vault.unlock("transfer-test-password"); assert.throws(() => transfer.commit(preview.token, "create"), /expired/); assert.throws(() => transfer.preview(document([server, { ...server, port: -1 }]))); assert.equal(profiles.list().length, 0); preview = transfer.preview(document([server])); assert.throws(() => transfer.commit(preview.token, "invalid"), /policy/); assert.equal(profiles.list().length, 0); });