130 lines · 4.1 KB · TypeScript
RawDownload
// CRITICALimport { describe, expect, it } from "bun:test";import { Hono } from "hono";import type { AppContext } from "../types/context";import {  createMutatingAuthMiddleware,  createMutatingRateLimitMiddleware,  resetMutatingRateLimitStoreForTests,} from "./security-middleware"; /** * Build a minimal app context for middleware tests. * @param apiKey Optional API key to require auth. * @returns Mocked app context. */function createContext(apiKey?: string): AppContext {  return {    config: {      host: "0.0.0.0",      port: 8080,      inference_port: 8000,      data_dir: "/tmp",      db_path: "/tmp/controller.db",      models_dir: "/tmp/models",      strict_openai_models: false,      daytona_agent_mode: false,      agent_fs_local_fallback: false,      cors_origins: ["http://localhost:3000"],      ...(apiKey ? { api_key: apiKey } : {}),    },    logger: {      debug: () => undefined,      info: () => undefined,      warn: () => undefined,      error: () => undefined,    },  } as unknown as AppContext;} describe("security middleware", () => {  it("blocks read requests without API key when configured", async () => {    resetMutatingRateLimitStoreForTests();    const app = new Hono();    const context = createContext("secret-token");     app.use("*", createMutatingAuthMiddleware(context));    app.get("/secure", (ctx) => ctx.json({ ok: true }));     const response = await app.request("/secure", { method: "GET" });    expect(response.status).toBe(401);    expect(await response.json()).toEqual({ detail: "Unauthorized" });  });   it("accepts protected requests with bearer token or x-api-key", async () => {    resetMutatingRateLimitStoreForTests();    const app = new Hono();    const context = createContext("secret-token");     app.use("*", createMutatingAuthMiddleware(context));    app.get("/secure", (ctx) => ctx.json({ ok: true }));     const bearer = await app.request("/secure", {      method: "GET",      headers: { Authorization: "Bearer secret-token" },    });    expect(bearer.status).toBe(200);     const apiKeyHeader = await app.request("/secure", {      method: "GET",      headers: { "X-API-Key": "secret-token" },    });    expect(apiKeyHeader.status).toBe(200);  });   it("does not require auth when API key is not configured", async () => {    resetMutatingRateLimitStoreForTests();    const app = new Hono();    const context = createContext();     app.use("*", createMutatingAuthMiddleware(context));    app.post("/secure", (ctx) => ctx.json({ ok: true }));     const response = await app.request("/secure", { method: "POST" });    expect(response.status).toBe(200);  });   it("allows public health checks without auth", async () => {    resetMutatingRateLimitStoreForTests();    const app = new Hono();    const context = createContext("secret-token");     app.use("*", createMutatingAuthMiddleware(context));    app.get("/health", (ctx) => ctx.json({ ok: true }));     const response = await app.request("/health", { method: "GET" });    expect(response.status).toBe(200);  });   it("rate limits mutating requests only", async () => {    resetMutatingRateLimitStoreForTests();    const app = new Hono();    const context = createContext();     app.use("*", createMutatingRateLimitMiddleware(context, { maxRequests: 2, windowMs: 60_000 }));    app.get("/secure", (ctx) => ctx.json({ ok: true }));    app.post("/secure", (ctx) => ctx.json({ ok: true }));     const getResponse = await app.request("/secure", { method: "GET" });    expect(getResponse.status).toBe(200);     const first = await app.request("/secure", {      method: "POST",      headers: { "X-Forwarded-For": "203.0.113.7" },    });    expect(first.status).toBe(200);     const second = await app.request("/secure", {      method: "POST",      headers: { "X-Forwarded-For": "203.0.113.7" },    });    expect(second.status).toBe(200);     const third = await app.request("/secure", {      method: "POST",      headers: { "X-Forwarded-For": "203.0.113.7" },    });    expect(third.status).toBe(429);    expect(await third.json()).toEqual({ detail: "Rate limit exceeded" });  });});