scripts/test-dev-dashboard.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import http from "node:http";
import { createRequire } from "node:module";
import { mkdtemp, mkdir, writeFile, symlink, rm } from "node:fs/promises";
import net from "node:net";
import { tmpdir } from "node:os";
import { resolve, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { test } from "node:test";
test("embedded dashboard starts real Vite and authenticates shutdown", { timeout: 180000 }, async (t) => {
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
const reservation = net.createServer();
await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve));
const port = reservation.address().port;
await new Promise(resolve => reservation.close(resolve));
const session = randomUUID();
const dependencies = randomUUID();
const child = spawn(process.execPath, [resolve(root, "firecrab-cli/src/micromanager/dev/dashboard.mjs"), root, String(port)], {
cwd: tmpdir(), env: { ...process.env, FIRECRAB_DEV_SESSION: session, FIRECRAB_DEV_DEPENDENCIES: dependencies }, stdio: ["ignore", "pipe", "pipe"],
});
const exited = new Promise(resolve => child.once("exit", (code) => resolve(code)));
let logs = "";
child.stdout.on("data", value => logs += value);
child.stderr.on("data", value => logs += value);
t.after(() => { if (child.exitCode === null) child.kill(); });
const url = `http://127.0.0.1:${port}`;
const headers = { "X-Firecrab-Dev-Session": session };
let ready = false;
for (let attempt = 0; attempt < 1500; attempt++) {
if (child.exitCode !== null) throw new Error(logs);
try {
const response = await fetch(`${url}/__firecrab_dev/`, { headers });
if (response.headers.get("content-type")?.includes("application/json")) {
assert.deepEqual(await response.json(), { source: root, session, dependencies, port, api_port: 5523, resource_port: 5524, isolated: false });
ready = true; break;
}
} catch {}
await new Promise(resolve => setTimeout(resolve, 100));
}
assert.ok(ready, `Vite did not answer the ownership endpoint: ${logs}`);
const page = await fetch(url);
assert.equal(page.status, 200);
const html = await page.text();
assert.match(html, /name="firecrab-resource-endpoint" content="\/__firecrab_resources\/api\/service\/resources"/);
assert.ok(!html.includes(session), "public bootstrap must not expose the ownership credential");
assert.equal(page.headers.get("content-security-policy"), null);
assert.equal((await fetch(`${url}/@vite/client`)).status, 200);
const terminal = await fetch(`${url}/src/components/Console.tsx`);
assert.equal(terminal.status, 200, await terminal.text());
const main = await fetch(`${url}/src/main.tsx`);
assert.equal(main.status, 200, await main.text());
assert.equal((await fetch(`${url}/__firecrab_dev/stop`, { method: "POST" })).status, 403);
assert.equal((await fetch(`${url}/__firecrab_dev/stop`, { method: "POST", headers: { "X-Firecrab-Dev-Session": randomUUID() } })).status, 403);
assert.equal((await fetch(`${url}/__firecrab_dev/stop`, { method: "POST", headers })).status, 202);
assert.equal(await exited, 0, logs);
});
test("isolated dashboards reject missing private backend ports before listening", () => {
const repository = fileURLToPath(new URL("..", import.meta.url));
for (const [api, resource] of [["", "45122"], ["45121", ""], ["5523x", "45122"]]) {
const child = spawnSync(process.execPath, [join(repository, "firecrab-cli/src/micromanager/dev/dashboard.mjs"), repository, "8098"], {
env: {
...process.env, FIRECRAB_DEV_SESSION: randomUUID(), FIRECRAB_DEV_ISOLATED: "1",
FIRECRAB_DEV_API_PORT: api, FIRECRAB_DEV_RESOURCE_PORT: resource,
}, encoding: "utf8", timeout: 10000,
});
assert.notEqual(child.status, 0);
assert.match(child.stderr, /Invalid development dashboard\/backend port/);
}
});
test("isolated dashboards keep private API/resource routes and stop independently", { timeout: 180000 }, async (t) => {
const repository = fileURLToPath(new URL("..", import.meta.url));
const temporary = await mkdtemp(join(tmpdir(), "firecrab-isolated-dashboards-"));
t.after(() => rm(temporary, { recursive: true, force: true }));
const backend = async (identity) => {
const listener = http.createServer((request, response) => {
const { host, origin } = request.headers;
const allowed = !identity.endsWith("-api") || !origin || origin === `http://${host}`;
response.writeHead(allowed ? 200 : 403, { "Content-Type": "application/json", "Connection": "close" });
response.end(JSON.stringify({ identity, host, origin, path: request.url }));
});
await new Promise(resolve => listener.listen(0, "127.0.0.1", resolve));
t.after(() => new Promise(resolve => listener.close(resolve)));
return listener.address().port;
};
const launch = async (name) => {
const root = join(temporary, name);
const frontend = join(root, "firecrab-frontend");
await mkdir(frontend, { recursive: true });
await writeFile(join(frontend, "package.json"), '{"type":"module"}');
await writeFile(join(frontend, "index.html"), "<html><head><title>Legacy checkout</title></head><body>fixture</body></html>");
await symlink(join(repository, "firecrab-frontend/node_modules"), join(frontend, "node_modules"), "junction");
// An older source checkout still points to the primary API and permits any
// Host. The embedded launcher must override it for a private instance.
await writeFile(join(frontend, "vite.config.js"), `export default {server:{allowedHosts:true,proxy:{'/api':'http://127.0.0.1:5523','/auth/callback':'http://127.0.0.1:5523','/ws':{target:'ws://127.0.0.1:5523',ws:true}}}};`);
const api_port = await backend(`${name}-api`);
const resource_port = await backend(`${name}-resources`);
const reservation = net.createServer();
await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve));
const port = reservation.address().port;
await new Promise(resolve => reservation.close(resolve));
const session = randomUUID();
const dependencies = randomUUID();
const child = spawn(process.execPath, [join(repository, "firecrab-cli/src/micromanager/dev/dashboard.mjs"), root, String(port)], {
cwd: tmpdir(), env: {
...process.env, FIRECRAB_DEV_SESSION: session, FIRECRAB_DEV_DEPENDENCIES: dependencies,
FIRECRAB_DEV_API_PORT: String(api_port), FIRECRAB_DEV_RESOURCE_PORT: String(resource_port), FIRECRAB_DEV_ISOLATED: "1",
}, stdio: ["ignore", "pipe", "pipe"],
});
const exited = new Promise(resolve => child.once("exit", resolve));
let logs = "";
child.stdout.on("data", value => logs += value);
child.stderr.on("data", value => logs += value);
t.after(() => { if (child.exitCode === null) child.kill(); });
const url = `http://127.0.0.1:${port}`;
const headers = { "X-Firecrab-Dev-Session": session };
let ready = false;
for (let attempt = 0; attempt < 300; attempt++) {
if (child.exitCode !== null) throw new Error(logs);
try {
const response = await fetch(`${url}/__firecrab_dev/`, { headers });
if (response.ok) {
assert.deepEqual(await response.json(), { source: root, session, dependencies, port, api_port, resource_port, isolated: true });
ready = true; break;
}
} catch {}
await new Promise(resolve => setTimeout(resolve, 100));
}
assert.ok(ready, logs);
const stop = async () => {
assert.equal((await fetch(`${url}/__firecrab_dev/stop`, { method: "POST", headers })).status, 202);
assert.equal(await exited, 0, logs);
};
return { name, url, session, resource_port, stop };
};
const first = await launch("first");
const second = await launch("second");
for (const instance of [first, second]) {
const headers = { Origin: instance.url };
const api = await (await fetch(`${instance.url}/api/host`, { headers })).json();
assert.equal(api.identity, `${instance.name}-api`);
assert.equal(api.host, instance.url.slice("http://".length));
assert.equal(api.origin, instance.url);
const callbackPath = "/auth/callback?state=fixture-state&code=fixture-code";
const callback = await (await fetch(`${instance.url}${callbackPath}`)).json();
assert.equal(callback.identity, `${instance.name}-api`);
assert.equal(callback.host, instance.url.slice("http://".length));
assert.equal(callback.path, callbackPath);
assert.equal((await fetch(`${instance.url}/api/host`, { headers: { Origin: "https://evil.example" } })).status, 403);
const resources = await (await fetch(`${instance.url}/__firecrab_resources/api/service/resources`, { headers })).json();
assert.equal(resources.identity, `${instance.name}-resources`);
assert.equal(resources.host, `127.0.0.1:${instance.resource_port}`);
assert.equal(resources.origin, instance.url);
assert.equal(resources.path, "/api/service/resources");
const page = await fetch(instance.url);
const html = await page.text();
assert.match(html, /name="firecrab-resource-endpoint"/);
assert.ok(!html.includes(instance.session));
const policy = page.headers.get("content-security-policy");
assert.equal(policy, `connect-src 'self' ws://localhost:${new URL(instance.url).port} ws://127.0.0.1:${new URL(instance.url).port}`);
assert.ok(!policy.includes(":5524"), "legacy resource requests must not reach the primary controller");
const badHost = await new Promise((resolve, reject) => {
const request = http.request(`${instance.url}/api/host`, { headers: { Host: "evil.example", Origin: "http://evil.example" } }, response => {
response.resume(); resolve(response.statusCode);
});
request.on("error", reject); request.end();
});
assert.equal(badHost, 403);
}
await first.stop();
assert.equal((await (await fetch(`${second.url}/api/host`)).json()).identity, "second-api");
assert.equal((await fetch(`${second.url}/__firecrab_resources/api/service/resources`)).status, 200);
await second.stop();
});
test("development API proxy preserves the dashboard origin on a custom port", { timeout: 180000 }, async (t) => {
const frontend = fileURLToPath(new URL("../firecrab-frontend/", import.meta.url));
const require = createRequire(resolve(frontend, "package.json"));
const { createServer, loadConfigFromFile } = await import(pathToFileURL(require.resolve("vite")).href);
// Match the API's same-origin policy without a VM or a listener on port 5523.
const callbacks = [];
const upstream = http.createServer((request, response) => {
const { host, origin } = request.headers;
if (request.url.startsWith("/auth/callback")) callbacks.push(request.url);
response.writeHead(!origin || origin === `http://${host}` ? 200 : 403, {
"Content-Type": "application/json", "Connection": "close",
});
response.end(JSON.stringify({ host, origin, method: request.method }));
});
await new Promise(resolve => upstream.listen(0, "127.0.0.1", resolve));
t.after(() => new Promise(resolve => upstream.close(resolve)));
const target = `http://127.0.0.1:${upstream.address().port}`;
const previousPort = process.env.FIRECRAB_DEV_API_PORT;
process.env.FIRECRAB_DEV_API_PORT = String(upstream.address().port);
let config;
try {
({ config } = await loadConfigFromFile({ command: "serve", mode: "test" }, resolve(frontend, "vite.config.ts")));
} finally {
if (previousPort === undefined) delete process.env.FIRECRAB_DEV_API_PORT;
else process.env.FIRECRAB_DEV_API_PORT = previousPort;
}
assert.equal(config.server.proxy["/api"].target, target);
assert.equal(config.server.proxy["/auth/callback"].target, target);
const server = await createServer({
...config, configFile: false, root: frontend,
server: {
...config.server, host: "127.0.0.1", port: 0, strictPort: true,
},
});
t.after(() => server.close());
await server.listen();
const port = server.httpServer.address().port;
const request = async (method, host, origin, path = "/api/proxy-origin") => {
const response = await new Promise((resolve, reject) => {
const pending = http.request({
hostname: "127.0.0.1", port, path, method,
headers: { Host: host, ...(origin ? { Origin: origin } : {}) },
}, resolve);
pending.on("error", reject);
pending.end();
});
let body = "";
for await (const chunk of response) body += chunk;
return { status: response.statusCode, body };
};
for (const hostname of ["localhost", "127.0.0.1"]) {
const host = `${hostname}:${port}`;
for (const method of ["GET", "POST"]) {
const response = await request(method, host, `http://${host}`);
assert.equal(response.status, 200, `${method} from http://${host}`);
assert.deepEqual(JSON.parse(response.body), { host, origin: `http://${host}`, method });
}
const callback = await request("GET", host, undefined, "/auth/callback?state=fixture-state&code=fixture-code");
assert.equal(callback.status, 200);
assert.deepEqual(JSON.parse(callback.body), { host, method: "GET" });
for (const origin of ["https://evil.example", `http://${hostname}:${port + 1}`]) {
assert.equal((await request("POST", host, origin)).status, 403, origin);
}
}
assert.deepEqual(callbacks, Array(2).fill("/auth/callback?state=fixture-state&code=fixture-code"));
const untrustedHost = `evil.example:${port}`;
assert.equal((await request("POST", untrustedHost, `http://${untrustedHost}`)).status, 403);
});