返回 CodeWhale
public-api-security.test.ts
根目录 / web / lib / public-api-security.test.ts
1 import { readFileSync } from "node:fs";
2 import { describe, expect, it } from "vitest";
3
4 function routeSource(path: string): string {
5 return readFileSync(new URL(`../app/api/${path}/route.ts`, import.meta.url), "utf8");
6 }
7
8 function librarySource(path: string): string {
9 return readFileSync(new URL(path, import.meta.url), "utf8");
10 }
11
12 describe("public API security contracts", () => {
13 it("keeps the unauthenticated feed cached and detached from the server token", () => {
14 const source = routeSource("github/feed");
15 expect(source).toContain('export const dynamic = "force-static"');
16 expect(source).toContain("fetchFeed(undefined, 50)");
17 expect(source).not.toContain("GITHUB_TOKEN");
18 expect(source).not.toContain('dynamic = "force-dynamic"');
19 });
20
21 it("validates the draft namespace before admin discard can reach KV", () => {
22 const source = routeSource("admin/post");
23 expect(source).toContain("parseDraftKey(draftKey)");
24 expect(source.indexOf("parseDraftKey(draftKey)")).toBeLessThan(
25 source.indexOf("getDraft(env.CURATED_KV, draftKey)"),
26 );
27 expect(source.indexOf("getDraft(env.CURATED_KV, draftKey)")).toBeLessThan(
28 source.indexOf("deleteDraft(env.CURATED_KV, draftKey)"),
29 );
30 });
31
32 it("bounds the public login body before comparing the maintainer token", () => {
33 const source = routeSource("admin/login");
34 expect(source).toContain("readBoundedUrlEncodedForm(req, MAX_LOGIN_BODY_BYTES)");
35 expect(source).not.toContain("req.formData()");
36 expect(source.indexOf("readBoundedUrlEncodedForm(req, MAX_LOGIN_BODY_BYTES)")).toBeLessThan(
37 source.indexOf("safeEqual(submitted, env.MAINTAINER_TOKEN)"),
38 );
39 });
40
41 it("keeps paid review callers on the canonical persisted draft namespaces", () => {
42 const source = librarySource("./community-agent-tasks.ts");
43 expect(source).toContain('hasFreshDraft(env.CURATED_KV, "triage"');
44 expect(source).toContain('hasFreshDraft(env.CURATED_KV, "pr-review"');
45 expect(source).not.toContain('hasFreshDraft(env.CURATED_KV, "issue"');
46 expect(source).not.toContain('hasFreshDraft(env.CURATED_KV, "pr"');
47 });
48
49 it("digest post: never returns ok:true for a no-op (happy path returns real url/number)", () => {
50 const source = routeSource("admin/post");
51 // The old false-success sentinel must be gone
52 expect(source).not.toContain("digest-skipped");
53 expect(source).not.toContain("Digest pages are not posted as comments");
54 // Happy path must surface the real GitHub Issue outcome
55 expect(source).toContain("number: issue.number");
56 expect(source).toContain("url: issue.html_url");
57 // The draft must be marked posted and stored back on success
58 const digestBlock = source.slice(source.indexOf('draft.type === "digest"'));
59 expect(digestBlock.indexOf("draft.posted = true")).toBeLessThan(
60 digestBlock.indexOf('action: "posted"'),
61 );
62 });
63
64 it("digest post: GitHub API failure surfaces a 502 error, not ok:true", () => {
65 const source = routeSource("admin/post");
66 // On a failed digest GitHub call the handler must return a non-ok error payload
67 expect(source).toContain("digestRes.ok");
68 // Must propagate the GitHub status rather than swallowing it
69 const digestErrorPath = source.slice(
70 source.indexOf("digestRes.ok"),
71 source.indexOf("digestRes.ok") + 300,
72 );
73 expect(digestErrorPath).toContain("status: 502");
74 expect(digestErrorPath).not.toContain('ok: true');
75 });
76 });
77
77 lines TYPESCRIPT