Something went wrong. Try again.
Something went wrong. Try again.
6.1 kB · 109 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110import { afterEach, beforeEach, describe, expect, it } from "vitest";import { execFileSync } from "node:child_process";import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import type { AddressInfo } from "node:net";import type { Server } from "node:http";import { parseCentralArgs } from "../../central/centralArgs";import { runOperator } from "../../central/operator";import { Central } from "../../central/service";import { CentralStore } from "../../central/store";import { createCentralServer } from "../../central/http";import { ensureDataDir, loadOrCreateAdminToken } from "../../central/keys";import { devKeys } from "../helpers/centralKeys";
/** The operator commands with real files: tokens written mode 600 and never printed, archives written to disk, errors as exit codes. */
let dir: string;let data: string;let server: Server;let store: CentralStore;let url: string;let admin: string;let repo: string;
beforeEach(async () => { dir = mkdtempSync(join(tmpdir(), "aite-operatorfn-")); repo = join(dir, "tests"); data = join(dir, "data"); mkdirSync(join(repo, "scripts"), { recursive: true }); execFileSync("git", ["init", "-q", "-b", "main", repo]); writeFileSync(join(repo, "scripts/a.aite"), 'test "a"\n start http://127.0.0.1:4780/x.html\n step "s"\n sleep 1ms\n'); execFileSync("git", ["-C", repo, "add", "-A"]); execFileSync("git", ["-C", repo, "-c", "user.email=t@aite.invalid", "-c", "user.name=t", "commit", "-q", "-m", "t"]); ensureDataDir(data); admin = loadOrCreateAdminToken(data); store = new CentralStore(join(data, "central.db")); const central = new Central({ store, dataDir: data, now: Date.now, ...devKeys(data).deps, leaseMs: 60_000, maxAttempts: 3, maxArchiveBytes: 1_000_000 }); const hosts: string[] = []; server = createCentralServer({ central, adminToken: admin, hosts, testRepo: repo, maxArchiveBytes: 1_000_000 }); await new Promise<void>((r) => server.listen(0, "127.0.0.1", r)); const port = (server.address() as AddressInfo).port; hosts.push(`127.0.0.1:${port}`); url = `http://127.0.0.1:${port}`;});afterEach(async () => { await new Promise<void>((r) => { server.closeAllConnections(); server.close(() => r()); }); store.close(); rmSync(dir, { recursive: true, force: true });});
async function op(args: string[]) { const p = parseCentralArgs([...args, "--dir", data, "--url", url]); if (!p.ok || p.command.kind === "serve") throw new Error("parse"); const out: string[] = []; const err: string[] = []; const code = await runOperator(p.command, { fetch, adminToken: readFileSync(join(data, "admin-token"), "utf8").trim(), print: (t) => void out.push(t), error: (t) => void err.push(t), writeSecret: (path, text) => { mkdirSync(join(path, ".."), { recursive: true }); writeFileSync(path, text, { mode: 0o600 }); chmodSync(path, 0o600); }, writeFile: (path, bytes) => writeFileSync(path, bytes), readFile: (path) => readFileSync(path, "utf8"), }); return { code, out: out.join("\n"), err: err.join("\n") };}
async function addDevice(name = "d", file = join(dir, `code-${Math.random().toString(36).slice(2)}`)) { const r = await op(["enrol", "--person", "did:plc:alex", "--origin", "http://127.0.0.1:4780", "--name", name, "--code-out", file]); const code = readFileSync(file, "utf8").trim(); const res = await fetch(`${url}/api/enrol`, { method: "POST", body: JSON.stringify({ code }) }); return { r, code, file, status: res.status };}
describe("operator commands with real files", () => { it("enrol writes the one-time code to a file only the owner can read and prints no secret; the device redeems it; list and revoke follow", async () => { const file = join(dir, "secrets/dev.code"); const { r, code, status } = await addDevice("balrog", file); expect(r.code).toBe(0); expect(status).toBe(201); expect(code).toMatch(/^[A-Z2-7]{4}(-[A-Z2-7]{4}){3}$/); expect(r.out).not.toContain(code); expect(statSync(file).mode & 0o777).toBe(0o600); const list = JSON.parse((await op(["device", "list"])).out); expect(list.devices).toHaveLength(1); expect((await op(["device", "revoke", list.devices[0].id])).code).toBe(0); expect((await op(["device", "revoke", list.devices[0].id])).code).toBe(1); }); it("job create from a commit, list and show; an archive that does not exist is an error and writes nothing", async () => { await addDevice(); const dev = JSON.parse((await op(["device", "list"])).out).devices[0].id as string; const made = await op(["job", "create", "--device", dev, "--ref", "main", "--file", "scripts/a.aite", "--start", "http://127.0.0.1:4780/x.html", "--allow", "http://127.0.0.1:4780"]); expect(made.code, made.err).toBe(0); const id = JSON.parse(made.out).job.id as string; expect(JSON.parse((await op(["job", "list"])).out).jobs.map((j: { id: string }) => j.id)).toEqual([id]); expect(JSON.parse((await op(["job", "show", id])).out).job.state).toBe("offered"); const out = join(dir, "none.zip"); expect((await op(["job", "archive", id, "--out", out])).code).toBe(1); expect(() => statSync(out)).toThrow(); expect((await op(["job", "cancel", id])).code).toBe(0); }); it("key prints the release key and the dispatch key", async () => { expect((await op(["key"])).out).toBe(`release key (bundles and the revocation list): ${readFileSync(join(data, "signing-key.pub"), "utf8").trim()}\ndispatch key (jobs): ${readFileSync(join(data, "dispatch-key.pub"), "utf8").trim()}`); }); it("a refused job exits 1 with the service's reason", async () => { await addDevice(); const dev = JSON.parse((await op(["device", "list"])).out).devices[0].id as string; const r = await op(["job", "create", "--device", dev, "--ref", "main", "--file", "scripts/a.aite", "--start", "https://evil.example/", "--allow", "https://evil.example"]); expect(r.code).toBe(1); expect(r.err).toContain("not allowed on this device"); });});