What ships (verifiable without live DB, 64 new tests): - db/migrations/0000_initial_schema.sql (Drizzle-generated, 7 tables) + .down.sql + registry entry - db/migrations/rehearse.ts: forward-then-rollback round-trip with row-count hash check (DoD 2.2) - infra/docker-compose.yml: postgres 17 + redis 7 + openobserve for local dev (5433/6380/5080) - packages/schema/src/rate-limit.ts: pluggable store; 4 tests including 21st-of-20 reject (DoD 2.4) - packages/schema/src/csrf.ts: HMAC double-submit token; 8 tests covering forgery + tamper + malformed - packages/schema/src/authz.ts: 3-role Cerbos-equivalent rules (operator/approver/viewer); 6 tests - packages/schema/src/api-contracts.ts: Zod schemas for /api/content, /api/approvals, /api/publications, /api/feature-flags + idempotencyKeyOf; 11 tests What defers to live-DB session: - 2.3 admin route handlers integration tests (401/403/200/422 contract suite) - 2.2 actual rehearsal execution against staging DB Total: 79/79 tests pass across 9 files in 4 packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { issueCsrf, verifyCsrf } from "./csrf";
|
|
|
|
const SECRET = "test-secret-32-bytes-or-more-please-make-it-long";
|
|
|
|
describe("CSRF double-submit token", () => {
|
|
it("issues a token where cookie carries raw + sig and header carries raw", () => {
|
|
const t = issueCsrf(SECRET);
|
|
expect(t.cookieValue.split(".").length).toBe(2);
|
|
expect(t.cookieValue.startsWith(t.headerValue)).toBe(true);
|
|
});
|
|
|
|
it("verifies a freshly issued token", () => {
|
|
const t = issueCsrf(SECRET);
|
|
expect(verifyCsrf(t.cookieValue, t.headerValue, SECRET)).toBe(true);
|
|
});
|
|
|
|
it("rejects mismatched header", () => {
|
|
const t = issueCsrf(SECRET);
|
|
expect(verifyCsrf(t.cookieValue, "different-header", SECRET)).toBe(false);
|
|
});
|
|
|
|
it("rejects tampered signature", () => {
|
|
const t = issueCsrf(SECRET);
|
|
const tampered = t.cookieValue.replace(/.$/, (c) => (c === "0" ? "1" : "0"));
|
|
expect(verifyCsrf(tampered, t.headerValue, SECRET)).toBe(false);
|
|
});
|
|
|
|
it("rejects different secret (forgery)", () => {
|
|
const t = issueCsrf(SECRET);
|
|
expect(verifyCsrf(t.cookieValue, t.headerValue, "wrong-secret")).toBe(false);
|
|
});
|
|
|
|
it("rejects null/empty inputs", () => {
|
|
expect(verifyCsrf(null, "x", SECRET)).toBe(false);
|
|
expect(verifyCsrf("x.y", null, SECRET)).toBe(false);
|
|
expect(verifyCsrf("x.y", "x", "")).toBe(false);
|
|
});
|
|
|
|
it("rejects malformed cookie (no dot)", () => {
|
|
expect(verifyCsrf("nodothere", "nodothere", SECRET)).toBe(false);
|
|
});
|
|
|
|
it("rejects empty secret on issue", () => {
|
|
expect(() => issueCsrf("")).toThrow();
|
|
});
|
|
});
|