aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/middleware/cors.test.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-30 22:08:47 +0900
committernsfisis <nsfisis@gmail.com>2025-12-30 22:08:47 +0900
commitc2eb7513834eeb5adfa53fff897f585de87e4821 (patch)
tree9e914051ca67e2f9e1fa301119bdec398ec9e55f /src/server/middleware/cors.test.ts
parentb839cae49efd4b9d35c2868a4137101a4d71bd7f (diff)
downloadkioku-c2eb7513834eeb5adfa53fff897f585de87e4821.tar.gz
kioku-c2eb7513834eeb5adfa53fff897f585de87e4821.tar.zst
kioku-c2eb7513834eeb5adfa53fff897f585de87e4821.zip
feat(security): add rate limiting and CORS middleware
- Add rate limiting to login endpoint (5 requests/minute per IP) - Configure CORS middleware with environment-based origin control - Expose rate limit headers in CORS for client visibility - Update hono to 4.11.3 for rate limiter peer dependency 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server/middleware/cors.test.ts')
-rw-r--r--src/server/middleware/cors.test.ts169
1 files changed, 169 insertions, 0 deletions
diff --git a/src/server/middleware/cors.test.ts b/src/server/middleware/cors.test.ts
new file mode 100644
index 0000000..6f413a5
--- /dev/null
+++ b/src/server/middleware/cors.test.ts
@@ -0,0 +1,169 @@
+import { Hono } from "hono";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { createCorsMiddleware } from "./cors.js";
+
+describe("createCorsMiddleware", () => {
+ const originalEnv = process.env.CORS_ORIGIN;
+
+ afterEach(() => {
+ if (originalEnv === undefined) {
+ delete process.env.CORS_ORIGIN;
+ } else {
+ process.env.CORS_ORIGIN = originalEnv;
+ }
+ });
+
+ describe("when CORS_ORIGIN is not set", () => {
+ beforeEach(() => {
+ delete process.env.CORS_ORIGIN;
+ });
+
+ it("does not add CORS headers", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://attacker.com" },
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
+ });
+
+ it("does not allow preflight requests", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ method: "OPTIONS",
+ headers: {
+ Origin: "https://attacker.com",
+ "Access-Control-Request-Method": "POST",
+ },
+ });
+
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
+ });
+ });
+
+ describe("when CORS_ORIGIN is set to single origin", () => {
+ beforeEach(() => {
+ process.env.CORS_ORIGIN = "https://allowed.example.com";
+ });
+
+ it("allows requests from the configured origin", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://allowed.example.com" },
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBe(
+ "https://allowed.example.com",
+ );
+ });
+
+ it("does not allow requests from other origins", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://attacker.com" },
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
+ });
+
+ it("handles preflight requests correctly", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.post("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ method: "OPTIONS",
+ headers: {
+ Origin: "https://allowed.example.com",
+ "Access-Control-Request-Method": "POST",
+ },
+ });
+
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBe(
+ "https://allowed.example.com",
+ );
+ expect(res.headers.get("Access-Control-Allow-Methods")).toContain("POST");
+ });
+
+ it("exposes rate limit headers", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ method: "OPTIONS",
+ headers: {
+ Origin: "https://allowed.example.com",
+ "Access-Control-Request-Method": "GET",
+ },
+ });
+
+ const exposeHeaders = res.headers.get("Access-Control-Expose-Headers");
+ expect(exposeHeaders).toContain("RateLimit-Limit");
+ expect(exposeHeaders).toContain("RateLimit-Remaining");
+ expect(exposeHeaders).toContain("RateLimit-Reset");
+ });
+ });
+
+ describe("when CORS_ORIGIN is set to multiple origins", () => {
+ beforeEach(() => {
+ process.env.CORS_ORIGIN =
+ "https://app.example.com, https://admin.example.com";
+ });
+
+ it("allows requests from first configured origin", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://app.example.com" },
+ });
+
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBe(
+ "https://app.example.com",
+ );
+ });
+
+ it("allows requests from second configured origin", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://admin.example.com" },
+ });
+
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBe(
+ "https://admin.example.com",
+ );
+ });
+
+ it("does not allow requests from unlisted origins", async () => {
+ const app = new Hono();
+ app.use("*", createCorsMiddleware());
+ app.get("/test", (c) => c.json({ ok: true }));
+
+ const res = await app.request("/test", {
+ headers: { Origin: "https://other.example.com" },
+ });
+
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
+ });
+ });
+});