Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
- Use functions and plain objects, not classes.
- Start each source file with a comment that says what the file owns. Give each export a short JSDoc.
- Update `README.md` in the same change when behavior, config, or the control API changes.
- Test through a real proxy and a local upstream server in `packages/service/tests/`. Do not mock modules. Do not reach the internet.
- Test through a real proxy and a local upstream server in `packages/service/tests/`. Start them with `startTestService()` and `startUpstream()` of `tests/harness.ts`. Wrap a tight timing limit in `budget()`, so that it allows for coverage, unless coverage barely slows that check. Do not mock modules. Do not reach the internet.
- The proxy decrypts HTTPS. Never write a credential: every recording goes through `packages/service/src/secrets.ts`.
- The upstream host always comes from `allow`, never from the client.
- Write docs in ASD-STE100 English: common words, active voice, short sentences.
Expand Down
69 changes: 69 additions & 0 deletions packages/service/tests/harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* The shared setup of the service tests: a local upstream and a service in
* the test process. Each one closes when its test finishes.
*/
import { createHash } from "node:crypto";
import { createServer, type RequestListener, type Server } from "node:http";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { onTestFinished } from "vitest";
import {
startRoachService,
type RoachService,
type RoachServiceConfig,
} from "../src/service.ts";

/**
* The time limit of a timing check: `ms`, or 3 times `ms` with coverage.
* Coverage makes the service slower (`vitest.config.ts`).
*/
export const budget = (ms: number) =>
ms * Number(process.env.ROACH_TEST_SLOWDOWN ?? "1");

/** The write token of a test service. */
export const TOKEN = "write-token";

/** The SHA-256 of a token in hex, as `writeTokenHash` keeps it. */
const hash = (token: string) =>
createHash("sha256").update(token).digest("hex");

/**
* Start an HTTP upstream on 127.0.0.1 with `handler`. Returns the server
* and its origin. It closes when the test finishes.
*/
export async function startUpstream(
handler: RequestListener,
): Promise<{ server: Server; origin: string }> {
const server = createServer(handler);
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
onTestFinished(
() => new Promise<void>((resolve) => server.close(() => resolve())),
);
const address = server.address();
if (!address || typeof address === "string") throw new Error("No port");
return { server, origin: `http://127.0.0.1:${address.port}` };
}

/**
* Start a service in this process, with the recordings in a new directory.
* It can reach the upstream on 127.0.0.1 (`privateNetworks`), and its write
* token is `TOKEN`. `extra` adds to or replaces this config. The service
* closes, and its directory goes, when the test finishes.
*/
export async function startTestService(
extra: Partial<RoachServiceConfig> = {},
): Promise<{ service: RoachService; directory: string }> {
const directory = await mkdtemp(path.join(tmpdir(), "roach-"));
const service = await startRoachService({
directory,
privateNetworks: true,
writeTokenHash: hash(TOKEN),
...extra,
});
onTestFinished(async () => {
await service.close();
await rm(directory, { recursive: true, force: true });
});
return { service, directory };
}
37 changes: 6 additions & 31 deletions packages/service/tests/recording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,7 @@
* Recording and replay: modes, sessions, changing values, and misses. Each
* test starts runs on a service with a local upstream.
*/
import { createHash } from "node:crypto";
import { createServer, type Server } from "node:http";
import {
mkdir,
mkdtemp,
readdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { ProxyAgent, request } from "undici";
import {
Expand All @@ -28,15 +18,14 @@ import {
describeRecordingMisses,
describeRecordingStats,
} from "@sentry/roach/report";
import { startRoachService, type RoachService } from "../src/service.ts";
import type { RoachService } from "../src/service.ts";
import { startTestService, startUpstream, TOKEN } from "./harness.ts";
import { keyRequest } from "../src/request-key.ts";
import type { RecordingMode, RecordingRule } from "@sentry/roach/types";
import { extractValues, VALUE_PATTERNS } from "@sentry/roach/values";

const TOKEN = "write-token";
const TENANT = "acme/alpha";

let upstream: Server;
let origin: string;
let liveRequests: number;
/** The event stream that the upstream sends for a request body. */
Expand Down Expand Up @@ -184,7 +173,7 @@ beforeEach(async () => {
liveRequests = 0;
respond = () => `data: ${liveRequests}\n\n`;
upstreamGate = Promise.resolve();
upstream = createServer((incoming, outgoing) => {
({ origin } = await startUpstream((incoming, outgoing) => {
liveRequests += 1;
const chunks: Buffer[] = [];
incoming.on("data", (chunk: Buffer) => chunks.push(chunk));
Expand All @@ -193,28 +182,14 @@ beforeEach(async () => {
outgoing.writeHead(200, { "content-type": "text/event-stream" });
outgoing.end(respond(Buffer.concat(chunks).toString("utf8")));
});
});
await new Promise<void>((resolve) =>
upstream.listen(0, "127.0.0.1", resolve),
);
const address = upstream.address();
if (!address || typeof address === "string") throw new Error("No port");
origin = `http://127.0.0.1:${address.port}`;
directory = await mkdtemp(path.join(tmpdir(), "roach-"));
service = await startRoachService({
directory,
privateNetworks: true,
writeTokenHash: createHash("sha256").update(TOKEN).digest("hex"),
});
}));
({ service, directory } = await startTestService());
});

afterEach(async () => {
await agent?.close();
agent = undefined;
await Promise.all(runs.splice(0).map((run) => run.close().catch(() => {})));
await service.close();
await new Promise<void>((resolve) => upstream.close(() => resolve()));
await rm(directory, { recursive: true, force: true });
});

describe("recording", () => {
Expand Down
Loading
Loading