test(helpers): add db, fetch, app, and auth test helpers
withTestDb sets up an in-memory SQLite handle with migrations applied, swaps the production singleton via _setDbForTest, and restores in a finally block. installFetchStub captures every outbound request and throws if restored twice. requestApp wraps Oak's app.handle so route tests can run without listen(). stubAuthenticate flips the server's authenticator indirection.
This commit is contained in:
49
tests/_helpers/app.ts
Normal file
49
tests/_helpers/app.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Application } from "@oak/oak";
|
||||||
|
|
||||||
|
export type RequestOptions = {
|
||||||
|
method?: string;
|
||||||
|
path: string;
|
||||||
|
headers?: HeadersInit;
|
||||||
|
body?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TestResponse = {
|
||||||
|
status: number;
|
||||||
|
headers: Headers;
|
||||||
|
text: string;
|
||||||
|
json: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestApp = async (
|
||||||
|
app: Application,
|
||||||
|
opts: RequestOptions,
|
||||||
|
): Promise<TestResponse> => {
|
||||||
|
const method = opts.method ?? "GET";
|
||||||
|
const url = `http://test.local${opts.path}`;
|
||||||
|
const init: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: opts.headers,
|
||||||
|
};
|
||||||
|
if (opts.body !== undefined && method !== "GET" && method !== "HEAD") {
|
||||||
|
init.body = typeof opts.body === "string"
|
||||||
|
? opts.body
|
||||||
|
: JSON.stringify(opts.body);
|
||||||
|
const headers = new Headers(opts.headers);
|
||||||
|
if (!headers.has("Content-Type")) {
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
init.headers = headers;
|
||||||
|
}
|
||||||
|
const req = new Request(url, init);
|
||||||
|
const res = await app.handle(req);
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("app.handle returned undefined");
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers,
|
||||||
|
text,
|
||||||
|
json: () => JSON.parse(text),
|
||||||
|
};
|
||||||
|
};
|
||||||
18
tests/_helpers/auth.ts
Normal file
18
tests/_helpers/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
_resetAuthenticatorForTest,
|
||||||
|
_setAuthenticatorForTest,
|
||||||
|
} from "../../server.ts";
|
||||||
|
|
||||||
|
export type AuthStub = { restore: () => void };
|
||||||
|
|
||||||
|
export const stubAuthenticate = (didOrError: string | Error): AuthStub => {
|
||||||
|
_setAuthenticatorForTest(() => {
|
||||||
|
if (didOrError instanceof Error) {
|
||||||
|
return Promise.reject(didOrError);
|
||||||
|
}
|
||||||
|
return Promise.resolve(didOrError);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
restore: () => _resetAuthenticatorForTest(),
|
||||||
|
};
|
||||||
|
};
|
||||||
60
tests/_helpers/db.ts
Normal file
60
tests/_helpers/db.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Database } from "@db/sqlite";
|
||||||
|
import {
|
||||||
|
_resetDbForTest,
|
||||||
|
_setDbForTest,
|
||||||
|
addWebhookSubscription,
|
||||||
|
upsertNote,
|
||||||
|
type WebhookVerb,
|
||||||
|
} from "../../src/data/db.ts";
|
||||||
|
import { applyMigrations } from "../../src/migrations/apply.ts";
|
||||||
|
import type { Note } from "../../src/data/note.ts";
|
||||||
|
|
||||||
|
export const withTestDb = (
|
||||||
|
fn: (db: Database) => Promise<void> | void,
|
||||||
|
): () => Promise<void> => {
|
||||||
|
return async () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
applyMigrations(db);
|
||||||
|
_setDbForTest(db);
|
||||||
|
try {
|
||||||
|
await fn(db);
|
||||||
|
} finally {
|
||||||
|
_resetDbForTest();
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let seedCounter = 0;
|
||||||
|
|
||||||
|
export const seedNote = (overrides: Partial<Note> = {}): Note => {
|
||||||
|
seedCounter++;
|
||||||
|
const note: Note = {
|
||||||
|
did: overrides.did ?? "did:plc:test",
|
||||||
|
rkey: overrides.rkey ?? `seed${seedCounter.toString().padStart(6, "0")}`,
|
||||||
|
title: overrides.title ?? "Test title",
|
||||||
|
publishedAt: overrides.publishedAt ?? "2026-01-01T00:00:00.000Z",
|
||||||
|
createdAt: overrides.createdAt ?? "2026-01-01T00:00:00.000Z",
|
||||||
|
discoverable: overrides.discoverable,
|
||||||
|
language: overrides.language,
|
||||||
|
content: overrides.content,
|
||||||
|
};
|
||||||
|
upsertNote(note);
|
||||||
|
return note;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedWebhook = (overrides: {
|
||||||
|
did?: string;
|
||||||
|
method?: string;
|
||||||
|
url?: string;
|
||||||
|
token?: string;
|
||||||
|
verb?: WebhookVerb;
|
||||||
|
} = {}) => {
|
||||||
|
return addWebhookSubscription({
|
||||||
|
did: overrides.did ?? "did:plc:test",
|
||||||
|
method: overrides.method ?? "POST",
|
||||||
|
url: overrides.url ?? "https://example.com/hook",
|
||||||
|
token: overrides.token,
|
||||||
|
verb: overrides.verb ?? "create",
|
||||||
|
});
|
||||||
|
};
|
||||||
57
tests/_helpers/fetch_stub.ts
Normal file
57
tests/_helpers/fetch_stub.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export type FetchHandler = (
|
||||||
|
req: Request,
|
||||||
|
) => Response | Promise<Response>;
|
||||||
|
|
||||||
|
export type CapturedRequest = {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
headers: Headers;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FetchStub = {
|
||||||
|
calls: CapturedRequest[];
|
||||||
|
restore: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const installFetchStub = (handler: FetchHandler): FetchStub => {
|
||||||
|
const original = globalThis.fetch;
|
||||||
|
const calls: CapturedRequest[] = [];
|
||||||
|
let restored = false;
|
||||||
|
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
const req = new Request(input as RequestInfo, init);
|
||||||
|
const body = req.method === "GET" || req.method === "HEAD"
|
||||||
|
? ""
|
||||||
|
: await req.clone().text();
|
||||||
|
calls.push({
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: new Headers(req.headers),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
return await handler(req);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
restore: () => {
|
||||||
|
if (restored) throw new Error("FetchStub.restore() called twice");
|
||||||
|
restored = true;
|
||||||
|
globalThis.fetch = original;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jsonResponse = (
|
||||||
|
body: unknown,
|
||||||
|
init: ResponseInit = {},
|
||||||
|
): Response => {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status: init.status ?? 200,
|
||||||
|
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const errorResponse = (status: number, text = "error"): Response =>
|
||||||
|
new Response(text, { status });
|
||||||
Reference in New Issue
Block a user