Skip to content
File

Blob: test/route-cache-sync.worker.test.ts

typescript140 lines
1import { beforeAll, describe, expect, it } from "vitest";
2import { env } from "cloudflare:workers";
3 
4import { createDb } from "@/worker/db/d1/client";
5import { deleteRepositoryById, updateRepositoryVisibility } from "@/worker/db/d1/dal";
6import { putRouteCacheRecord, routeCacheKey } from "@/worker/repositories/routeCache";
7 
8import { ensureD1Migrations } from "./util/d1Setup";
9import { seedRepo, type SeededRepo } from "./util/repoSeed";
10import { runQueueMessage } from "./util/queue";
11 
12beforeAll(async () => {
13 await ensureD1Migrations(env);
14});
15 
16function uniqueNs(): string {
17 return `rcs-${Math.random().toString(36).slice(2, 8)}`;
18}
19 
20function syncMessage(
21 s: SeededRepo,
22 override?: Partial<{ namespaceSlug: string; repoSlug: string }>
23) {
24 return {
25 kind: "route-cache-sync" as const,
26 repositoryId: s.repositoryId,
27 namespaceSlug: override?.namespaceSlug ?? s.namespaceSlug,
28 repoSlug: override?.repoSlug ?? s.repoSlug,
29 enqueuedAt: Date.now(),
30 };
31}
32 
33describe("route-cache-sync consumer", () => {
34 it("D1 public + KV absent -> writes canonical record", async () => {
35 const s = await seedRepo(env, {
36 namespaceSlug: uniqueNs(),
37 repoSlug: "site",
38 visibility: "public",
39 skipRouteCache: true,
40 });
41 expect(await env.ROUTES.get(s.routeCacheKey)).toBeNull();
42 const result = await runQueueMessage(syncMessage(s));
43 expect(result.acked).toBe(true);
44 const raw = await env.ROUTES.get(s.routeCacheKey);
45 expect(raw).not.toBeNull();
46 const record = JSON.parse(raw!) as {
47 repositoryId: string;
48 namespaceId: string;
49 doName: string;
50 };
51 expect(record.repositoryId).toBe(s.repositoryId);
52 expect(record.namespaceId).toBe(s.namespaceId);
53 expect(record.doName).toBe(s.doName);
54 });
55 
56 it("D1 private + KV present -> deletes canonical key", async () => {
57 const s = await seedRepo(env, {
58 namespaceSlug: uniqueNs(),
59 repoSlug: "site",
60 visibility: "private",
61 });
62 await putRouteCacheRecord(env, s.namespaceSlug, s.repoSlug, {
63 repositoryId: s.repositoryId,
64 namespaceId: s.namespaceId,
65 doName: s.doName,
66 updatedAt: Date.now(),
67 });
68 expect(await env.ROUTES.get(s.routeCacheKey)).not.toBeNull();
69 const result = await runQueueMessage(syncMessage(s));
70 expect(result.acked).toBe(true);
71 expect(await env.ROUTES.get(s.routeCacheKey)).toBeNull();
72 });
73 
74 it("D1 row missing -> deletes captured key", async () => {
75 const s = await seedRepo(env, {
76 namespaceSlug: uniqueNs(),
77 repoSlug: "site",
78 visibility: "public",
79 });
80 const db = createDb(env.DB);
81 await deleteRepositoryById(db, s.repositoryId);
82 const result = await runQueueMessage(syncMessage(s));
83 expect(result.acked).toBe(true);
84 expect(await env.ROUTES.get(s.routeCacheKey)).toBeNull();
85 });
86 
87 it("private -> public flip: re-running sync repopulates ROUTES", async () => {
88 const s = await seedRepo(env, {
89 namespaceSlug: uniqueNs(),
90 repoSlug: "site",
91 visibility: "public",
92 skipRouteCache: true,
93 });
94 await runQueueMessage(syncMessage(s));
95 expect(await env.ROUTES.get(s.routeCacheKey)).not.toBeNull();
96 const db = createDb(env.DB);
97 await updateRepositoryVisibility(db, s.repositoryId, "private", Date.now());
98 await runQueueMessage(syncMessage(s));
99 expect(await env.ROUTES.get(s.routeCacheKey)).toBeNull();
100 await updateRepositoryVisibility(db, s.repositoryId, "public", Date.now());
101 await runQueueMessage(syncMessage(s));
102 expect(await env.ROUTES.get(s.routeCacheKey)).not.toBeNull();
103 });
104 
105 it("stale captured key + canonical mismatch: deletes captured, puts canonical", async () => {
106 const s = await seedRepo(env, {
107 namespaceSlug: uniqueNs(),
108 repoSlug: "site",
109 visibility: "public",
110 skipRouteCache: true,
111 });
112 const staleNamespaceSlug = `${s.namespaceSlug}-stale`;
113 await putRouteCacheRecord(env, staleNamespaceSlug, s.repoSlug, {
114 repositoryId: s.repositoryId,
115 namespaceId: s.namespaceId,
116 doName: s.doName,
117 updatedAt: Date.now(),
118 });
119 const result = await runQueueMessage(syncMessage(s, { namespaceSlug: staleNamespaceSlug }));
120 expect(result.acked).toBe(true);
121 expect(await env.ROUTES.get(routeCacheKey(staleNamespaceSlug, s.repoSlug))).toBeNull();
122 expect(await env.ROUTES.get(s.routeCacheKey)).not.toBeNull();
123 });
124 
125 it("replay: running the same message twice is idempotent", async () => {
126 const s = await seedRepo(env, {
127 namespaceSlug: uniqueNs(),
128 repoSlug: "site",
129 visibility: "public",
130 skipRouteCache: true,
131 });
132 const message = syncMessage(s);
133 const first = await runQueueMessage(message);
134 const second = await runQueueMessage(message);
135 expect(first.acked).toBe(true);
136 expect(second.acked).toBe(true);
137 expect(await env.ROUTES.get(s.routeCacheKey)).not.toBeNull();
138 });
139});