chore: sync in-progress work across marketing, admin, API and tests
Snapshot of uncommitted work that had accumulated in the tree alongside the Turnstile changes: - marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and structured data - admin billing actions and a per-user portfolio view, plus an admin error boundary - rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface - CSP and proxy adjustments, accounting/webhook lib updates - Playwright config and an e2e/unit test suite - next bumped to ^16.3.4 with the lockfile regenerated - generated AGENTS.md / CLAUDE.md Authored by other sessions working in this tree; committed here so the Turnstile work could be pushed without leaving the tree dirty. Typecheck passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8f90347659
commit
1d02598786
@@ -0,0 +1,84 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
import { hit, clientIp } from "@/lib/rate-limit"
|
||||
|
||||
// The limiter is module-level state shared across the process, so every test
|
||||
// uses a unique key prefix to stay independent of the others.
|
||||
let n = 0
|
||||
const key = (label: string) => `test:${label}:${++n}:${Math.random()}`
|
||||
|
||||
test.describe("hit()", () => {
|
||||
test("allows requests up to the limit and rejects the one after", () => {
|
||||
const k = key("basic")
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expect(hit(k, 5, 60).ok, `request ${i + 1} of 5 should be allowed`).toBe(true)
|
||||
}
|
||||
expect(hit(k, 5, 60).ok, "the 6th request should be rejected").toBe(false)
|
||||
})
|
||||
|
||||
test("reports remaining budget accurately", () => {
|
||||
const k = key("remaining")
|
||||
expect(hit(k, 3, 60).remaining).toBe(2)
|
||||
expect(hit(k, 3, 60).remaining).toBe(1)
|
||||
expect(hit(k, 3, 60).remaining).toBe(0)
|
||||
// Over the limit, remaining stays clamped at zero rather than going negative.
|
||||
expect(hit(k, 3, 60).remaining).toBe(0)
|
||||
})
|
||||
|
||||
test("keeps separate keys independent", () => {
|
||||
const a = key("iso-a")
|
||||
const b = key("iso-b")
|
||||
for (let i = 0; i < 3; i++) hit(a, 3, 60)
|
||||
expect(hit(a, 3, 60).ok, "key A is exhausted").toBe(false)
|
||||
expect(hit(b, 3, 60).ok, "key B is untouched").toBe(true)
|
||||
})
|
||||
|
||||
test("resets after the window elapses", async () => {
|
||||
const k = key("window")
|
||||
// A 1-second window so the test can actually wait it out.
|
||||
expect(hit(k, 1, 1).ok).toBe(true)
|
||||
expect(hit(k, 1, 1).ok).toBe(false)
|
||||
await new Promise((r) => setTimeout(r, 1100))
|
||||
expect(hit(k, 1, 1).ok, "a fresh window should allow requests again").toBe(true)
|
||||
})
|
||||
|
||||
test("returns a retryAfter of at least one second while limited", () => {
|
||||
const k = key("retry")
|
||||
hit(k, 1, 60)
|
||||
const limited = hit(k, 1, 60)
|
||||
expect(limited.ok).toBe(false)
|
||||
expect(limited.retryAfter).toBeGreaterThan(0)
|
||||
expect(limited.retryAfter).toBeLessThanOrEqual(60)
|
||||
})
|
||||
|
||||
test("reports the configured limit back to the caller", () => {
|
||||
expect(hit(key("limit"), 42, 60).limit).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("clientIp()", () => {
|
||||
const req = (headers: Record<string, string>) => new Request("https://x.test", { headers })
|
||||
|
||||
test("takes the first entry of x-forwarded-for", () => {
|
||||
expect(clientIp(req({ "x-forwarded-for": "1.2.3.4, 5.6.7.8, 9.9.9.9" }))).toBe("1.2.3.4")
|
||||
})
|
||||
|
||||
test("trims whitespace around the client address", () => {
|
||||
expect(clientIp(req({ "x-forwarded-for": " 1.2.3.4 , 5.6.7.8" }))).toBe("1.2.3.4")
|
||||
})
|
||||
|
||||
test("falls back to x-real-ip when x-forwarded-for is absent", () => {
|
||||
expect(clientIp(req({ "x-real-ip": "8.8.8.8" }))).toBe("8.8.8.8")
|
||||
})
|
||||
|
||||
test("degrades to a single shared bucket rather than no limit at all", () => {
|
||||
// The important property: a request with no proxy headers must still land in
|
||||
// SOME bucket. Returning a unique value per request would silently disable
|
||||
// the limit for anyone who can strip headers.
|
||||
expect(clientIp(req({}))).toBe("unknown")
|
||||
expect(clientIp(req({}))).toBe("unknown")
|
||||
})
|
||||
|
||||
test("ignores an empty x-forwarded-for and falls through", () => {
|
||||
expect(clientIp(req({ "x-forwarded-for": "", "x-real-ip": "8.8.4.4" }))).toBe("8.8.4.4")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user