Files
fristydev/proxy.test.ts
T
fristy 864b9226fc
Build & Push / Lint & Build (pull_request) Failing after 52s
Build & Push / Build & Push Image (pull_request) Skipped
Build & Push / Trigger Deploy (pull_request) Skipped
desloppify: code health overhaul plus abuse hardening (140 green, strict 87.2)
Honest baseline after removing 45 gaming suppressions. Structural: lib/game plus root hooks extraction, CORS origin echo, DEV_ORIGINS layering. Fixes: error-cause binding, discord 5xx split, null-cache guard, co-located tests, rejecting-fetch coverage, naming and typing cleanup, socket controller, cache bound, cascade helpers, latch resets. Hardening: per-IP rate limit on API routes, deterministic-miss UX, retry cooldown plus cap. Suite 140 green, build plus lint clean.
2026-09-11 23:54:59 +03:00

99 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { NextRequest } from "next/server";
import { _resetRateLimit, proxy } from "@/proxy";
afterEach(() => {
_resetRateLimit();
});
function apiRequest(path: string, init?: { method?: string; origin?: string | null }): NextRequest {
const headers = new Headers();
if (init?.origin !== null) {
headers.set("origin", init?.origin ?? "https://fristy.net");
}
return new NextRequest(`https://fristy.net${path}`, {
method: init?.method ?? "GET",
headers,
});
}
describe("proxy /api/*", () => {
it("answers OPTIONS preflight with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1", { method: "OPTIONS" }));
expect(response.status).toBe(204);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net");
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS");
});
it("rejects foreign origins", () => {
const response = proxy(apiRequest("/api/game/1", { origin: "https://evil.test" }));
expect(response.status).toBe(403);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net");
});
it("passes same-origin API requests with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1"));
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net");
});
});
describe("proxy rate limit", () => {
function ipRequest(ip: string): NextRequest {
const request = apiRequest("/api/game/1");
request.headers.set("cf-connecting-ip", ip);
return request;
}
it("allows requests under the per-IP limit", () => {
for (let i = 0; i < 30; i += 1) {
expect(proxy(ipRequest("10.0.0.1")).status).not.toBe(429);
}
});
it("returns 429 with Retry-After past the limit", async () => {
for (let i = 0; i < 30; i += 1) {
proxy(ipRequest("10.0.0.2"));
}
const limited = proxy(ipRequest("10.0.0.2"));
expect(limited.status).toBe(429);
expect(limited.headers.get("Retry-After")).toBe("60");
expect(limited.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net");
await expect(limited.json()).resolves.toEqual({ error: "Too many requests" });
});
it("isolates buckets per client IP", () => {
for (let i = 0; i < 30; i += 1) {
proxy(ipRequest("10.0.0.3"));
}
expect(proxy(ipRequest("10.0.0.3")).status).toBe(429);
expect(proxy(ipRequest("10.0.0.4")).status).not.toBe(429);
});
it("prefers cf-connecting-ip over x-forwarded-for", () => {
for (let i = 0; i < 30; i += 1) {
const request = apiRequest("/api/game/1");
request.headers.set("cf-connecting-ip", "10.0.0.5");
request.headers.set("x-forwarded-for", "10.0.0.6");
proxy(request);
}
expect(proxy(ipRequest("10.0.0.5")).status).toBe(429);
expect(proxy(ipRequest("10.0.0.6")).status).not.toBe(429);
});
});
describe("proxy locale", () => {
it("sets the locale cookie from accept-language", () => {
const request = apiRequest("/");
request.headers.set("accept-language", "de-DE,de;q=0.9");
const response = proxy(request);
expect(response.cookies.get("locale")?.value).toBe("de");
});
it("passes through when the cookie already exists", () => {
const request = apiRequest("/");
request.cookies.set("locale", "en");
const response = proxy(request);
expect(response.headers.get("set-cookie")).toBeNull();
});
});