import { test, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import worker, { type Env } from "./worker.ts";
const SECRET = "whsec_test";
const SESSION = "3f9c1b60-4e2a-4d18-9a77-6c0b2e5d81af";
const TURN = "turn_21c17d7a76de4338a4e2cc2f";
const fixtures = {
completed: { object: "event", type: "turn.completed", created_at: 1790345031466, data: { session_id: SESSION, turn_id: TURN } },
failed: { object: "event", type: "turn.failed", created_at: 1790345031999, data: { session_id: SESSION, turn_id: TURN } },
actionRequired: { object: "event", type: "session.action_required", created_at: 1790345029549, data: { session_id: SESSION, required_action: { type: "question" } } },
};
const api: Record<string, unknown> = {
[`/v1/sessions/${SESSION}`]: { object: "session", id: SESSION, metadata: { order: "A-4471" } },
[`/v1/sessions/${SESSION}/turns/${TURN}`]: { object: "turn", id: TURN, status: "completed", error: null },
[`/v1/sessions/${SESSION}/turns?limit=1`]: { object: "list", data: [{ object: "turn", id: TURN, status: "completed", error: null }] },
[`/v1/sessions/${SESSION}/items?limit=20`]: { object: "list", data: [{ type: "message", role: "assistant", content: "shipped" }] },
};
function sign(timestamp: string, body: string): string {
return createHmac("sha256", SECRET).update(`${timestamp}.${body}`).digest("hex");
}
function kv(): KVNamespace & { size(): number } {
const map = new Map<string, string>();
return { get: async (k) => map.get(k) ?? null, put: async (k, v) => { map.set(k, v); }, size: () => map.size };
}
let env: Env & { RESULTS: ReturnType<typeof kv> };
let pending: Promise<unknown>[];
let calls: string[];
beforeEach(() => {
env = { GOBARE_API: "https://api.gobare.test", GOBARE_TOKEN: "gbr_pat_test", GOBARE_WEBHOOK_SECRET: SECRET, SEEN: kv(), RESULTS: kv() };
pending = [];
calls = [];
globalThis.fetch = (async (input: string | URL | Request) => {
const path = String(input).replace(env.GOBARE_API, "");
calls.push(path);
return path in api ? Response.json(api[path]) : new Response("not found", { status: 404 });
}) as typeof fetch;
});
async function deliver(event: object, options: { timestamp?: string; signature?: string; body?: string } = {}) {
const body = options.body ?? JSON.stringify(event);
const timestamp = options.timestamp ?? String(Date.now());
const request = new Request("https://hooks.example/gobare", {
method: "POST",
body,
headers: { "x-gobare-timestamp": timestamp, "x-gobare-signature": options.signature ?? sign(timestamp, body) },
});
const response = await worker.fetch(request, env, { waitUntil: (p) => { pending.push(p); } });
await Promise.all(pending);
return response.status;
}
test("a signed delivery is accepted and handled", async () => {
assert.equal(await deliver(fixtures.completed), 200);
assert.deepEqual(JSON.parse((await env.RESULTS.get(SESSION))!), { metadata: { order: "A-4471" }, status: "completed", error: null, reply: "shipped" });
});
test("a body changed after signing is refused", async () => {
const timestamp = String(Date.now());
const body = JSON.stringify(fixtures.completed);
assert.equal(await deliver(fixtures.completed, { timestamp, signature: sign(timestamp, body), body: body.replace(TURN, "turn_someone_else") }), 401);
});
test("a re-serialised body is refused, which is why you verify the raw bytes", async () => {
const timestamp = String(Date.now());
const raw = JSON.stringify(fixtures.completed);
assert.equal(await deliver(fixtures.completed, { timestamp, signature: sign(timestamp, raw), body: JSON.stringify(JSON.parse(raw), null, 2) }), 401);
});
test("a replayed delivery from ten minutes ago is refused", async () => {
assert.equal(await deliver(fixtures.completed, { timestamp: String(Date.now() - 10 * 60_000) }), 400);
});
test("a timestamp converted to seconds is refused, not accepted by accident", async () => {
assert.equal(await deliver(fixtures.completed, { timestamp: String(Math.floor(Date.now() / 1000)) }), 400);
});
test("the same delivery twice does the work once", async () => {
assert.equal(await deliver(fixtures.completed), 200);
assert.equal(await deliver(fixtures.completed), 200);
assert.equal(calls.filter((c) => c.endsWith(`/turns/${TURN}`)).length, 1);
});
test("an event the handler does not act on is still answered 200, so it is not retried for hours", async () => {
assert.equal(await deliver(fixtures.actionRequired), 200);
assert.equal(env.RESULTS.size(), 0);
});
test("turn.failed arriving after turn.completed reads current state rather than trusting order", async () => {
await deliver(fixtures.completed);
await deliver(fixtures.failed);
assert.equal(JSON.parse((await env.RESULTS.get(SESSION))!).status, "completed");
});
test("a turn event with no turn_id is handled from the latest turn, not dropped", async () => {
assert.equal(await deliver({ ...fixtures.completed, data: { session_id: SESSION, turn_id: null } }), 200);
assert.equal(JSON.parse((await env.RESULTS.get(SESSION))!).status, "completed");
assert.ok(calls.includes(`/v1/sessions/${SESSION}/turns?limit=1`));
});