Files

156 lines
5.7 KiB
TypeScript

import assert from "node:assert/strict";
import { createServer, request as httpRequest } from "node:http";
import test from "node:test";
import { denFetch } from "@openwork/behaviors";
import type { Place } from "../src/place.ts";
import { faultProxy } from "../src/faults.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("Upstream 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()));
}
function absoluteGet(proxyUrl: string, target: string): Promise<string> {
const proxy = new URL(proxyUrl);
return new Promise((resolve, reject) => {
const request = httpRequest({ hostname: proxy.hostname, port: proxy.port, path: target }, (response) => {
response.setEncoding("utf8");
let body = "";
response.on("data", (chunk: string) => body += chunk);
response.on("end", () => resolve(body));
});
request.on("error", reject);
request.end();
});
}
test("faultProxy consumes status and latency rules before passing through", async () => {
const upstream = createServer((request, response) => {
response.writeHead(200, { "content-type": "application/json", "x-upstream": "yes" });
response.end(JSON.stringify({ method: request.method, path: request.url }));
});
const port = await listen(upstream);
try {
await using proxy = await faultProxy({
apiUrl: `http://127.0.0.1:${port}`,
webUrl: `http://127.0.0.1:${port}`,
});
await proxy.faults.status("/api/den/flaky", 429, { times: 2, body: { error: "slow down" } });
const first = await fetch(`${proxy.ref.webUrl}/api/den/flaky`);
const second = await fetch(`${proxy.ref.webUrl}/api/den/flaky`);
const passed = await fetch(`${proxy.ref.webUrl}/api/den/flaky`);
assert.equal(first.status, 429);
assert.deepEqual(await first.json(), { error: "slow down" });
assert.equal(second.status, 429);
assert.equal(passed.status, 200);
assert.equal(passed.headers.get("x-upstream"), "yes");
assert.deepEqual(await passed.json(), { method: "GET", path: "/api/den/flaky" });
await proxy.faults.latency("/delayed", 25);
const startedAt = Date.now();
const delayed = await fetch(`${proxy.ref.webUrl}/delayed`);
assert.equal(delayed.status, 200);
assert(Date.now() - startedAt >= 15);
assert.equal((await fetch(`${proxy.ref.webUrl}/delayed`)).status, 200);
const behaviorResult = await denFetch(proxy.ref, "/behavior");
assert.equal(behaviorResult.response.status, 200);
assert.deepEqual(behaviorResult.body, { method: "GET", path: "/api/den/behavior" });
assert.deepEqual(
proxy.requests.map(({ path, status, faulted }) => ({ path, status, faulted })),
[
{ path: "/api/den/flaky", status: 429, faulted: true },
{ path: "/api/den/flaky", status: 429, faulted: true },
{ path: "/api/den/flaky", status: 200, faulted: false },
{ path: "/delayed", status: 200, faulted: true },
{ path: "/delayed", status: 200, faulted: false },
{ path: "/api/den/behavior", status: 200, faulted: false },
],
);
assert.deepEqual(await proxy.requestLog(), proxy.requests);
assert.notEqual(await proxy.requestLog(), proxy.requests);
} finally {
await close(upstream);
}
});
test("faultProxy clear removes pending rules", async () => {
const upstream = createServer((_request, response) => {
response.writeHead(204);
response.end();
});
const port = await listen(upstream);
try {
await using proxy = await faultProxy({
apiUrl: `http://127.0.0.1:${port}`,
webUrl: `http://127.0.0.1:${port}`,
});
await proxy.faults.status("/", 500, { times: 3 });
await proxy.faults.clear();
assert.equal((await fetch(proxy.ref.webUrl)).status, 204);
assert.equal(proxy.requests[0]?.faulted, false);
} finally {
await close(upstream);
}
});
test("faultProxy requires the Den sandbox id for Daytona placement", async () => {
const place: Place = {
kind: "daytona",
host: () => undefined,
db: async () => { throw new Error("unused"); },
exposeMock: async () => { throw new Error("unused"); },
denBase: () => ({ kind: "daytona", ref: "dev" }),
};
await assert.rejects(
faultProxy(
{ apiUrl: "https://den-api.example.test", webUrl: "https://den.example.test" },
{ place, sandbox: undefined },
),
/fault proxy on Daytona needs the Den sandbox id; pass `sandbox: den\.placement\.sandboxId`/,
);
});
test("faultProxy pins absolute-form request targets to its upstream", async () => {
let attackerRequests = 0;
let upstreamRequestUrl: string | undefined;
const upstream = createServer((request, response) => {
upstreamRequestUrl = request.url;
response.end("upstream");
});
const attacker = createServer((_request, response) => {
attackerRequests += 1;
response.end("attacker");
});
const [upstreamPort, attackerPort] = await Promise.all([listen(upstream), listen(attacker)]);
try {
await using proxy = await faultProxy({
apiUrl: `http://127.0.0.1:${upstreamPort}`,
webUrl: `http://127.0.0.1:${upstreamPort}`,
});
assert.equal(
await absoluteGet(proxy.ref.webUrl, `http://127.0.0.1:${attackerPort}/steered?x=1`),
"upstream",
);
assert.equal(upstreamRequestUrl, "/steered?x=1");
assert.equal(attackerRequests, 0);
} finally {
await Promise.all([close(upstream), close(attacker)]);
}
});