Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions test/content-gated-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* /api/content — the commercial/academic (content-pipeline) branch. The confidential branch is
* covered in confidential-paths.test.ts; this file pins the second branch of the same route, which
* serves gated bodies from the server-only content-bodies module: the entitlement gate, the org
* scope, the embargo, the disclosure acknowledgement, and the fail-closed access log.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";

const h = vi.hoisted(() => ({
session: {} as Record<string, unknown>,
logOk: true,
logged: [] as unknown[],
}));

vi.mock("@/lib/auth/request-session", () => ({
resolveRequestSession: async () => h.session,
}));
vi.mock("@/lib/content/confidential-store", () => ({
CONFIDENTIAL_DOCS: {},
CONFIDENTIAL_DISCLOSURES: {},
authorizeConfidentialRead: () => false,
}));
vi.mock("@/lib/content/access-log", () => ({
recordAccess: (e: unknown) => {
if (!h.logOk) return false;
h.logged.push(e);
return true;
},
}));
vi.mock("@/content/_generated/content", () => {
const base = { sourceKind: "authored", source: "test", toc: [], body: null };
return {
CONTENT_NAV: [],
CONTENT_DOCS: {
"/g/commercial": { ...base, slug: "/g/commercial", title: "Commercial", tier: "commercial", orgId: null },
"/g/org": { ...base, slug: "/g/org", title: "Org", tier: "commercial", orgId: "org-a" },
"/g/embargo": { ...base, slug: "/g/embargo", title: "Embargo", tier: "commercial", orgId: null, embargoUntil: 9_999_999_999_999 },
"/g/nda": { ...base, slug: "/g/nda", title: "NDA", tier: "commercial", orgId: null, disclosureRequired: true, disclosureId: "nda-x" },
"/g/public": { ...base, slug: "/g/public", title: "Public", tier: "public", orgId: null, body: "PUBLIC" },
},
};
});
vi.mock("@/content/_generated/content-bodies", () => ({
CONTENT_BODIES: {
"/g/commercial": "COMMERCIAL-BODY",
"/g/org": "ORG-BODY",
"/g/embargo": "EMBARGO-BODY",
"/g/nda": "NDA-BODY",
},
}));

const commercial = (orgId: string | null = null) => ({
required: true,
authenticated: true,
sub: "u:reader",
kycStatus: "verified",
entitlement: { tier: "commercial", orgId, expiresAt: null },
});

async function get(path: string, headers: Record<string, string> = {}) {
const { GET } = await import("@/app/api/content/[...slug]/route");
const res = await GET(new Request(`http://x/api/content${path}`, { headers }), {
params: Promise.resolve({ slug: path.slice(1).split("/") }),
});
const text = await res.text();
return { status: res.status, text };
}

beforeEach(() => {
h.session = commercial();
h.logOk = true;
h.logged.length = 0;
});

describe("/api/content gated content-pipeline branch", () => {
it("serves a commercial body to a commercial reader and logs the read", async () => {
const r = await get("/g/commercial");
expect(r.status).toBe(200);
expect(r.text).toContain("COMMERCIAL-BODY");
expect(h.logged).toHaveLength(1);
});

it("an anonymous or public-tier caller gets 404 and no body", async () => {
h.session = { required: true, authenticated: false, kycStatus: "none" };
const r = await get("/g/commercial");
expect(r.status).toBe(404);
expect(r.text).not.toContain("COMMERCIAL-BODY");
expect(h.logged).toHaveLength(0);
});

it("an expired entitlement collapses to public and is refused", async () => {
h.session = { ...commercial(), entitlement: { tier: "commercial", orgId: null, expiresAt: 1 } };
const r = await get("/g/commercial");
expect(r.status).toBe(404);
expect(r.text).not.toContain("COMMERCIAL-BODY");
});

it("an org-scoped body is refused to another org's reader", async () => {
h.session = commercial("org-b");
const other = await get("/g/org");
expect(other.status).toBe(404);
expect(other.text).not.toContain("ORG-BODY");
h.session = commercial("org-a");
const own = await get("/g/org");
expect(own.status).toBe(200);
expect(own.text).toContain("ORG-BODY");
});

it("an embargoed body is withheld (423) even from an entitled reader", async () => {
const r = await get("/g/embargo");
expect(r.status).toBe(423);
expect(r.text).not.toContain("EMBARGO-BODY");
});

it("a disclosure-gated body needs the matching acknowledgement", async () => {
const none = await get("/g/nda");
expect(none.status).toBe(412);
expect(none.text).not.toContain("NDA-BODY");
const wrong = await get("/g/nda", { "x-codex-ack": "other" });
expect(wrong.status).toBe(412);
const ok = await get("/g/nda", { "x-codex-ack": "nda-x" });
expect(ok.status).toBe(200);
expect(ok.text).toContain("NDA-BODY");
});

it("a read that cannot be access-logged is not served (503, no body)", async () => {
h.logOk = false;
const r = await get("/g/commercial");
expect(r.status).toBe(503);
expect(r.text).not.toContain("COMMERCIAL-BODY");
});

it("public docs are not served by this route", async () => {
const r = await get("/g/public");
expect(r.status).toBe(404);
});
});
54 changes: 54 additions & 0 deletions test/route-limits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Every unauthenticated route that fans out to a paid or shared backend applies the limiter at the
* route itself. rate-limit.test.ts pins sandbox/infer; this file pins the chat and sandbox/relay
* routes. Memory and the model provider are mocked so no test request leaves the process.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { resetRateLimits } from "@/lib/security/rate-limit";

