// 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" }); });});
// CRITICAL
import { 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" });
});
});