server/boat-included.e2e.test.ts
Rama main · réplica de lectura del repositorio original.
// One stub plays Boat (/boat) and the Admin's relay (/relay), so neither key is ever sent to the other side. Disposable home; no network.
import { randomUUID } from "node:crypto";
import { spawn, type ChildProcess } from "node:child_process";
import { createServer, type Server } from "node:http";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, expect, it } from "vitest";
import { removeTempDir, waitForExit } from "./testing/cleanup.ts";
import { freePortBlock } from "./testing/ports.ts";
const SERVER_DIR = dirname(fileURLToPath(import.meta.url));
const INCLUDED = `box_omb_${randomUUID()}`;
const OWN = "box_own_person_key";
const INCLUDED_BOX = "bx_23456789";
let stub: Server;
let stubBase = "";
/** Computers each account holds, by stub prefix. */
const accounts: Record<string, Array<{ id: string; name: string; state: string }>> = { boat: [], relay: [] };
const requests: Array<{ side: string; method: string; path: string; auth: string }> = [];
let home: string;
let journal: string;
let base: string;
let child: ChildProcess;
let log = "";
async function api(method: string, path: string, body?: unknown) {
const response = await fetch(`${base}${path}`, {
method,
headers: body === undefined ? {} : { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return { status: response.status, body: await response.json().catch(() => null) as any };
}
beforeAll(async () => {
stub = createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://stub.test");
req.resume();
req.on("end", () => {
const [, side = ""] = /^\/(boat|relay)\//.exec(url.pathname) ?? [];
requests.push({ side, method: req.method ?? "GET", path: url.pathname, auth: String(req.headers.authorization ?? "") });
const send = (status: number, payload: unknown) => {
res.writeHead(status, { "content-type": "application/json" });
res.end(JSON.stringify(payload));
};
const expected = side === "relay" ? `Bearer ${INCLUDED}` : side === "boat" ? `Bearer ${OWN}` : "";
if (!side || req.headers.authorization !== expected) return send(401, { ok: false, code: "unauthorized", message: "Unauthorized" });
// Boat speaks its public API v1; the Admin relay still speaks the old box dialect.
const legacy = side === "relay";
const path = url.pathname.replace(legacy ? /^\/relay\/api\/box\/v1/ : /^\/boat\/api\/v1/, "");
const [root, one, many] = legacy ? ["/boxes", "box", "boxes"] : ["/sandboxes", "sandbox", "sandboxes"];
const rows = accounts[side]!;
if (path === root && req.method === "GET") return send(200, { ok: true, [many]: rows, pageInfo: { nextCursor: null } });
const box = new RegExp(`^${root}/([^/]+)$`).exec(path);
if (box && req.method === "GET") {
const row = rows.find((candidate) => candidate.id === box[1]);
return row ? send(200, { ok: true, [one]: row }) : send(404, { ok: false, code: "not_found", message: "Not found" });
}
send(404, { ok: false, code: "not_found", message: "Not found" });
});
});
await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
stubBase = `http://127.0.0.1:${(stub.address() as { port: number }).port}`;
home = mkdtempSync(join(tmpdir(), "omb-boat-included-"));
const dataDir = join(home, ".openmausbot");
mkdirSync(dataDir, { recursive: true });
// One deliberately unknown engine: nothing probes an installed CLI.
writeFileSync(join(dataDir, "config.json"), JSON.stringify({ instances: { fixture: { driver: "not-a-real-driver" } } }));
// A computer this installation made on the included account.
accounts.relay = [{ id: INCLUDED_BOX, name: "ogb-included-fixture", state: "archived" }];
journal = join(dataDir, "box-create-requests.json");
writeFileSync(journal, JSON.stringify({ version: 1, requests: [{
botId: "included-fixture-bot", requestBody: JSON.stringify({ fixture: true }), idempotencyKey: randomUUID(),
createdAt: Date.now(), boxId: INCLUDED_BOX, resolved: true,
}] }));
const port = await freePortBlock([0, 1]);
base = `http://127.0.0.1:${port}`;
child = spawn(process.execPath, [join(SERVER_DIR, "index.ts")], {
cwd: join(SERVER_DIR, ".."),
env: {
PATH: process.env.PATH,
...(process.env.PATHEXT ? { PATHEXT: process.env.PATHEXT } : {}),
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
HOME: home, USERPROFILE: home, OMB_DATA_DIR: dataDir, OMB_PORT: String(port), OMB_WEBHOOK_PORT: String(port + 1),
OMB_BOAT_API: `${stubBase}/boat/api/v1`,
OMB_CLOUD_BOAT_URL: `${stubBase}/relay/api/box/v1`,
OMB_CLOUD_BOAT_TOKEN: INCLUDED,
},
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout?.on("data", (chunk) => { log += chunk; });
child.stderr?.on("data", (chunk) => { log += chunk; });
const deadline = Date.now() + 20_000;
for (;;) {
if (child.exitCode !== null) throw new Error(`the server exited:\n${log}`);
try { if ((await api("GET", "/api/health")).body?.pid === child.pid) break; } catch {}
if (Date.now() > deadline) throw new Error(`the server did not start:\n${log}`);
await new Promise((resolve) => setTimeout(resolve, 100));
}
}, 30_000);
afterAll(async () => {
if (child) await waitForExit(child, { signal: "SIGTERM" });
if (stub) await new Promise<void>((resolve) => stub.close(() => resolve()));
if (home) await removeTempDir(home);
});
const crossed = () => requests.filter((request) =>
(request.side === "relay" && request.auth.includes(OWN)) || (request.side === "boat" && request.auth.includes(INCLUDED)));
it("refuses an own Boat key while this installation has computers on the included account", async () => {
expect((await api("GET", "/api/config")).body.box).toEqual({ configured: true, included: true });
const refused = await api("PUT", "/api/config", { box: { token: OWN } });
expect(refused.status).toBe(409);
expect(refused.body.error).toBe("that Boat token cannot access the remembered cloud computers from this installation");
expect((await api("GET", "/api/config")).body.box).toEqual({ configured: true, included: true });
expect(readFileSync(join(home, ".openmausbot", "config.json"), "utf8")).not.toContain(OWN);
expect(crossed()).toEqual([]);
});
it("once they are gone, the own key wins; clearing it falls back to the included account", async () => {
// The included computer was deleted (its record retired).
accounts.relay = [];
writeFileSync(journal, JSON.stringify({ version: 1, requests: [] }));
const added = await api("PUT", "/api/config", { box: { token: OWN } });
expect(added.status, JSON.stringify(added.body)).toBe(200);
expect(added.body.box).toEqual({ configured: true, last4: OWN.slice(-4) });
const cleared = await api("PUT", "/api/config", { box: { token: "" } });
expect(cleared.status, JSON.stringify(cleared.body)).toBe(200);
expect(cleared.body.box).toEqual({ configured: true, included: true });
expect(readFileSync(join(home, ".openmausbot", "config.json"), "utf8")).not.toContain(INCLUDED);
expect(requests.some((request) => request.side === "boat" && request.auth === `Bearer ${OWN}`)).toBe(true);
expect(crossed()).toEqual([]);
expect(log).not.toContain(INCLUDED);
});