99 lines
3.7 KiB
TypeScript
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.dev");
|
|
}
|
|
return new NextRequest(`https://fristy.dev${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.dev");
|
|
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.dev");
|
|
});
|
|
|
|
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.dev");
|
|
});
|
|
});
|
|
|
|
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.dev");
|
|
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();
|
|
});
|
|
});
|