Files
story/test/e2e/visual-family-firmware-trust-artifact-controls.mjs

125 lines
9.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRequire } from "node:module";
import { createHash, randomUUID } from "node:crypto";
import { execFileSync } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require("playwright");
const outputDirectory = new URL("./screenshots/", import.meta.url);
const webURL = process.env.VISUAL_WEB_URL ?? "http://localhost:3100";
const webHostPattern = new RegExp(new URL(webURL).host.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
const oidcURLPattern = new RegExp(process.env.VISUAL_OIDC_URL_PATTERN ?? "localhost:58180");
const username = process.env.VISUAL_USERNAME ?? "owner";
const password = process.env.VISUAL_PASSWORD ?? "LocalOnly123!";
const marker = randomUUID().slice(0, 8);
const householdPublicID = "10000000-0000-0000-0000-000000000211";
const keyID = `visual-trust-key-${marker}`;
const publicKeyBytes = Buffer.alloc(32);
const publicKey = publicKeyBytes.toString("base64");
const fingerprint = createHash("sha256").update(publicKeyBytes).digest("hex");
const firmwareBytes = Buffer.from(`controlled-firmware-artifact-${marker}`, "utf8");
const firmwareDigest = createHash("sha256").update(firmwareBytes).digest("hex");
const signature = Buffer.alloc(64).toString("base64");
const firmwareVersion = `8.8.${Number.parseInt(marker.slice(0, 2), 16)}`;
const releaseNotes = `受控固件制品-${marker}`;
await mkdir(outputDirectory, { recursive: true });
function psql(statement) {
return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", "voice", "-d", "voice_platform", "-Atc", statement], {
cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
}).trim();
}
const keyCountBefore = Number(psql("SELECT count(*) FROM firmware_signing_key;"));
const releaseCountBefore = Number(psql("SELECT count(*) FROM firmware_release;"));
const auditCountBefore = Number(psql("SELECT count(*) FROM audit_event;"));
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "C:/Program Files/Google/Chrome/Application/chrome.exe" });
const page = await browser.newPage({ viewport: { width: 1440, height: 1150 }, deviceScaleFactor: 1 });
const consoleErrors = [];
const failedRequests = [];
const mutationRequests = [];
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); });
page.on("request", (request) => {
const pathname = new URL(request.url()).pathname;
if ((request.method() === "POST" && (/^\/api\/v1\/households\/[^/]+\/firmware-signing-keys$/.test(pathname) || /^\/api\/v1\/households\/[^/]+\/firmware-releases$/.test(pathname) || /^\/api\/v1\/firmware-releases\/[^/]+\/finalize$/.test(pathname))) || request.method() === "PUT") mutationRequests.push(`${request.method()} ${pathname}`);
});
page.on("requestfailed", (request) => {
const failure = request.failure()?.errorText ?? "failed";
const requestURL = new URL(request.url());
if (request.method() === "GET" && requestURL.pathname === "/api/v1/events" && failure.includes("ERR_ABORTED")) return;
failedRequests.push(`${request.method()} ${request.url()} ${failure}`);
});
try {
await page.goto(webURL, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "登录研发环境" }).click();
await page.waitForURL(oidcURLPattern);
await page.locator("#username").fill(username);
await page.locator("#password").fill(password);
await page.locator("#kc-login").click();
await page.waitForURL(webHostPattern, { timeout: 15000 });
await page.getByRole("heading", { name: "家庭业务工作台" }).waitFor();
await page.getByRole("button", { name: "固件发布", exact: true }).click();
const keyForm = page.locator("form.tool-form").filter({ hasText: "登记可信签名公钥" });
await keyForm.getByLabel("密钥编号").fill(keyID);
await keyForm.getByLabel("Ed25519公钥(Base64)").fill("not-base64");
const invalidKeyBlocked = await keyForm.getByRole("button", { name: "核对并登记公钥" }).isDisabled();
const invalidKeyExplained = (await keyForm.textContent())?.includes("必须是标准Base64编码的32字节Ed25519公钥") === true;
await keyForm.getByLabel("Ed25519公钥(Base64)").fill(publicKey);
const validKeyEnabled = await keyForm.getByRole("button", { name: "核对并登记公钥" }).isEnabled();
await keyForm.getByRole("button", { name: "核对并登记公钥" }).click();
let control = page.getByRole("form", { name: "家庭业务受控操作" });
await control.waitFor();
let text = await control.textContent();
const keyTargetBound = text?.includes(householdPublicID) === true && text?.includes(keyID) === true && text?.includes("32字节") === true && text?.includes(fingerprint) === true && text?.includes("复用租户内相同编号会更新") === true;
let action = control.getByRole("button", { name: "确认登记可信公钥" });
const keyNeedsAcknowledgement = await action.isDisabled();
await control.getByLabel(/我已通过受控渠道核对密钥编号/).check();
const keyActionEnabled = await action.isEnabled();
await control.screenshot({ path: fileURLToPath(new URL("family-firmware-key-register-control.png", outputDirectory)) });
await control.getByRole("button", { name: "取消" }).click();
const keyCancelClosed = await control.count() === 0;
const releaseForm = page.locator("form.firmware-publish-form");
await releaseForm.getByLabel("设备型号").fill("simulated-terminal");
await releaseForm.getByLabel("主板版本").fill("sim-rev-a");
await releaseForm.getByLabel("固件版本").fill(firmwareVersion);
await releaseForm.getByLabel("签名密钥编号").fill(keyID);
await releaseForm.getByLabel("最低安全基线").fill("1.0.0");
await releaseForm.getByLabel("发布说明").fill(releaseNotes);
await releaseForm.getByLabel("Ed25519签名(Base64)").fill("invalid-signature");
await releaseForm.getByLabel("固件文件").setInputFiles({ name: `firmware-${marker}.bin`, mimeType: "application/octet-stream", buffer: firmwareBytes });
await releaseForm.getByText(firmwareDigest).waitFor();
const invalidSignatureBlocked = await releaseForm.getByRole("button", { name: "核对并上传校验" }).isDisabled();
const invalidSignatureExplained = (await releaseForm.textContent())?.includes("签名必须是标准Base64编码的64字节Ed25519结果") === true;
await releaseForm.getByLabel("Ed25519签名(Base64)").fill(signature);
const releaseReady = await releaseForm.getByRole("button", { name: "核对并上传校验" }).isEnabled();
await releaseForm.getByRole("button", { name: "核对并上传校验" }).click();
control = page.getByRole("form", { name: "家庭业务受控操作" });
await control.waitFor();
text = await control.textContent();
const releaseTargetBound = text?.includes(householdPublicID) === true && text?.includes("simulated-terminal / sim-rev-a") === true && text?.includes(firmwareVersion) === true && text?.includes(`firmware-${marker}.bin`) === true && text?.includes(firmwareDigest) === true && text?.includes(keyID) === true && text?.includes("1.0.0") === true && text?.includes(releaseNotes) === true && text?.includes("firmware-manifest-v1") === true && text?.includes("15分钟上传地址") === true;
action = control.getByRole("button", { name: "确认上传并验证制品" });
const releaseNeedsAcknowledgement = await action.isDisabled();
await control.getByLabel(/我已核对设备型号、主板、版本、文件大小和SHA-256/).check();
const releaseActionEnabled = await action.isEnabled();
await control.screenshot({ path: fileURLToPath(new URL("family-firmware-release-publish-control.png", outputDirectory)) });
await control.getByRole("button", { name: "取消" }).click();
const releaseCancelClosed = await control.count() === 0;
const keysUnchanged = Number(psql("SELECT count(*) FROM firmware_signing_key;")) === keyCountBefore;
const releasesUnchanged = Number(psql("SELECT count(*) FROM firmware_release;")) === releaseCountBefore;
const auditUnchanged = Number(psql("SELECT count(*) FROM audit_event;")) === auditCountBefore;
const overflow = await page.evaluate(() => [...document.querySelectorAll("body *")]
.filter((element) => element instanceof HTMLElement && element.scrollWidth > element.clientWidth + 2 && getComputedStyle(element).overflowX === "visible")
.slice(0, 20)
.map((element) => `${element.tagName}.${element.className}: ${element.clientWidth}/${element.scrollWidth}`));
const result = { invalidKeyBlocked, invalidKeyExplained, validKeyEnabled, keyTargetBound, keyNeedsAcknowledgement, keyActionEnabled, keyCancelClosed, invalidSignatureBlocked, invalidSignatureExplained, releaseReady, releaseTargetBound, releaseNeedsAcknowledgement, releaseActionEnabled, releaseCancelClosed, keysUnchanged, releasesUnchanged, auditUnchanged, mutationRequests, overflow, consoleErrors, failedRequests };
console.log(JSON.stringify(result, null, 2));
if (Object.entries(result).some(([key, value]) => !["mutationRequests", "overflow", "consoleErrors", "failedRequests"].includes(key) && value !== true) || mutationRequests.length || overflow.length || consoleErrors.length || failedRequests.length) throw new Error("Firmware trust/artifact controls visual smoke failed");
} finally {
await browser.close();
}