Skip to content
File

Blob: test/repository-resolver.worker.test.ts

typescript182 lines
1import { applyD1Migrations } from "cloudflare:test";
2import { env } from "cloudflare:workers";
3import { beforeAll, beforeEach, describe, expect, it } from "vitest";
4 
5import { createDb } from "@/worker/db/d1/client";
6import {
7 insertMembershipIfMissing,
8 insertRepositoryIfNew,
9 insertUserIfNew,
10} from "@/worker/db/d1/dal";
11import { putRouteCacheRecord, routeCacheKey } from "@/worker/repositories/routeCache";
12import { claimNamespace } from "@/worker/db/d1/dal/namespaces";
13import { repositories } from "@/worker/db/d1/schema";
14import { resolveRepositoryRoute } from "@/worker/repositories/route";
15import { eq } from "drizzle-orm";
16 
17import { readAppD1Migrations } from "./util/d1Migrations";
18 
19beforeAll(async () => {
20 await applyD1Migrations(env.DB, readAppD1Migrations());
21});
22 
23const USER_ID = "user-rr";
24const NAMESPACE_ID = "ns-rr";
25const NAMESPACE_SLUG = "rr-rachel";
26const REPO_ID = "repo-rr";
27const REPO_SLUG = "site";
28const DO_NAME = "repo:uuid-rr";
29 
30beforeEach(async () => {
31 const db = createDb(env.DB);
32 const now = Date.now();
33 await insertUserIfNew(db, { id: USER_ID, tesseraSub: "sub-rr", createdAt: now });
34 await claimNamespace(db, {
35 id: NAMESPACE_ID,
36 slug: NAMESPACE_SLUG,
37 createdBy: USER_ID,
38 createdAt: now,
39 });
40 await insertMembershipIfMissing(db, {
41 namespaceId: NAMESPACE_ID,
42 userId: USER_ID,
43 createdAt: now,
44 });
45 await insertRepositoryIfNew(db, {
46 id: REPO_ID,
47 namespaceId: NAMESPACE_ID,
48 createdBy: USER_ID,
49 slug: REPO_SLUG,
50 doName: DO_NAME,
51 visibility: "public",
52 createdAt: now,
53 updatedAt: now,
54 });
55 // Reset known cache keys.
56 for (const slug of [REPO_SLUG, "renamed", "ghost"]) {
57 await env.ROUTES.delete(routeCacheKey(NAMESPACE_SLUG, slug));
58 }
59});
60 
61describe("resolveRepositoryRoute", () => {
62 it("returns the route via KV when cached metadata matches D1", async () => {
63 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
64 repositoryId: REPO_ID,
65 namespaceId: NAMESPACE_ID,
66 doName: DO_NAME,
67 updatedAt: Date.now(),
68 });
69 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG);
70 expect(route).toMatchObject({
71 routeNamespaceSlug: NAMESPACE_SLUG,
72 routeRepoSlug: REPO_SLUG,
73 namespaceId: NAMESPACE_ID,
74 repositoryId: REPO_ID,
75 doName: DO_NAME,
76 visibility: "public",
77 source: "kv",
78 });
79 });
80 
81 it("falls through KV when the cached repositoryId is stale", async () => {
82 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
83 repositoryId: "ghost-id",
84 namespaceId: NAMESPACE_ID,
85 doName: DO_NAME,
86 updatedAt: Date.now(),
87 });
88 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG);
89 expect(route?.source).toBe("d1");
90 expect(route?.repositoryId).toBe(REPO_ID);
91 });
92 
93 it("falls through KV when the cached doName is stale", async () => {
94 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
95 repositoryId: REPO_ID,
96 namespaceId: NAMESPACE_ID,
97 doName: "stale/doName",
98 updatedAt: Date.now(),
99 });
100 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG);
101 expect(route?.source).toBe("d1");
102 expect(route?.doName).toBe(DO_NAME);
103 });
104 
105 it("falls through KV when the repository's current slug differs from the URL", async () => {
106 // Old URL key still in KV; the repo has been renamed in D1.
107 const db = createDb(env.DB);
108 await db
109 .update(repositories)
110 .set({ slug: "renamed", updatedAt: Date.now() })
111 .where(eq(repositories.id, REPO_ID));
112 try {
113 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
114 repositoryId: REPO_ID,
115 namespaceId: NAMESPACE_ID,
116 doName: DO_NAME,
117 updatedAt: Date.now(),
118 });
119 // Old URL no longer resolves; the resolver must NOT return the renamed repo.
120 expect(await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG)).toBeNull();
121 // New URL resolves via D1.
122 const renamed = await resolveRepositoryRoute(env, NAMESPACE_SLUG, "renamed");
123 expect(renamed?.source).toBe("d1");
124 expect(renamed?.repositoryId).toBe(REPO_ID);
125 } finally {
126 // Restore for sibling tests; D1 in this pool is shared across tests.
127 await db
128 .update(repositories)
129 .set({ slug: REPO_SLUG, updatedAt: Date.now() })
130 .where(eq(repositories.id, REPO_ID));
131 }
132 });
133 
134 it("falls through KV when the cached namespaceId no longer matches the URL slug", async () => {
135 // Stash a bad cached record claiming a different namespaceId.
136 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
137 repositoryId: REPO_ID,
138 namespaceId: "ns-other",
139 doName: DO_NAME,
140 updatedAt: Date.now(),
141 });
142 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG);
143 // The slug-based fallback still finds the canonical repo via D1.
144 expect(route?.source).toBe("d1");
145 expect(route?.namespaceId).toBe(NAMESPACE_ID);
146 });
147 
148 it("returns null on KV miss + D1 miss", async () => {
149 expect(await resolveRepositoryRoute(env, NAMESPACE_SLUG, "ghost")).toBeNull();
150 });
151 
152 it("returns the route via D1 on KV miss", async () => {
153 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG);
154 expect(route?.source).toBe("d1");
155 expect(route?.repositoryId).toBe(REPO_ID);
156 });
157 
158 it("returns null in cache-only mode when KV misses even if D1 has the row", async () => {
159 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG, {
160 mode: "route-cache-only",
161 });
162 expect(route).toBeNull();
163 });
164 
165 it("returns null in cache-only mode when the cached record is stale", async () => {
166 await putRouteCacheRecord(env, NAMESPACE_SLUG, REPO_SLUG, {
167 repositoryId: "ghost-id",
168 namespaceId: NAMESPACE_ID,
169 doName: DO_NAME,
170 updatedAt: Date.now(),
171 });
172 const route = await resolveRepositoryRoute(env, NAMESPACE_SLUG, REPO_SLUG, {
173 mode: "route-cache-only",
174 });
175 expect(route).toBeNull();
176 });
177 
178 it("returns null when the namespace slug does not exist", async () => {
179 expect(await resolveRepositoryRoute(env, "nope", REPO_SLUG)).toBeNull();
180 });
181});