import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { UnraidDeployKeyHost, parseDeployKeyMarker } = require("../src/main/unraid-deploy-key-host.cjs"); const SERVER = { id: "unraid", basePath: "/mnt/user/appdata" }; const REPOSITORY = { fullName: "Jens/Portfolio" }; // The host reaches the server through a single exec call, so capturing the script // it sends is the only way to assert what actually happens to the key material. function keyHost(stdout = "") { const scripts = []; const ssh = { exec: async (serverId, command, options) => { const encoded = command.match(/printf '%s' '([^']+)'/)?.[1] || ""; scripts.push({ serverId, options, script: Buffer.from(encoded, "base64").toString("utf8") }); return { stdout }; }, }; return { host: new UnraidDeployKeyHost({ ssh }), scripts }; } test("deploy-key storage is repository-scoped, deterministic and stays under the server base path", () => { const { host } = keyHost(); const first = host.paths(REPOSITORY, SERVER); const again = host.paths({ fullName: "jens/portfolio" }, SERVER); const other = host.paths({ fullName: "Jens/Other" }, SERVER); assert.deepEqual(first, again, "the same repository always resolves to the same directory"); assert.notEqual(first.directory, other.directory, "a different repository never shares a key directory"); for (const value of Object.values(first)) { assert.ok(value.startsWith("/mnt/user/appdata/.forgeflow/git-credentials/"), value); assert.ok(!value.includes("..")); } assert.ok(!first.directory.toLowerCase().includes("portfolio"), "the repository name is hashed, not embedded"); }); test("the server pull remote is taken from the first usable SSH URL and refused when there is none", () => { const { host } = keyHost(); assert.equal( host.remote({ ...REPOSITORY, localStatus: { remoteUrl: "https://gitea.example/Jens/Portfolio.git" }, sshUrl: "git@gitea.example:Jens/Portfolio.git" }, {}), "git@gitea.example:Jens/Portfolio.git", "an HTTPS remote is skipped in favour of the SSH URL", ); assert.equal( host.remote({ ...REPOSITORY }, { cloneUrl: "ssh://git@gitea.example:2222/Jens/Portfolio.git" }), "ssh://git@gitea.example:2222/Jens/Portfolio.git", ); assert.throws( () => host.remote({ ...REPOSITORY, sshUrl: "https://gitea.example/Jens/Portfolio.git" }, {}), (error) => { assert.equal(error.code, "SERVER_GIT_SSH_URL_REQUIRED"); return true; }, ); }); test("the Git SSH environment pins the scoped key and refuses an unknown host", () => { const { host } = keyHost(); const paths = host.paths(REPOSITORY, SERVER); const environment = host.environment(paths); assert.match(environment, /IdentitiesOnly=yes/); assert.match(environment, /BatchMode=yes/); assert.match(environment, /StrictHostKeyChecking=yes/); assert.ok(environment.includes(paths.knownHosts), "the pinned host key file is repository-scoped"); assert.ok(environment.includes(paths.privateKey)); }); test("a backup copies the current key material into a fresh recovery slot", async () => { const publicKey = "ssh-ed25519 QkFL forgeflow"; const { host, scripts } = keyHost( `__FORGEFLOW_KEY_BACKUP__\nrecovery=/mnt/user/appdata/.forgeflow/git-credentials/abc/recovery/backup-1\npublicKey=${Buffer.from(publicKey).toString("base64")}\n`, ); const backup = await host.backup({ repository: REPOSITORY, server: SERVER }); assert.equal(backup.publicKey, publicKey); assert.match(backup.recovery, /recovery\/backup-1$/); assert.match(scripts[0].script, /umask 077/, "recovered key material is not world readable"); assert.match(scripts[0].script, /deploy-key deploy-key\.pub known_hosts/); }); test("candidate verification only reports ready on a real remote commit", async () => { const remoteSha = "d".repeat(40); const candidate = { paths: { privateKey: "/k/deploy-key", publicKey: "/k/deploy-key.pub", knownHosts: "/k/known_hosts" } }; const context = { repository: { ...REPOSITORY, sshUrl: "git@gitea.example:Jens/Portfolio.git" }, profile: { branch: "main" }, server: SERVER, candidate, }; const proven = keyHost(`__FORGEFLOW_KEY_PROOF__\nremoteSha=${remoteSha}\nfingerprint=SHA256:new\nhostFingerprint=SHA256:host\n`); const proof = await proven.host.verifyCandidate(context); assert.deepEqual(proof, { ready: true, remoteSha, fingerprint: "SHA256:new", hostFingerprint: "SHA256:host" }); assert.match(proven.scripts[0].script, /git ls-remote --exit-code/); assert.match(proven.scripts[0].script, /refs\/heads\/main/); assert.equal(proven.scripts[0].options.timeout, 45_000); assert.deepEqual(await proven.host.preflightCandidate(context), proof); const unproven = keyHost("__FORGEFLOW_KEY_PROOF__\nremoteSha=\nfingerprint=\nhostFingerprint=\n"); assert.equal((await unproven.host.verifyCandidate(context)).ready, false); await assert.rejects(() => unproven.host.preflightCandidate(context), /did not prove the remote branch/); }); test("verifying the active key uses the repository-scoped paths rather than a candidate", async () => { const { host, scripts } = keyHost(`__FORGEFLOW_KEY_PROOF__\nremoteSha=${"e".repeat(40)}\nfingerprint=SHA256:active\nhostFingerprint=SHA256:host\n`); const paths = host.paths(REPOSITORY, SERVER); const proof = await host.verifyActive({ repository: { ...REPOSITORY, sshUrl: "git@gitea.example:Jens/Portfolio.git" }, profile: { branch: "main" }, server: SERVER, }); assert.equal(proof.ready, true); assert.ok(scripts[0].script.includes(paths.privateKey)); assert.ok(scripts[0].script.includes(paths.knownHosts)); }); test("promotion only replaces key material after proving the candidate is complete", async () => { const { host, scripts } = keyHost(); const paths = host.paths(REPOSITORY, SERVER); const candidate = { paths: { directory: "/c", privateKey: "/c/deploy-key", publicKey: "/c/deploy-key.pub", knownHosts: "/c/known_hosts" } }; await host.promote({ repository: REPOSITORY, server: SERVER, candidate }); const script = scripts[0].script; assert.ok(script.includes("test -s '/c/deploy-key'"), "an empty candidate key is refused before anything is replaced"); assert.ok(script.includes("test -s '/c/known_hosts'")); assert.ok(script.indexOf("test -s") < script.indexOf("mv "), "the checks run before the swap"); // The staging suffix is appended outside the quoted path, so the command reads // mv ''.new '' rather than mv '.new' ''. assert.ok(script.includes(`mv '${paths.privateKey}'.new '${paths.privateKey}'`), "the swap is atomic"); assert.ok(script.includes(`cp -p '/c/deploy-key' '${paths.privateKey}'.new`), "the copy lands on the staging name first"); }); test("rollback restores the recovery slot and removes the candidate", async () => { const { host, scripts } = keyHost(); const paths = host.paths(REPOSITORY, SERVER); await host.rollback({ repository: REPOSITORY, server: SERVER, candidate: { paths: { directory: "/candidate" } }, previous: { key: { recovery: "/recovery/backup-1" } }, }); assert.ok(scripts[0].script.includes("cp -p '/recovery/backup-1'")); assert.ok(scripts[0].script.includes(paths.directory)); assert.ok(scripts[0].script.includes("rm -rf -- '/candidate'")); }); test("committing a rotation discards only the candidate directory", async () => { const { host, scripts } = keyHost(); await host.commit({ server: SERVER, candidate: { paths: { directory: "/candidate" } } }); // Every script carries the strict-mode preamble that bash() prepends. assert.equal(scripts[0].script.split("\n").at(-1), "rm -rf -- '/candidate'"); assert.ok(!scripts[0].script.includes(".forgeflow/git-credentials"), "the active key directory is never touched on commit"); }); test("every server script runs under strict mode with Git prompts disabled", async () => { const { host, scripts } = keyHost(); await host.commit({ server: SERVER, candidate: { paths: { directory: "/candidate" } } }); assert.match(scripts[0].script, /^set -euo pipefail\nexport GIT_TERMINAL_PROMPT=0\n/); assert.equal(scripts[0].serverId, SERVER.id); }); test("revocation moves key material aside so it can still be restored", async () => { const { host, scripts } = keyHost(); const paths = host.paths(REPOSITORY, SERVER); await host.revoke({ repository: REPOSITORY, server: SERVER }); const script = scripts[0].script; assert.ok(script.includes(`${paths.recovery}/revoked-`), "revoked material is kept in the recovery area"); assert.match(script, /mv /, "the key is moved, never deleted"); assert.ok(!/rm -rf/.test(script), "revocation must not destroy the recovery path"); }); test("restore reinstates the newest recovery slot and reports the public evidence", async () => { const publicKey = "ssh-ed25519 UkVT forgeflow"; const { host, scripts } = keyHost( `__FORGEFLOW_KEY_RESTORE__\npublicKey=${Buffer.from(publicKey).toString("base64")}\nfingerprint=SHA256:restored\nhostFingerprint=SHA256:host\n`, ); const restored = await host.restore({ repository: REPOSITORY, server: SERVER }); assert.deepEqual(restored, { publicKey, fingerprint: "SHA256:restored", hostFingerprint: "SHA256:host" }); assert.match(scripts[0].script, /sort \| tail -1/, "the newest slot is chosen deterministically"); assert.ok(scripts[0].script.includes('test -n "$slot"'), "restoring without a recovery slot fails loudly"); }); test("marker parsing keeps values that themselves contain separators", () => { const parsed = parseDeployKeyMarker("noise\n__M__\nkey=a=b=c\nempty\nother=1\n", "__M__"); assert.deepEqual(parsed, { key: "a=b=c", empty: "", other: "1" }); });