vi.mock("@/lib/ai/memory", () => ({ searchMemory: async () => [] }));
vi.mock("@/lib/ai/provider", () => ({ answer: async () => ({ text: "ok", sources: [] }) }));

beforeEach(() => {
resetRateLimits();
delete process.env.CITRATE_RELAY_URL; // relay route fails closed (503) before any fetch
delete process.env.UPSTASH_REDIS_REST_URL;
delete process.env.UPSTASH_REDIS_REST_TOKEN;
});

function req(path: string, ip: string): Request {
return new Request(`http://localhost${path}`, {
method: "POST",
headers: { "x-forwarded-for": ip, "content-type": "application/json" },
body: JSON.stringify({ query: "what is citrate" }),
});
}

async function statuses(post: (r: Request) => Promise<Response>, path: string, ip: string, n: number) {
const out: number[] = [];
for (let i = 0; i < n; i++) out.push((await post(req(path, ip))).status);
return out;
}

describe("route-level limiter", () => {
it("/api/chat returns 429 after 20 requests from one IP", async () => {
const { POST } = await import("@/app/api/chat/route");
const s = await statuses(POST, "/api/chat", "7.7.7.7", 21);
expect(s.slice(0, 20).every((x) => x !== 429)).toBe(true);
expect(s[20]).toBe(429);
});

it("/api/sandbox/relay returns 429 after 10 requests from one IP", async () => {
const { POST } = await import("@/app/api/sandbox/relay/route");
const s = await statuses(POST, "/api/sandbox/relay", "8.8.4.4", 11);
expect(s.slice(0, 10).every((x) => x === 503)).toBe(true); // fail-closed: no relay configured
expect(s[10]).toBe(429);
});

it("the relay limit is per IP: another caller is not throttled by the first", async () => {
const { POST } = await import("@/app/api/sandbox/relay/route");
await statuses(POST, "/api/sandbox/relay", "8.8.4.4", 11);
const other = await POST(req("/api/sandbox/relay", "1.0.0.1"));
expect(other.status).toBe(503);
});
});
143 changes: 143 additions & 0 deletions test/tier-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Fail-closed defaults on the tier/entitlement path, and the client/server content split:
* - a partial MEMORY_REPO_TIERS override never drops a declared tier, even when an operator sets
* MEM_DEFAULT_TIER=public (where a replace-instead-of-merge bug becomes visible);
* - an unlisted repo with no MEM_DEFAULT_TIER is confidential;
* - a principal lookup does not hand out a gated tier to an unverified KYC status;
* - org-scoped content is readable only by that org (or an admin);
* - the generated client module carries no gated body, and the body module is server-only;
* - mermaid renders with securityLevel "strict" (its SVG is inserted via innerHTML).
*/
import { describe, it, expect, afterEach } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { resolveRepoTiers, repoTierFrom, DEFAULT_REPO_TIERS } from "@/lib/ai/memory";
import { resolveEntitlement } from "@/lib/auth/entitlement";
import { canRead } from "@/prototype/fixtures";
import { CONTENT_DOCS } from "@/content/_generated/content";

const ROOT = path.resolve(__dirname, "..");
const saved = process.env.MEM_DEFAULT_TIER;
afterEach(() => {
if (saved === undefined) delete process.env.MEM_DEFAULT_TIER;
else process.env.MEM_DEFAULT_TIER = saved;
});

describe("memory repo tiers", () => {
it("a partial override keeps every declared tier when MEM_DEFAULT_TIER=public", () => {
process.env.MEM_DEFAULT_TIER = "public";
const map = resolveRepoTiers('{"citrate-docs":"public"}');
const declared = Object.entries(DEFAULT_REPO_TIERS);
expect(declared.length).toBeGreaterThan(0);
for (const [repo, tier] of declared) expect(repoTierFrom(map, repo)).toBe(tier);
expect(repoTierFrom(map, "citrate-docs")).toBe("public");
});

it("an unlisted repo with no MEM_DEFAULT_TIER is confidential", () => {
delete process.env.MEM_DEFAULT_TIER;
expect(repoTierFrom(resolveRepoTiers(undefined), "some-unlisted-repo")).toBe("confidential");
});
});

