deployment-service.cjs sat at 52% and unraid-deploy-key-host.cjs at 39% of its functions, both hidden behind a healthy aggregate. They are now at 100% lines and functions, tested through real HTTP endpoints and by intercepting the shell script the key host sends, rather than by mocking the boundary away. What is pinned down: a successful workflow run still fails when the server cannot prove it runs that exact commit; a rollback ends as rolled-back rather than success; an unreachable status endpoint is never treated as healthy; a failed poll is recorded on the operation instead of losing it; deploy keys stay repository-scoped under the server base path with a pinned host key; promotion verifies the candidate before swapping atomically; and revocation moves key material to recovery instead of deleting it. Two assumptions turned out to be wrong and the tests follow the real behaviour: the previous-SHA check runs before the already-live check, and a rollback against an unreachable endpoint surfaces the underlying network error. Covering clone-target exposed a real defect: a remote ending in "....git" yielded the folder name "...". Windows strips trailing dots, so that resolves back to the project root itself, past an escape guard that only looks for "..". A dots-only name now falls back to "repository", consistent with how an empty name was already handled. As a side effect "." and ".." resolve to a usable folder instead of raising an error. Coverage gates: the aggregate moves to 85/85/68, and a new per-module gate (60 statements, 50 functions, 36 branches) stops a single module from silently collapsing behind the total. It reuses the data from the first run, so the suite is not executed twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
9.6 KiB
JavaScript
195 lines
9.6 KiB
JavaScript
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 '<path>'.new '<path>' rather than mv '<path>.new' '<path>'.
|
|
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" });
|
|
});
|