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.ok( script.indexOf('SITE_URL="${SITE_URL%/}"') < script.indexOf('API_CORS_ORIGIN="${API_CORS_ORIGIN:-${SITE_URL}}"'), "the default CORS origin must use the normalized site URL", ); assert.match(script, /Could not inspect the current index version/); assert.doesNotMatch(script, /s3 sync[\s\S]{0,250}--delete/); });