100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const root = process.cwd();
|
|
const bash =
|
|
process.platform === "win32"
|
|
? ((process.env.PATH ?? "")
|
|
.split(delimiter)
|
|
.flatMap((dir) => [
|
|
join(dir, "bash.exe"),
|
|
join(dirname(dir), "bin", "bash.exe"),
|
|
join(dirname(dirname(dir)), "bin", "bash.exe"),
|
|
])
|
|
.find((path) => existsSync(path)) ?? "bash")
|
|
: "bash";
|
|
const sha = "a".repeat(40);
|
|
function simulate(scenario: string, namespace = "", expectedStatus = 0) {
|
|
const dir = mkdtempSync(join(tmpdir(), "cms-publish-test-"));
|
|
try {
|
|
const result = spawnSync(
|
|
bash,
|
|
[resolve(root, "scripts/publish-container.sh")],
|
|
{
|
|
cwd: dir,
|
|
encoding: "utf8",
|
|
timeout: 10000,
|
|
env: {
|
|
...process.env,
|
|
BASH_ENV: resolve(root, "src/test/publish-container-harness.sh"),
|
|
TEST_DIR: dir.replaceAll("\\", "/"),
|
|
TEST_SHA: sha,
|
|
SCENARIO: scenario,
|
|
REGISTRY_SERVER: "https://registry.invalid",
|
|
REGISTRY_REPOSITORY: "owner/cms",
|
|
REGISTRY_USER: "Simo",
|
|
REGISTRY_NAMESPACE: namespace,
|
|
REGISTRY_TOKEN: "fixture-only",
|
|
},
|
|
},
|
|
);
|
|
if (result.error) throw result.error;
|
|
expect(result.status, result.stdout + result.stderr).toBe(expectedStatus);
|
|
return readFileSync(join(dir, "calls"), "utf8");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
describe("verified application image reuse", () => {
|
|
it("reuses only the exact image digest that passed deployment checks", () => {
|
|
const calls = simulate("verified");
|
|
expect(calls).toContain(
|
|
`docker tag sha256:candidate registry.invalid/simo/cms:${sha}`,
|
|
);
|
|
expect(calls).not.toContain("docker build --network=host --build-arg");
|
|
expect(
|
|
calls.indexOf("verify scripts/verify-portable-image.mjs"),
|
|
).toBeLessThan(calls.indexOf("regctl image copy"));
|
|
});
|
|
it.each(["missing", "mismatch"])(
|
|
"builds committed source when verification marker is %s",
|
|
(scenario) => {
|
|
const calls = simulate(scenario);
|
|
expect(calls).toContain("docker build --network=host --build-arg");
|
|
expect(calls).not.toContain("docker tag sha256:candidate");
|
|
},
|
|
);
|
|
});
|
|
|
|
it("uses the token account namespace instead of the repository owner", () => {
|
|
const calls = simulate("verified");
|
|
expect(calls).toContain(`registry.invalid/simo/cms:${sha}\n`);
|
|
expect(calls).not.toContain("registry.invalid/owner/cms");
|
|
});
|
|
it("supports an explicit organization namespace", () => {
|
|
const calls = simulate("verified", "My-Org");
|
|
expect(calls).toContain(`registry.invalid/my-org/cms:${sha}\n`);
|
|
});
|
|
|
|
it("bounds uploads and verifies both published image configs", () => {
|
|
const calls = simulate("verified");
|
|
expect(calls).toContain("--blob-chunk 8388608 --blob-max 8388608");
|
|
expect(calls).not.toContain("docker push");
|
|
expect(calls.match(/regctl image import/g)).toHaveLength(2);
|
|
expect(calls.match(/regctl image copy/g)).toHaveLength(2);
|
|
expect(calls.match(/regctl manifest get/g)).toHaveLength(4);
|
|
expect(calls.match(/--platform linux\/amd64/g)).toHaveLength(4);
|
|
});
|
|
it.each(["upload-fails", "wrong-config", "bad-checksum"])(
|
|
"stops publication on %s",
|
|
(scenario) => {
|
|
const calls = simulate(scenario, "", 1);
|
|
expect(calls.match(/regctl image copy/g)?.length ?? 0).toBeLessThanOrEqual(
|
|
1,
|
|
);
|
|
},
|
|
);
|