Skip to content
File

Blob: test/receive-connectivity.worker.test.ts

typescript331 lines
1import { it, expect } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3import { decodePktLines, pktLine, flushPkt, concatChunks } from "@/worker/git";
4import { postReceivePack, uniqueRepoId, buildPack, zero40 } from "./util/test-helpers";
5import { setupRepoForTests } from "./util/repoSeed";
6import { bytesToHex } from "@/worker/common/hex";
7 
8it("receive-pack connectivity: rejects commit whose root tree is missing", async () => {
9 const owner = "o";
10 const repo = uniqueRepoId("r-connectivity-missing-tree");
11 const seededRepo = await setupRepoForTests(env, owner, repo);
12 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
13 
14 // Construct a commit that points to a tree OID that is NOT present anywhere on the server
15 const missingTreeOid = "c".repeat(40);
16 const author = `You <you@example.com> 0 +0000`;
17 const committer = author;
18 const msg = "missing tree\n";
19 const commitPayload = new TextEncoder().encode(
20 `tree ${missingTreeOid}\n` + `author ${author}\n` + `committer ${committer}\n\n${msg}`
21 );
22 
23 // Compute commit oid for assertions (like real git object oid)
24 const commitOid = await (async () => {
25 const head = new TextEncoder().encode(`commit ${commitPayload.byteLength}\0`);
26 const raw = new Uint8Array(head.length + commitPayload.length);
27 raw.set(head, 0);
28 raw.set(commitPayload, head.length);
29 const hash = await crypto.subtle.digest("SHA-1", raw);
30 return bytesToHex(new Uint8Array(hash));
31 })();
32 
33 // Create a PACK containing ONLY the commit (missing its root tree)
34 const pack = await buildPack([{ type: "commit", payload: commitPayload }]);
35 
36 // Commands section (pkt-lines) followed by flush and then raw pack bytes
37 const cmd = `${zero40()} ${commitOid} refs/heads/main\0 report-status ofs-delta agent=test\n`;
38 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
39 
40 const res = await postReceivePack(url, body);
41 expect(res.status).toBe(200);
42 
43 // report-status should contain ng for the ref due to missing-objects
44 const bytes = new Uint8Array(await res.arrayBuffer());
45 const items = decodePktLines(bytes);
46 const lines = items.filter((i) => i.type === "line").map((i: any) => (i.text as string).trim());
47 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
48 const ng = lines.find((l) => l.startsWith("ng refs/heads/main"));
49 expect(ng && /missing-objects/.test(ng)).toBeTruthy();
50 
51 // Verify ref was NOT created
52 const refsRes = await workerExports.default.fetch(
53 `https://example.com/${owner}/${repo}/admin/refs`,
54 { headers: { Cookie: seededRepo.cookieHeader } }
55 );
56 expect(refsRes.status).toBe(200);
57 const refs = await refsRes.json<any>();
58 expect(refs.find((r: any) => r.name === "refs/heads/main")).toBeUndefined();
59});
60 
61it("receive-pack connectivity: accepts annotated tag pointing to commit with present tree", async () => {
62 const owner = "o";
63 const repo = uniqueRepoId("r-tag-commit-ok");
64 await setupRepoForTests(env, owner, repo);
65 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
66 
67 // Build empty tree and commit
68 const treePayload = new Uint8Array(0);
69 const treeOid = await (async () => {
70 const head = new TextEncoder().encode(`tree ${treePayload.byteLength}\0`);
71 const raw = new Uint8Array(head.length + treePayload.length);
72 raw.set(head, 0);
73 raw.set(treePayload, head.length);
74 const hash = await crypto.subtle.digest("SHA-1", raw);
75 return bytesToHex(new Uint8Array(hash));
76 })();
77 const author = `You <you@example.com> 0 +0000`;
78 const committer = author;
79 const msg = "tag->commit ok\n";
80 const commitPayload = new TextEncoder().encode(
81 `tree ${treeOid}\n` + `author ${author}\n` + `committer ${committer}\n\n${msg}`
82 );
83 const commitOid = await (async () => {
84 const head = new TextEncoder().encode(`commit ${commitPayload.byteLength}\0`);
85 const raw = new Uint8Array(head.length + commitPayload.length);
86 raw.set(head, 0);
87 raw.set(commitPayload, head.length);
88 const hash = await crypto.subtle.digest("SHA-1", raw);
89 return bytesToHex(new Uint8Array(hash));
90 })();
91 // Annotated tag pointing to the commit
92 const tagPayload = new TextEncoder().encode(
93 `object ${commitOid}\n` +
94 `type commit\n` +
95 `tag v1\n` +
96 `tagger You <you@example.com> 0 +0000\n\nmsg\n`
97 );
98 const tagOid = await (async () => {
99 const head = new TextEncoder().encode(`tag ${tagPayload.byteLength}\0`);
100 const raw = new Uint8Array(head.length + tagPayload.length);
101 raw.set(head, 0);
102 raw.set(tagPayload, head.length);
103 const hash = await crypto.subtle.digest("SHA-1", raw);
104 return bytesToHex(new Uint8Array(hash));
105 })();
106 
107 const pack = await buildPack([
108 { type: "tree", payload: treePayload },
109 { type: "commit", payload: commitPayload },
110 { type: "tag", payload: tagPayload },
111 ]);
112 
113 const cmd = `${zero40()} ${tagOid} refs/tags/v1\0 report-status ofs-delta agent=test\n`;
114 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
115 const res = await postReceivePack(url, body);
116 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
117 .filter((i) => i.type === "line")
118 .map((i: any) => (i.text as string).trim());
119 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
120 expect(lines).toContain("ok refs/tags/v1");
121});
122 
123it("receive-pack connectivity: accepts annotated tag pointing to tree present", async () => {
124 const owner = "o";
125 const repo = uniqueRepoId("r-tag-tree-ok");
126 await setupRepoForTests(env, owner, repo);
127 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
128 
129 const treePayload = new Uint8Array(0);
130 const treeOid = await (async () => {
131 const head = new TextEncoder().encode(`tree ${treePayload.byteLength}\0`);
132 const raw = new Uint8Array(head.length + treePayload.length);
133 raw.set(head, 0);
134 raw.set(treePayload, head.length);
135 const hash = await crypto.subtle.digest("SHA-1", raw);
136 return bytesToHex(new Uint8Array(hash));
137 })();
138 const tagPayload = new TextEncoder().encode(
139 `object ${treeOid}\n` +
140 `type tree\n` +
141 `tag v2\n` +
142 `tagger You <you@example.com> 0 +0000\n\nmsg\n`
143 );
144 const tagOid = await (async () => {
145 const head = new TextEncoder().encode(`tag ${tagPayload.byteLength}\0`);
146 const raw = new Uint8Array(head.length + tagPayload.length);
147 raw.set(head, 0);
148 raw.set(tagPayload, head.length);
149 const hash = await crypto.subtle.digest("SHA-1", raw);
150 return bytesToHex(new Uint8Array(hash));
151 })();
152 const pack = await buildPack([
153 { type: "tree", payload: treePayload },
154 { type: "tag", payload: tagPayload },
155 ]);
156 const cmd = `${zero40()} ${tagOid} refs/tags/v2\0 report-status ofs-delta agent=test\n`;
157 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
158 const res = await postReceivePack(url, body);
159 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
160 .filter((i) => i.type === "line")
161 .map((i: any) => (i.text as string).trim());
162 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
163 expect(lines).toContain("ok refs/tags/v2");
164});
165 
166it("receive-pack connectivity: rejects annotated tag pointing to commit with missing tree", async () => {
167 const owner = "o";
168 const repo = uniqueRepoId("r-tag-commit-missing-tree");
169 await setupRepoForTests(env, owner, repo);
170 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
171 const missingTreeOid = "c".repeat(40);
172 const author = `You <you@example.com> 0 +0000`;
173 const committer = author;
174 const msg = "tag->commit missing tree\n";
175 const commitPayload = new TextEncoder().encode(
176 `tree ${missingTreeOid}\n` + `author ${author}\n` + `committer ${committer}\n\n${msg}`
177 );
178 const commitOid = await (async () => {
179 const head = new TextEncoder().encode(`commit ${commitPayload.byteLength}\0`);
180 const raw = new Uint8Array(head.length + commitPayload.length);
181 raw.set(head, 0);
182 raw.set(commitPayload, head.length);
183 const hash = await crypto.subtle.digest("SHA-1", raw);
184 return bytesToHex(new Uint8Array(hash));
185 })();
186 const tagPayload = new TextEncoder().encode(
187 `object ${commitOid}\n` +
188 `type commit\n` +
189 `tag v3\n` +
190 `tagger You <you@example.com> 0 +0000\n\nmsg\n`
191 );
192 const tagOid = await (async () => {
193 const head = new TextEncoder().encode(`tag ${tagPayload.byteLength}\0`);
194 const raw = new Uint8Array(head.length + tagPayload.length);
195 raw.set(head, 0);
196 raw.set(tagPayload, head.length);
197 const hash = await crypto.subtle.digest("SHA-1", raw);
198 return bytesToHex(new Uint8Array(hash));
199 })();
200 const pack = await buildPack([
201 { type: "commit", payload: commitPayload },
202 { type: "tag", payload: tagPayload },
203 ]);
204 const cmd = `${zero40()} ${tagOid} refs/tags/v3\0 report-status ofs-delta agent=test\n`;
205 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
206 const res = await postReceivePack(url, body);
207 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
208 .filter((i) => i.type === "line")
209 .map((i: any) => (i.text as string).trim());
210 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
211 const ng = lines.find((l) => l.startsWith("ng refs/tags/v3"));
212 expect(ng && /missing-objects/.test(ng)).toBeTruthy();
213});
214 
215it("receive-pack connectivity: accepts direct ref to tree present", async () => {
216 const owner = "o";
217 const repo = uniqueRepoId("r-tree-ref-ok");
218 await setupRepoForTests(env, owner, repo);
219 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
220 
221 // Build an empty tree
222 const treePayload = new Uint8Array(0);
223 const treeOid = await (async () => {
224 const head = new TextEncoder().encode(`tree ${treePayload.byteLength}\0`);
225 const raw = new Uint8Array(head.length + treePayload.length);
226 raw.set(head, 0);
227 raw.set(treePayload, head.length);
228 const hash = await crypto.subtle.digest("SHA-1", raw);
229 return bytesToHex(new Uint8Array(hash));
230 })();
231 
232 const pack = await buildPack([{ type: "tree", payload: treePayload }]);
233 
234 const cmd = `${zero40()} ${treeOid} refs/tags/tree-only\0 report-status ofs-delta agent=test\n`;
235 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
236 const res = await postReceivePack(url, body);
237 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
238 .filter((i) => i.type === "line")
239 .map((i: any) => (i.text as string).trim());
240 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
241 expect(lines).toContain("ok refs/tags/tree-only");
242});
243 
244it("receive-pack connectivity: accepts direct ref to blob present", async () => {
245 const owner = "o";
246 const repo = uniqueRepoId("r-blob-ref-ok");
247 await setupRepoForTests(env, owner, repo);
248 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
249 
250 const blobPayload = new TextEncoder().encode("hello\n");
251 const blobOid = await (async () => {
252 const head = new TextEncoder().encode(`blob ${blobPayload.byteLength}\0`);
253 const raw = new Uint8Array(head.length + blobPayload.length);
254 raw.set(head, 0);
255 raw.set(blobPayload, head.length);
256 const hash = await crypto.subtle.digest("SHA-1", raw);
257 return bytesToHex(new Uint8Array(hash));
258 })();
259 
260 const pack = await buildPack([{ type: "blob", payload: blobPayload }]);
261 
262 const cmd = `${zero40()} ${blobOid} refs/tags/blob-only\0 report-status ofs-delta agent=test\n`;
263 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
264 const res = await postReceivePack(url, body);
265 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
266 .filter((i) => i.type === "line")
267 .map((i: any) => (i.text as string).trim());
268 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
269 expect(lines).toContain("ok refs/tags/blob-only");
270});
271 
272it("receive-pack connectivity: accepts nested tag->tag->tree present", async () => {
273 const owner = "o";
274 const repo = uniqueRepoId("r-nested-tag-tree-ok");
275 await setupRepoForTests(env, owner, repo);
276 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
277 
278 const treePayload = new Uint8Array(0);
279 const treeOid = await (async () => {
280 const head = new TextEncoder().encode(`tree ${treePayload.byteLength}\0`);
281 const raw = new Uint8Array(head.length + treePayload.length);
282 raw.set(head, 0);
283 raw.set(treePayload, head.length);
284 const hash = await crypto.subtle.digest("SHA-1", raw);
285 return bytesToHex(new Uint8Array(hash));
286 })();
287 const tag1Payload = new TextEncoder().encode(
288 `object ${treeOid}\n` +
289 `type tree\n` +
290 `tag v1\n` +
291 `tagger You <you@example.com> 0 +0000\n\nmsg\n`
292 );
293 const tag1Oid = await (async () => {
294 const head = new TextEncoder().encode(`tag ${tag1Payload.byteLength}\0`);
295 const raw = new Uint8Array(head.length + tag1Payload.length);
296 raw.set(head, 0);
297 raw.set(tag1Payload, head.length);
298 const hash = await crypto.subtle.digest("SHA-1", raw);
299 return bytesToHex(new Uint8Array(hash));
300 })();
301 const tag2Payload = new TextEncoder().encode(
302 `object ${tag1Oid}\n` +
303 `type tag\n` +
304 `tag v2\n` +
305 `tagger You <you@example.com> 0 +0000\n\nmsg2\n`
306 );
307 const tag2Oid = await (async () => {
308 const head = new TextEncoder().encode(`tag ${tag2Payload.byteLength}\0`);
309 const raw = new Uint8Array(head.length + tag2Payload.length);
310 raw.set(head, 0);
311 raw.set(tag2Payload, head.length);
312 const hash = await crypto.subtle.digest("SHA-1", raw);
313 return bytesToHex(new Uint8Array(hash));
314 })();
315 
316 const pack = await buildPack([
317 { type: "tree", payload: treePayload },
318 { type: "tag", payload: tag1Payload },
319 { type: "tag", payload: tag2Payload },
320 ]);
321 
322 const cmd = `${zero40()} ${tag2Oid} refs/tags/v2\0 report-status ofs-delta agent=test\n`;
323 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
324 const res = await postReceivePack(url, body);
325 const lines = decodePktLines(new Uint8Array(await res.arrayBuffer()))
326 .filter((i) => i.type === "line")
327 .map((i: any) => (i.text as string).trim());
328 expect(lines.some((l) => l.startsWith("unpack ok"))).toBe(true);
329 expect(lines).toContain("ok refs/tags/v2");
330});