Something went wrong. Try again.
Something went wrong. Try again.
8.1 kB · 139 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140import { afterAll, beforeAll, describe, expect, it } from "vitest";import { execFileSync } from "node:child_process";import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import { pinnedChromePath, realChromePathDeps } from "../../tools/chromePath";import { startFixtureServer, type FixtureServer } from "../../runner/fixtureServer";import { startExtensionHost } from "../../tools/extensionHost";import { generateSigningKey } from "../../tools/bundleSign";import { readZip } from "../../shared/zip";import { startFakeCentral } from "../helpers/fakeCentral";import { validate } from "../helpers/jsonSchema";
/** * P8-11 in a real Chrome for Testing: the extension's worker polls a central system over real HTTP (reachable only because the manifest has a host permission for * it), takes a job created by hand, verifies the Ed25519 signature with WebCrypto, runs the script in the engine host with real input, builds the archive, keeps * it in IndexedDB until the server confirms its hash, and survives the browser being closed with an upload still waiting. */
const root = resolve(__dirname, "../..");const chrome = pinnedChromePath(root, realChromePathDeps(root));const schema = JSON.parse(readFileSync(join(root, "shared/runjson.schema.json"), "utf8"));
let dir: string;let site: FixtureServer;let origin: string;let key: Awaited<ReturnType<typeof generateSigningKey>>;let central: Awaited<ReturnType<typeof startFakeCentral>>;let host: Awaited<ReturnType<typeof startExtensionHost>> | undefined;
const SCRIPT = (o: string, verify = 'verify text:"sent: 1"') => `test "send"\n start ${o}/form.html\n timeout 3s\n step "send"\n click css:"#send"\n ${verify}\n screenshot "after"\n`;
beforeAll(async () => { dir = mkdtempSync(join(tmpdir(), "aite-jobchrome-")); const siteDir = join(dir, "site"); mkdirSync(siteDir); for (const e of readdirSync(join(root, "fixtures/commands"), { withFileTypes: true })) if (e.isFile()) copyFileSync(join(root, "fixtures/commands", e.name), join(siteDir, e.name)); site = await startFixtureServer({ root: siteDir, port: 0 }); origin = `http://127.0.0.1:${site.port}`; key = await generateSigningKey(); central = await startFakeCentral({ privateKey: key.privateKey });}, 30_000);
afterAll(async () => { await host?.close(); await central?.close(); await site?.close(); rmSync(dir, { recursive: true, force: true });});
const open = async (work: string) => { host = await startExtensionHost({ root, chrome, workDir: join(dir, work), allow: [origin], central: central.url, signingKey: key.publicKey, dispatchKey: key.publicKey }); return host;};const configure = (h: NonNullable<typeof host>, token = central.token) => h.evaluate(`aiteWorker.configure(${JSON.stringify({ central: central.url, token, deviceId: central.deviceId, accounts: { "test-qian": "qian.test" } })})`).then(() => h.evaluate(`aiteWorker.setApprovedOrigins(${JSON.stringify([origin])})`));const tick = (h: NonNullable<typeof host>) => h.evaluate("aiteWorker.tick()") as Promise<{ kind: string; [k: string]: unknown }>;const pending = (h: NonNullable<typeof host>) => h.evaluate("aiteWorker.pending()") as Promise<Array<{ jobId: string; tries: number; lastError?: string }>>;
async function handJob(script = SCRIPT(origin)) { const bundle = await central.addBundle({ "scripts/send.aite": script }); return { bundle, job: central.createJob({ bundle, file: "scripts/send.aite", start: `${origin}/form.html`, allow: [origin], name: "send", session: "test-qian" }) };}
describe("the worker in a real Chrome", () => { it("takes a job created by hand, runs it with real input, uploads an archive the system `unzip` accepts, and the job is done on the server's own hash", async () => { const h = await open("w1"); const { job } = await handJob(); await configure(h); const r = await tick(h); expect(r, JSON.stringify(r)).toMatchObject({ kind: "done", jobId: job.id, testStatus: "passed", uploaded: true }); expect(central.jobs.get(job.id)!.state).toBe("done"); const bytes = central.archives.get(job.id)!.bytes; const zipPath = join(dir, "archive.zip"); writeFileSync(zipPath, bytes); expect(execFileSync("unzip", ["-t", zipPath]).toString()).toContain("No errors detected"); const files = Object.fromEntries(readZip(new Uint8Array(bytes)).map((f) => [f.name, f.data])); const run = JSON.parse(new TextDecoder().decode(files["run.json"])); expect(validate(schema, run)).toEqual([]); expect(run).toMatchObject({ profile: "test-qian", summary: { passed: 1, failed: 0 } }); expect(run.chrome).toMatch(/^155\./); const png = Object.entries(files).find(([n]) => n.startsWith("shots/"))!; expect([...png[1].slice(0, 4)]).toEqual([0x89, 0x50, 0x4e, 0x47]); // a real PNG from Page.captureScreenshot expect(await pending(h)).toEqual([]); }, 90_000);
it("refuses an altered bundle: Chrome's own Ed25519 check fails, nothing is run, the job is reported failed", async () => { const h = host!; const { job, bundle } = await handJob(); central.tamper(bundle, (b) => void (b.files["scripts/send.aite"] += ' step "evil"\n goto https://evil.example/\n')); const r = await tick(h); expect(r).toMatchObject({ kind: "refused", jobId: job.id, stage: "bundle" }); expect(central.jobs.get(job.id)!.failReason).toMatch(/bundle-refused/); expect(central.archives.has(job.id)).toBe(false); }, 60_000);
it("refuses a bundle signed by a key that is not the pinned one", async () => { for (const j of central.jobs.values()) if (j.state === "offered") j.state = "failed"; const attacker = await generateSigningKey(); const { job, bundle } = await handJob(); await central.resign(bundle, attacker.privateKey); const r = await tick(host!); expect(r).toMatchObject({ kind: "refused", jobId: job.id, stage: "bundle" }); expect(central.jobs.get(job.id)!.failReason).toContain("not signed by the key pinned"); expect(central.archives.has(job.id)).toBe(false); }, 60_000);
it("a dropped connection during the upload leaves the archive in IndexedDB; after the browser is closed and opened again the next tick uploads those bytes, and the job is done", async () => { // clear the jobs left over from earlier tests so only this one is offered for (const j of central.jobs.values()) if (j.state === "offered") j.state = "failed"; const { job } = await handJob(); central.faults.dropUploads = 2; // Chrome retries a PUT whose connection was reset once by itself, so the first attempt and that retry are both dropped const first = host!; const r = await tick(first); expect(r, JSON.stringify(r)).toMatchObject({ kind: "done", jobId: job.id, uploaded: false }); expect(central.jobs.get(job.id)!.state).toBe("accepted"); const waiting = await pending(first); expect(waiting).toHaveLength(1); expect(waiting[0]).toMatchObject({ jobId: job.id, tries: 1 }); await first.close(); host = undefined;
const second = await open("w1"); // the same profile folder: IndexedDB and chrome.storage survive, and so does the extension's id expect(await pending(second)).toHaveLength(1); const again = await tick(second); // settings come from chrome.storage.local, not from this run expect(again).toMatchObject({ kind: "uploaded", jobId: job.id }); expect(central.jobs.get(job.id)!.state).toBe("done"); expect(await pending(second)).toEqual([]); expect(central.requests.filter((x) => x.includes(`/jobs/${job.id}/accept`)).length).toBe(1); }, 120_000);
it("a worker with the wrong token is told unauthorized and takes nothing", async () => { for (const j of central.jobs.values()) if (j.state === "offered") j.state = "failed"; const { job } = await handJob(); await configure(host!, "not-the-token"); expect(await tick(host!)).toEqual({ kind: "unauthorized" }); expect(central.jobs.get(job.id)!.state).toBe("offered"); }, 60_000);});