import { test, expect } from "bun:test"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { existsSync } from "node:fs"; import { createContext, rateLimit, requestLogger, TTLCache, cacheControl, withCacheControl, etag, notModified, saveUpload, collectUploads, sanitizeFilename, UploadError, setSessionBackend, loadSession, type SessionBackend, type SessionEntry, type AsyncSessionBackend, type RateLimitStore, } from "../src/index.ts"; function memoryBackend(): SessionBackend { const map = new Map(); return { get: (id) => map.get(id), set: (id, e) => void map.set(id, e), delete: (id) => void map.delete(id), _map: map, } as SessionBackend & { _map: Map }; } function ctx(path = "/", headers: Record = {}) { const url = new URL(`http://x${path}`); return createContext(new Request(url, { headers }), url); } // --- rate limiting --------------------------------------------------------- test("rateLimit (trustProxy) allows up to max then 429 with headers", async () => { const mw = rateLimit({ max: 2, windowMs: 60_000, trustProxy: true }); const ok = () => new Response("ok"); const key = { "x-forwarded-for": "1.1.1.1" }; const r1 = await mw(ctx("/", key), ok); const r2 = await mw(ctx("/", key), ok); const r3 = await mw(ctx("/", key), ok); expect(r1.status).toBe(200); expect(r1.headers.get("RateLimit-Remaining")).toBe("1"); expect(r2.status).toBe(200); expect(r2.headers.get("RateLimit-Remaining")).toBe("0"); expect(r3.status).toBe(429); expect(r3.headers.get("retry-after")).toBeTruthy(); }); test("rateLimit (trustProxy) buckets are independent per key", async () => { const mw = rateLimit({ max: 1, windowMs: 60_000, trustProxy: true }); const ok = () => new Response("ok"); const a = await mw(ctx("/", { "x-forwarded-for": "2.2.2.2" }), ok); const b = await mw(ctx("/", { "x-forwarded-for": "3.3.3.3" }), ok); expect(a.status).toBe(200); expect(b.status).toBe(200); }); test("setSessionBackend routes session data through a custom backend", () => { const backend = memoryBackend() as SessionBackend & { _map: Map }; setSessionBackend(backend); try { const c = ctx("/"); c.session.set("k", "v"); const id = c.session.id(); expect(backend._map.has(id)).toBe(true); expect(backend._map.get(id)!.data).toEqual({ k: "v" }); } finally { setSessionBackend(memoryBackend()); // restore an equivalent for other tests } }); test("rateLimit accepts a custom (shared) store", async () => { const hits: string[] = []; const store: RateLimitStore = { hit(key, windowMs, now) { hits.push(key); return { count: hits.filter((k) => k === key).length, resetAt: now + windowMs }; }, }; const mw = rateLimit({ max: 1, windowMs: 1000, store, trustProxy: true }); const ok = () => new Response("ok"); const key = { "x-forwarded-for": "5.5.5.5" }; expect((await mw(ctx("/", key), ok)).status).toBe(200); expect((await mw(ctx("/", key), ok)).status).toBe(429); expect(hits.length).toBe(2); // both requests went through the injected store }); test("rateLimit awaits an ASYNC store (e.g. Redis)", async () => { const counts = new Map(); const store: RateLimitStore = { async hit(key, windowMs, now) { const n = (counts.get(key) ?? 0) + 1; counts.set(key, n); return { count: n, resetAt: now + windowMs }; }, }; const mw = rateLimit({ max: 1, windowMs: 1000, store, trustProxy: true }); const ok = () => new Response("ok"); const key = { "x-forwarded-for": "7.7.7.7" }; expect((await mw(ctx("/", key), ok)).status).toBe(200); expect((await mw(ctx("/", key), ok)).status).toBe(429); }); test("loadSession persists a session through an ASYNC backend across requests", async () => { const kv = new Map(); const backend: AsyncSessionBackend = { load: async (id) => kv.get(id), save: async (id, entry) => void kv.set(id, entry), destroy: async (id) => void kv.delete(id), }; const mw = loadSession(backend); // Request 1: write a value, capture the issued session id. const c1 = ctx("/"); await mw(c1, () => { c1.session.set("hits", 1); return new Response("ok"); }); const sid = c1.cookies.get("wrnexus.sid")!; expect(sid).toBeTruthy(); expect(kv.has(sid)).toBe(true); // saved to the async backend // Request 2: same cookie → session loads from the backend. const c2 = ctx("/", { cookie: `wrnexus.sid=${sid}` }); let seen: unknown; await mw(c2, () => { seen = c2.session.get("hits"); return new Response("ok"); }); expect(seen).toBe(1); }); test("rateLimit default keys on the non-spoofable peer IP, not XFF headers", async () => { const mw = rateLimit({ max: 1, windowMs: 60_000 }); const ok = () => new Response("ok"); // Same peer IP, different spoofed XFF → still one bucket (XFF ignored). const c1 = ctx("/", { "x-forwarded-for": "9.9.9.9" }); c1.ip = "10.0.0.1"; const c2 = ctx("/", { "x-forwarded-for": "8.8.8.8" }); c2.ip = "10.0.0.1"; expect((await mw(c1, ok)).status).toBe(200); expect((await mw(c2, ok)).status).toBe(429); }); test("rateLimit validates its in-memory key bound", () => { expect(() => rateLimit({ maxKeys: 0 })).toThrow("maxKeys"); }); // --- request logging ------------------------------------------------------- test("requestLogger emits a structured record with duration and id", async () => { const records: string[] = []; let t = 1000; const mw = requestLogger({ format: "json", sink: (line) => records.push(line), now: () => (t += 5), }); const res = await mw(ctx("/api/users"), () => new Response("x", { status: 201 })); expect(res.status).toBe(201); const rec = JSON.parse(records[0]!); expect(rec.method).toBe("GET"); expect(rec.path).toBe("/api/users"); expect(rec.status).toBe(201); expect(rec.durationMs).toBeGreaterThanOrEqual(0); expect(rec.id).toBeTruthy(); }); test("requestLogger logs status 500 when the handler throws", async () => { const records: string[] = []; const mw = requestLogger({ format: "json", sink: (l) => records.push(l) }); await expect( mw(ctx("/boom"), () => { throw new Error("nope"); }), ).rejects.toThrow("nope"); expect(JSON.parse(records[0]!).status).toBe(500); }); // --- caching --------------------------------------------------------------- test("TTLCache getOrLoad caches until expiry", async () => { const cache = new TTLCache(60_000); let calls = 0; const load = () => { calls++; return 42; }; expect(await cache.getOrLoad("k", load)).toBe(42); expect(await cache.getOrLoad("k", load)).toBe(42); expect(calls).toBe(1); cache.delete("k"); expect(await cache.getOrLoad("k", load)).toBe(42); expect(calls).toBe(2); }); test("TTLCache coalesces concurrent loads for the same key", async () => { const cache = new TTLCache(); let calls = 0; const loader = async () => { calls++; await Promise.resolve(); return 7; }; expect(await Promise.all([cache.getOrLoad("x", loader), cache.getOrLoad("x", loader)])).toEqual([ 7, 7, ]); expect(calls).toBe(1); }); test("TTLCache does not let an old in-flight load overwrite set, delete, or clear", async () => { const cache = new TTLCache(); let release!: (value: number) => void; const loading = cache.getOrLoad("x", () => new Promise((resolve) => (release = resolve))); await Promise.resolve(); cache.set("x", 9); release(1); expect(await loading).toBe(1); expect(cache.get("x")).toBe(9); let releaseClear!: (value: number) => void; const clearing = cache.getOrLoad( "y", () => new Promise((resolve) => (releaseClear = resolve)), ); await Promise.resolve(); cache.clear(); releaseClear(2); await clearing; expect(cache.get("y")).toBeUndefined(); }); test("cacheControl builds directives; no-store wins", () => { expect(cacheControl({ maxAge: 60, sMaxAge: 120 })).toBe("public, max-age=60, s-maxage=120"); expect(cacheControl({ private: true, noCache: true })).toBe("private, no-cache"); expect(cacheControl({ noStore: true, maxAge: 99 })).toBe("no-store"); const res = withCacheControl(new Response("x"), { maxAge: 30, immutable: true }); expect(res.headers.get("Cache-Control")).toBe("public, max-age=30, immutable"); }); test("etag + notModified drive conditional requests", () => { const tag = etag("hello world"); expect(tag).toMatch(/^W\/"/); expect(etag("hello world")).toBe(tag); // stable expect(etag("different")).not.toBe(tag); const req = new Request("http://x", { headers: { "if-none-match": tag } }); expect(notModified(req, tag)).toBe(true); expect(notModified(new Request("http://x"), tag)).toBe(false); }); // --- uploads --------------------------------------------------------------- test("sanitizeFilename strips traversal and separators", () => { const s = sanitizeFilename("../../etc/passwd"); expect(s).not.toContain("/"); expect(s).not.toContain(".."); expect(s).toContain("passwd"); expect(sanitizeFilename("a/b\\c.png")).toBe("a_b_c.png"); expect(sanitizeFilename("")).toBe("upload"); }); test("saveUpload writes a validated file and enforces limits", async () => { const dir = join(tmpdir(), "wire-upload-test"); const file = new File(["hello upload"], "note.txt", { type: "text/plain" }); const saved = await saveUpload(file, { dir, allowedTypes: ["text/plain", ".txt"] }); expect(saved.filename).toBe("note.txt"); expect(saved.size).toBe(12); expect(existsSync(saved.path)).toBe(true); await expect(saveUpload(file, { dir, maxBytes: 4 })).rejects.toThrow(UploadError); await expect(saveUpload(file, { dir, allowedTypes: ["image/png"] })).rejects.toThrow(UploadError); }); test("collectUploads returns only non-empty File fields", async () => { const form = new FormData(); form.append("name", "ada"); form.append("avatar", new File(["img"], "a.png", { type: "image/png" })); const uploads = collectUploads(form); expect(uploads.length).toBe(1); expect(uploads[0]!.field).toBe("avatar"); });