shoc-frontend-new/scripts/deploy-web.test.mjs

100 lines
3.7 KiB
JavaScript

import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import test from "node:test";
import { fileURLToPath } from "node:url";
const scriptPath = new URL("./deploy-web.sh", import.meta.url);
const script = readFileSync(scriptPath, "utf8");
const windowsGitBash = `${process.env.ProgramFiles ?? "C:\\Program Files"}\\Git\\bin\\bash.exe`;
const bash = process.platform === "win32" ? windowsGitBash : "bash";
const hasBash = process.platform !== "win32" || existsSync(windowsGitBash);
const nativeScriptPath = fileURLToPath(scriptPath);
const bashScriptPath =
process.platform === "win32"
? nativeScriptPath
.replace(/^([A-Za-z]):\\/, (_, drive) => `/${drive.toLowerCase()}/`)
.replaceAll("\\", "/")
: nativeScriptPath;
test("deploy script has valid bash syntax", { skip: !hasBash }, () => {
const result = spawnSync(bash, ["-n", bashScriptPath], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
});
test("deploy script fails closed before running tools", { skip: !hasBash }, () => {
const result = spawnSync(bash, [bashScriptPath], {
encoding: "utf8",
env: { PATH: process.env.PATH },
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /SITE_BUCKET must be set explicitly/);
});
test("target bucket mismatch fails before publishing", { skip: !hasBash }, () => {
const result = spawnSync(bash, [bashScriptPath], {
encoding: "utf8",
env: {
...process.env,
SITE_BUCKET: "wrong-bucket",
EXPECTED_SITE_BUCKET: "expected-bucket",
CLOUDFRONT_DISTRIBUTION_ID: "DIST123",
SITE_URL: "https://example.test",
EXPECTED_API_URL: "https://api.example.test/api",
VITE_API_URL: "https://api.example.test/api",
DEPLOY_RELEASE_ID: "1234567",
},
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /SITE_BUCKET does not match EXPECTED_SITE_BUCKET/);
});
test("content safety contract is present and ordered", () => {
for (const required of [
"get-bucket-versioning",
"head-object",
"list-object-versions",
"asset-versions.tsv",
"prior-asset-versions.tsv",
"prior-manifest.json",
"priorAssets",
"isCurrentManifest",
"--version-id",
"public,max-age=31536000,immutable",
"no-cache,no-store,must-revalidate",
"cloudfront wait invalidation-completed",
'fetch_route "/login"',
'fetch_route "${EXTENSIONLESS_SMOKE_PATH}"',
"Access-Control-Request-Method: GET",
".deploy/releases/${DEPLOY_RELEASE_ID}.json",
]) {
assert.ok(script.includes(required), `missing contract: ${required}`);
}
assert.ok(
script.indexOf('deployment_verified="true"') < script.indexOf("aws s3api list-object-versions"),
"pruning must occur only after remote verification",
);
assert.ok(
script.indexOf('grep -qi "^access-control-allow-methods:.*GET"') <
script.indexOf('aws s3 cp "${work_dir}/manifest.json"'),
"failed remote verification must not publish a retention manifest",
);
assert.ok(
script.indexOf('aws s3 cp "${work_dir}/manifest.json"') <
script.indexOf('deployment_verified="true"'),
"manifest publication failures must roll back the new index",
);
assert.ok(
script.indexOf('>"${work_dir}/prior-asset-keys.txt"') <
script.indexOf('aws s3 sync dist/ "s3://${SITE_BUCKET}/"'),
"the pre-manifest release must be inventoried before new assets publish",
);
assert.match(
script,
/if \(isCurrentManifest\) \{[\s\S]*manifest\.priorAssets/,
"only the current manifest may retain its pre-script rollback assets",
);
assert.match(script, /Could not inspect the current index version/);
assert.doesNotMatch(script, /s3 sync[\s\S]{0,250}--delete/);
});