describe("principal lookup and KYC", () => {
it("a gated principal with a non-verified KYC status resolves to public", () => {
for (const kycStatus of ["pending", "rejected", "none"]) {
const e = resolveEntitlement({}, { sub: "uuid:7f3c1a90-builder", kycStatus });
expect(e?.tier).toBe("public");
}
});

it("the same principal with verified KYC keeps its tier (control)", () => {
const e = resolveEntitlement({}, { sub: "uuid:7f3c1a90-builder", kycStatus: "verified" });
expect(e?.tier).toBe("commercial");
});
});

describe("org scope", () => {
const reader = (orgId: string | null) =>
({ required: true, authenticated: true, sub: "u", kycStatus: "verified", entitlement: { tier: "commercial", orgId, expiresAt: null } }) as never;
const node = { tier: "commercial" as const, orgId: "org-a" };

it("org-scoped content is refused to another org and to an org-less reader", () => {
expect(canRead(reader("org-b"), node, Date.now())).toBe(false);
expect(canRead(reader(null), node, Date.now())).toBe(false);
});

it("the owning org reads it (control)", () => {
expect(canRead(reader("org-a"), node, Date.now())).toBe(true);
});
});

/**
* Run the real generator against a throwaway fixture corpus (the committed corpus is all public
* today, so it cannot exercise the split). The script resolves ROOT from its own location, so a
* copy under a temp dir reads and writes only inside that dir.
*/
function runGeneratorOnFixture(): { client: string; bodies: string } {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-gen-"));
try {
fs.mkdirSync(path.join(tmp, "scripts"), { recursive: true });
fs.copyFileSync(path.join(ROOT, "scripts/build-content.mjs"), path.join(tmp, "scripts/build-content.mjs"));
const dir = path.join(tmp, "content", "guides");
fs.mkdirSync(dir, { recursive: true });
const page = (tier: string, marker: string) => `---\ntitle: ${tier} page\ntier: ${tier}\n---\n# ${tier}\n\n${marker}\n`;
fs.writeFileSync(path.join(dir, "pub.md"), page("public", "PUBLIC-MARKER-1"));
fs.writeFileSync(path.join(dir, "com.md"), page("commercial", "COMMERCIAL-MARKER-2"));
fs.writeFileSync(path.join(dir, "aca.md"), page("academic", "ACADEMIC-MARKER-3"));
execFileSync(process.execPath, [path.join(tmp, "scripts/build-content.mjs")], { cwd: tmp, stdio: "pipe" });
const gen = path.join(tmp, "content", "_generated");
return {
client: fs.readFileSync(path.join(gen, "content.ts"), "utf8"),
bodies: fs.readFileSync(path.join(gen, "content-bodies.ts"), "utf8"),
};
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}

describe("generated content split", () => {
it("the generator keeps gated bodies out of the client module and puts them in the server-only one", () => {
const { client, bodies } = runGeneratorOnFixture();
expect(client).toContain("PUBLIC-MARKER-1"); // control: public bodies stay client-side
expect(client).not.toContain("COMMERCIAL-MARKER-2");
expect(client).not.toContain("ACADEMIC-MARKER-3");
expect(bodies).toMatch(/^import "server-only";$/m);
expect(bodies).toContain("COMMERCIAL-MARKER-2");
expect(bodies).toContain("ACADEMIC-MARKER-3");
expect(bodies).not.toContain("PUBLIC-MARKER-1");
});

it("the committed client module carries no body for any gated doc", () => {
for (const d of Object.values(CONTENT_DOCS)) if (d.tier !== "public") expect(d.body ?? null).toBeNull();
});

it("the body module is server-only and no client component imports it", () => {
const bodies = fs.readFileSync(path.join(ROOT, "content/_generated/content-bodies.ts"), "utf8");
expect(bodies).toMatch(/^import "server-only";$/m);
const offenders: string[] = [];
const walk = (dir: string) => {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walk(p);
else if (/\.(tsx?|jsx?)$/.test(ent.name)) {
const src = fs.readFileSync(p, "utf8");
if (/^\s*["']use client["']/.test(src) && src.includes("content/_generated/content-bodies")) offenders.push(p);
}
}
};
for (const d of ["app", "components", "lib"]) walk(path.join(ROOT, d));
expect(offenders).toEqual([]);
});
});

describe("mermaid rendering", () => {
it("initialises with securityLevel strict", () => {
const md = fs.readFileSync(path.join(ROOT, "lib/md.tsx"), "utf8");
const levels = [...md.matchAll(/securityLevel:\s*"([a-z]+)"/g)].map((m) => m[1]);
expect(levels.length).toBeGreaterThan(0);
expect(levels.every((l) => l === "strict")).toBe(true);
});
});
Loading