| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | |
| 3 | import { InvalidReceivePackRequestError } from './protocol.errors.js'; |
| 4 | import { bufferBody } from './git-request-body.js'; |
| 5 | import { |
| 6 | isProbeRequest, |
| 7 | parseReceivePackRequest, |
| 8 | readReceivePackHeader, |
| 9 | } from './receive-pack-request.js'; |
| 10 | |
| 11 | const ZERO = '0'.repeat(40); |
| 12 | const NEW = '55ff3318cbb1ad74a1e1a1e6f4bd91f4b5a9c0d2'; |
| 13 | |
| 14 | function pkt(payload: string) { |
| 15 | const length = (Buffer.byteLength(payload) + 4).toString(16).padStart(4, '0'); |
| 16 | return length + payload; |
| 17 | } |
| 18 | |
| 19 | function body(lines: string[], pack = Buffer.alloc(0)) { |
| 20 | return Buffer.concat([Buffer.from(lines.join('') + '0000', 'utf8'), pack]); |
| 21 | } |
| 22 | |
| 23 | describe('parseReceivePackRequest', () => { |
| 24 | it('parses a single ref update with capabilities', () => { |
| 25 | const request = parseReceivePackRequest( |
| 26 | body( |
| 27 | [pkt(`${ZERO} ${NEW} refs/heads/main\0report-status side-band-64k\n`)], |
| 28 | Buffer.from('PACKPAYLOAD'), |
| 29 | ), |
| 30 | ); |
| 31 | |
| 32 | expect(request.transitions).toHaveLength(1); |
| 33 | expect(request.transitions[0].ref).toBe('refs/heads/main'); |
| 34 | expect(request.transitions[0].oldOid).toEqual(Buffer.alloc(20)); |
| 35 | expect(request.transitions[0].newOid).toEqual(Buffer.from(NEW, 'hex')); |
| 36 | expect(request.capabilities).toContain('side-band-64k'); |
| 37 | expect(request.pack.toString()).toBe('PACKPAYLOAD'); |
| 38 | }); |
| 39 | |
| 40 | it('parses a multi-ref atomic push', () => { |
| 41 | const request = parseReceivePackRequest( |
| 42 | body([ |
| 43 | pkt(`${ZERO} ${NEW} refs/heads/main\0report-status\n`), |
| 44 | pkt(`${ZERO} ${NEW} refs/heads/dev\n`), |
| 45 | ]), |
| 46 | ); |
| 47 | |
| 48 | expect(request.transitions.map((t) => t.ref)).toEqual([ |
| 49 | 'refs/heads/main', |
| 50 | 'refs/heads/dev', |
| 51 | ]); |
| 52 | }); |
| 53 | |
| 54 | it('accepts a delete-only push with no packfile', () => { |
| 55 | const request = parseReceivePackRequest( |
| 56 | body([pkt(`${NEW} ${ZERO} refs/heads/stale\n`)]), |
| 57 | ); |
| 58 | |
| 59 | expect(request.pack).toHaveLength(0); |
| 60 | expect(request.transitions[0].newOid).toEqual(Buffer.alloc(20)); |
| 61 | }); |
| 62 | |
| 63 | it('recognises the flush-only probe git sends before a chunked push', async () => { |
| 64 | expect(await isProbeRequest(bufferBody(Buffer.from('0000')))).toBe(true); |
| 65 | }); |
| 66 | |
| 67 | it('does not mistake a real push for a probe', async () => { |
| 68 | const push = bufferBody(body([pkt(`${ZERO} ${NEW} refs/heads/main\n`)])); |
| 69 | expect(await isProbeRequest(push)).toBe(false); |
| 70 | expect(await isProbeRequest(bufferBody(Buffer.alloc(0)))).toBe(false); |
| 71 | expect(await isProbeRequest(bufferBody(Buffer.from('0000PACK')))).toBe( |
| 72 | false, |
| 73 | ); |
| 74 | }); |
| 75 | |
| 76 | it('locates the packfile without reading it', async () => { |
| 77 | const raw = body( |
| 78 | [pkt(`${ZERO} ${NEW} refs/heads/main\0report-status\n`)], |
| 79 | Buffer.from('PACKPAYLOAD'), |
| 80 | ); |
| 81 | |
| 82 | const header = await readReceivePackHeader(bufferBody(raw)); |
| 83 | |
| 84 | expect(header.transitions[0].ref).toBe('refs/heads/main'); |
| 85 | expect(header.packSize).toBe('PACKPAYLOAD'.length); |
| 86 | expect(raw.subarray(header.packOffset).toString()).toBe('PACKPAYLOAD'); |
| 87 | }); |
| 88 | |
| 89 | it('rejects a body with no commands', () => { |
| 90 | expect(() => parseReceivePackRequest(body([]))).toThrow( |
| 91 | InvalidReceivePackRequestError, |
| 92 | ); |
| 93 | }); |
| 94 | |
| 95 | it('rejects a truncated pkt-line', () => { |
| 96 | expect(() => |
| 97 | parseReceivePackRequest( |
| 98 | Buffer.from(`00ff${ZERO} ${NEW} refs/heads/main`), |
| 99 | ), |
| 100 | ).toThrow(InvalidReceivePackRequestError); |
| 101 | }); |
| 102 | |
| 103 | it('rejects a payload that is not a packfile', () => { |
| 104 | expect(() => |
| 105 | parseReceivePackRequest( |
| 106 | body( |
| 107 | [pkt(`${ZERO} ${NEW} refs/heads/main\n`)], |
| 108 | Buffer.from('NOTAPACK'), |
| 109 | ), |
| 110 | ), |
| 111 | ).toThrow(InvalidReceivePackRequestError); |
| 112 | }); |
| 113 | }); |