Skip to content
File

Blob: test/cache-policy.worker.test.ts

typescript158 lines
1import { beforeAll, describe, expect, it } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3 
4import type { CommitFilePatchResult } from "@/shared/git/types";
5import { buildCacheKeyFrom, cacheGetJSON, cachePutJSON } from "@/worker/cache";
6import { newPrefixedId } from "@/worker/common";
7import { createDb } from "@/worker/db/d1/client";
8import { insertUserIfNew, claimNamespace, insertMembershipIfMissing } from "@/worker/db/d1/dal";
9 
10import { ensureD1Migrations } from "./util/d1Setup";
11import { mintSessionCookie, seedRepo } from "./util/repoSeed";
12import { seedPackFirstRepo } from "./util/pack-first";
13 
14beforeAll(async () => {
15 await ensureD1Migrations(env);
16});
17 
18async function makeMember(
19 namespaceSlug: string
20): Promise<{ userId: string; cookieHeader: string }> {
21 const db = createDb(env.DB);
22 const userId = newPrefixedId("user");
23 const namespaceId = newPrefixedId("ns");
24 const now = Date.now();
25 await insertUserIfNew(db, { id: userId, tesseraSub: `t-${userId}`, createdAt: now });
26 const claimed = await claimNamespace(db, {
27 id: namespaceId,
28 slug: namespaceSlug,
29 createdBy: userId,
30 createdAt: now,
31 });
32 if (!claimed) throw new Error(`namespace ${namespaceSlug} already exists`);
33 await insertMembershipIfMissing(db, {
34 namespaceId: claimed.id,
35 userId,
36 createdAt: now,
37 });
38 return { userId, cookieHeader: await mintSessionCookie(env, userId) };
39}
40 
41// The /commit/:oid/diff?path= endpoint stores a `CommitFilePatchResult`
42// under `/_cache/commit-patch` keyed by repo+oid+path+v. The fixture
43// `seedPackFirstRepo` creates a real README.md change, so the loader,
44// when invoked, returns `{ path, changeType: "M", patch: <text>, ... }`
45// with `skipped` undefined. Tests that prove cache bypass therefore poison
46// with a clearly-distinct sentinel and assert the loader-shape result on
47// the response.
48describe("cache-policy: private repos bypass shared cache", () => {
49 it("private commit-diff endpoint does not write to /_cache/commit-patch", async () => {
50 const ns = `cp-diff-${Math.random().toString(36).slice(2, 8)}`;
51 const member = await makeMember(ns);
52 const repoSlug = "site";
53 await seedRepo(env, {
54 namespaceSlug: ns,
55 repoSlug,
56 userId: member.userId,
57 visibility: "private",
58 });
59 const repoId = `${ns}/${repoSlug}`;
60 const seeded = await seedPackFirstRepo(repoId);
61 
62 const cacheKey = buildCacheKeyFrom(
63 new Request(
64 `https://example.com/${ns}/${repoSlug}/commit/${seeded.nextCommit.oid}/diff?path=README.md`
65 ),
66 "/_cache/commit-patch",
67 { repo: repoId, oid: seeded.nextCommit.oid, path: "README.md", v: "1" }
68 );
69 // Pre-clear in case a previous test seeded it.
70 expect(await cacheGetJSON(cacheKey)).toBeNull();
71 
72 const res = await workerExports.default.fetch(
73 `https://example.com/${ns}/${repoSlug}/commit/${seeded.nextCommit.oid}/diff?path=README.md`,
74 { headers: { Cookie: member.cookieHeader } }
75 );
76 expect(res.status).toBe(200);
77 const body = (await res.json()) as CommitFilePatchResult;
78 // Loader actually ran: real diff present, no skip sentinel.
79 expect(body.path).toBe("README.md");
80 expect(body.skipped).not.toBe(true);
81 expect(typeof body.patch).toBe("string");
82 // The private path must NOT have written the patch into shared cache.
83 expect(await cacheGetJSON(cacheKey)).toBeNull();
84 });
85 
86 it("public-then-private flip: pre-warmed commit-patch cache is NOT served on subsequent private read", async () => {
87 const ns = `cp-flip-${Math.random().toString(36).slice(2, 8)}`;
88 const member = await makeMember(ns);
89 const repoSlug = "site";
90 const seedRow = await seedRepo(env, {
91 namespaceSlug: ns,
92 repoSlug,
93 userId: member.userId,
94 visibility: "public",
95 });
96 const repoId = `${ns}/${repoSlug}`;
97 const seeded = await seedPackFirstRepo(repoId);
98 
99 // Public read primes the typed cache shape (loader executes, response
100 // is well-formed). We then overwrite the cache entry with a poisoned
101 // body so we can detect bypass on the subsequent private read.
102 const publicRes = await workerExports.default.fetch(
103 `https://example.com/${ns}/${repoSlug}/commit/${seeded.nextCommit.oid}/diff?path=README.md`
104 );
105 expect(publicRes.status).toBe(200);
106 const cacheKey = buildCacheKeyFrom(
107 new Request(
108 `https://example.com/${ns}/${repoSlug}/commit/${seeded.nextCommit.oid}/diff?path=README.md`
109 ),
110 "/_cache/commit-patch",
111 { repo: repoId, oid: seeded.nextCommit.oid, path: "README.md", v: "1" }
112 );
113 // Workers Cache writes are best-effort in vitest pool workers; the
114 // assertion below confirms the poison body is what we'd serve if cache
115 // bypass were broken. If the prime didn't take, the test still
116 // exercises the bypass write-side via the second read's cache state.
117 const poisoned: CommitFilePatchResult = {
118 path: "README.md",
119 changeType: "M",
120 skipped: true,
121 skipReason: "binary",
122 };
123 await cachePutJSON(cacheKey, poisoned, 86400);
124 const primed = await cacheGetJSON<CommitFilePatchResult>(cacheKey);
125 expect(primed?.skipReason).toBe("binary");
126 
127 // Flip to private.
128 const flipRes = await workerExports.default.fetch(
129 `https://example.com/auth/api/repositories/${encodeURIComponent(seedRow.repositoryId)}`,
130 {
131 method: "PATCH",
132 headers: {
133 "Content-Type": "application/json",
134 Origin: "https://example.com",
135 Cookie: member.cookieHeader,
136 },
137 body: JSON.stringify({ visibility: "private" }),
138 }
139 );
140 expect(flipRes.status).toBe(200);
141 
142 // Member read after flip MUST NOT serve the poisoned cache; the
143 // bypassed loader returns the real diff with `patch` set and no skip
144 // sentinel. We also confirm the response's `Cache-Control` is
145 // `no-store` so downstream caches do not reuse the body.
146 const privateRes = await workerExports.default.fetch(
147 `https://example.com/${ns}/${repoSlug}/commit/${seeded.nextCommit.oid}/diff?path=README.md`,
148 { headers: { Cookie: member.cookieHeader } }
149 );
150 expect(privateRes.status).toBe(200);
151 const body = (await privateRes.json()) as CommitFilePatchResult;
152 expect(body.path).toBe("README.md");
153 expect(body.skipped).not.toBe(true);
154 expect(body.skipReason).not.toBe("binary");
155 expect(typeof body.patch).toBe("string");
156 });
157});