mirror your GitHub repos to tangled.org automatically
1import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2import { RemoteRejectedError } from '../../server/utils/git-wire/errors'
3import { fetchAdvertisement, fetchPack } from '../../server/utils/git-wire/upload-pack'
4import { fakeGithubFetch, GitFixture } from '../utils/git-wire'
5
6const PACK_MAGIC = Buffer.from('PACK')
7
8async function drain(gen: AsyncGenerator<Buffer>): Promise<Buffer> {
9 const parts: Buffer[] = []
10 for await (const c of gen) parts.push(c)
11 return Buffer.concat(parts)
12}
13
14describe('upload-pack (against real git-upload-pack)', () => {
15 let fx: GitFixture
16 let realFetch: typeof globalThis.fetch
17
18 beforeEach(() => {
19 fx = new GitFixture()
20 realFetch = globalThis.fetch
21 })
22
23 afterEach(() => {
24 globalThis.fetch = realFetch
25 fx.cleanup()
26 vi.restoreAllMocks()
27 })
28
29 function wire(repos: Map<string, string>) {
30 globalThis.fetch = fakeGithubFetch(repos) as unknown as typeof globalThis.fetch
31 }
32
33 it('resolves a ref name to a SHA via the advertisement', async () => {
34 const bare = fx.initBare('gh.git')
35 const work = fx.initWork('work')
36 const sha = fx.commit(work, 'a.txt', 'hello')
37 fx.pushTo(work, bare, 'HEAD:refs/heads/main')
38 wire(new Map([['owner/repo', bare]]))
39
40 const adv = await fetchAdvertisement('owner/repo', 'tok')
41 expect(adv.refs.get('refs/heads/main')).toBe(sha)
42 expect(adv.capabilities.has('thin-pack')).toBe(true)
43 })
44
45 it('fetches a full pack on first sync (no haves)', async () => {
46 const bare = fx.initBare('gh.git')
47 const work = fx.initWork('work')
48 const sha = fx.commit(work, 'a.txt', 'hello')
49 fx.pushTo(work, bare, 'HEAD:refs/heads/main')
50 wire(new Map([['owner/repo', bare]]))
51
52 const { pack } = await fetchPack({ repoFullName: 'owner/repo', token: 'tok', want: sha, haves: [], maxBytes: 1 << 30 })
53 const bytes = await drain(pack)
54 expect(bytes.subarray(0, 4)).toEqual(PACK_MAGIC)
55 expect(bytes.length).toBeGreaterThan(0)
56 })
57
58 it('sends a smaller pack when the knot tip is offered as a have', async () => {
59 const bare = fx.initBare('gh.git')
60 const work = fx.initWork('work')
61 const first = fx.commit(work, 'a.txt', 'a'.repeat(5000))
62 fx.pushTo(work, bare, 'HEAD:refs/heads/main')
63 const second = fx.commit(work, 'b.txt', 'b'.repeat(5000))
64 fx.pushTo(work, bare, 'HEAD:refs/heads/main')
65 wire(new Map([['owner/repo', bare]]))
66
67 const full = await drain((await fetchPack({ repoFullName: 'owner/repo', token: 'tok', want: second, haves: [], maxBytes: 1 << 30 })).pack)
68 const incremental = await drain((await fetchPack({ repoFullName: 'owner/repo', token: 'tok', want: second, haves: [first], maxBytes: 1 << 30 })).pack)
69
70 expect(incremental.subarray(0, 4)).toEqual(PACK_MAGIC)
71 expect(incremental.length).toBeLessThan(full.length)
72 })
73
74 it('aborts with too-big once the byte cap is exceeded', async () => {
75 const bare = fx.initBare('gh.git')
76 const work = fx.initWork('work')
77 const sha = fx.commit(work, 'a.txt', 'x'.repeat(10_000))
78 fx.pushTo(work, bare, 'HEAD:refs/heads/main')
79 wire(new Map([['owner/repo', bare]]))
80
81 const { pack } = await fetchPack({ repoFullName: 'owner/repo', token: 'tok', want: sha, haves: [], maxBytes: 10 })
82 await expect(drain(pack)).rejects.toMatchObject({
83 constructor: RemoteRejectedError,
84 reason: 'too-big',
85 })
86 })
87
88 it('throws on a 404 from github', async () => {
89 wire(new Map())
90 await expect(fetchAdvertisement('missing/repo', 'tok')).rejects.toThrow(/404/)
91 })
92})