130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { createServer } from "node:http";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { FAULT_PROXY_SCRIPT } from "../src/fault-proxy-script.ts";
|
|
import type { Server } from "node:http";
|
|
|
|
function listen(server: Server): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const address = server.address();
|
|
if (typeof address === "object" && address !== null) resolve(address.port);
|
|
else reject(new Error("Test server did not expose a port."));
|
|
});
|
|
});
|
|
}
|
|
|
|
function close(server: Server): Promise<void> {
|
|
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
|
|
async function freePort(): Promise<number> {
|
|
const server = createServer();
|
|
const port = await listen(server);
|
|
await close(server);
|
|
return port;
|
|
}
|
|
|
|
test("fault proxy script exposes authenticated controls and preserves local fault semantics", async () => {
|
|
const upstream = createServer((request, response) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ path: request.url }));
|
|
});
|
|
const upstreamPort = await listen(upstream);
|
|
const proxyPort = await freePort();
|
|
const directory = await mkdtemp(join(tmpdir(), "openwork-fault-proxy-"));
|
|
const scriptPath = join(directory, "proxy.mjs");
|
|
await writeFile(scriptPath, FAULT_PROXY_SCRIPT);
|
|
const issuer = "https://fault-proxy.example.test";
|
|
const token = "test-control-token";
|
|
const child = spawn(process.execPath, [scriptPath], {
|
|
env: {
|
|
...process.env,
|
|
PORT: String(proxyPort),
|
|
UPSTREAM: `http://127.0.0.1:${upstreamPort}`,
|
|
ISSUER: issuer,
|
|
CONTROL_TOKEN: token,
|
|
},
|
|
stdio: "ignore",
|
|
});
|
|
const url = `http://127.0.0.1:${proxyPort}`;
|
|
const auth = { "x-openwork-fault-token": token };
|
|
const post = (path: string, body?: unknown): Promise<Response> => fetch(`${url}${path}`, {
|
|
method: "POST",
|
|
headers: body === undefined ? auth : { ...auth, "content-type": "application/json" },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
|
|
try {
|
|
const deadline = Date.now() + 5_000;
|
|
let health: Response | undefined;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
health = await fetch(`${url}/__openwork_faults/health`);
|
|
if (health.ok) break;
|
|
} catch {
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
}
|
|
assert(health?.ok, "fault proxy health did not become ready");
|
|
assert.deepEqual(await health.json(), { ok: true, issuer });
|
|
assert.equal((await fetch(`${url}/__openwork_faults/requests`)).status, 401);
|
|
assert.equal((await fetch(`${url}/__openwork_faults/clear`, { method: "POST" })).status, 401);
|
|
assert.equal((await post("/__openwork_faults/rules", {
|
|
kind: "status",
|
|
pathPrefix: "/flaky",
|
|
statusCode: 429,
|
|
times: 2,
|
|
body: { error: "injected" },
|
|
})).status, 204);
|
|
|
|
assert.equal((await fetch(`${url}/flaky/one`)).status, 429);
|
|
assert.equal((await fetch(`${url}/flaky/two`)).status, 429);
|
|
assert.equal((await fetch(`${url}/flaky/three`)).status, 200);
|
|
|
|
assert.equal((await post("/__openwork_faults/rules", {
|
|
kind: "latency",
|
|
pathPrefix: "/slow",
|
|
delayMs: 30,
|
|
})).status, 204);
|
|
const startedAt = Date.now();
|
|
assert.equal((await fetch(`${url}/slow`)).status, 200);
|
|
assert(Date.now() - startedAt >= 20);
|
|
|
|
assert.equal((await post("/__openwork_faults/rules", {
|
|
kind: "status",
|
|
pathPrefix: "/cleared",
|
|
statusCode: 500,
|
|
times: 3,
|
|
})).status, 204);
|
|
assert.equal((await post("/__openwork_faults/clear")).status, 204);
|
|
assert.equal((await fetch(`${url}/cleared`)).status, 200);
|
|
|
|
const logResponse = await fetch(`${url}/__openwork_faults/requests`, { headers: auth });
|
|
assert.equal(logResponse.status, 200);
|
|
const log: unknown = await logResponse.json();
|
|
assert.deepEqual(
|
|
typeof log === "object" && log !== null && "requests" in log && Array.isArray(log.requests)
|
|
? log.requests.map((entry) => ({ path: entry.path, status: entry.status, faulted: entry.faulted }))
|
|
: log,
|
|
[
|
|
{ path: "/flaky/one", status: 429, faulted: true },
|
|
{ path: "/flaky/two", status: 429, faulted: true },
|
|
{ path: "/flaky/three", status: 200, faulted: false },
|
|
{ path: "/slow", status: 200, faulted: true },
|
|
{ path: "/cleared", status: 200, faulted: false },
|
|
],
|
|
);
|
|
} finally {
|
|
child.kill("SIGTERM");
|
|
await close(upstream);
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|