| 1 | import { execFile, execFileSync } from 'node:child_process'; |
| 2 | import { mkdtempSync, rmSync |
| 3 | import { tmpdir } |
| 4 | import path from 'node:path |
| 5 | import { promisify } |
| 6 | import type { INestApplication |
| 7 | import request from 'supertest |
| 8 | import { afterAll, beforeAll |
| 9 | |
| 10 | import { hasBackends, signUp |
| 11 | |
| 12 | /** A push git would refuse must never reach the log: once there, every clone of the repository fails for good. */ |
| 13 | describe.skipIf(!hasBackends |
| 14 | let app: |
| 15 | let origin: |
| 16 | let owner: |
| 17 | let work: |
| 18 | const username = |
| 19 | let repo: |
| 20 | let first: |
| 21 | |
| 22 | const remote = |
| 23 | `${origin.replace( |
| 24 | const git = |
| 25 | execFileSync( |
| 26 | 'git', |
| 27 | ['-c', 'user.name=E2E' |
| 28 | { cwd: work, |
| 29 | ).trim(); |
| 30 | // The server runs in this process, so anything that talks to it must not block the event loop. |
| 31 | const run = |
| 32 | promisify(execFile)('git |
| 33 | cwd: work, |
| 34 | env: { ...process |
| 35 | }); |
| 36 | /** A receive-pack request git itself would never send: one ref update and whatever pack the test hands it. */ |
| 37 | const rawPush = |
| 38 | const command = |
| 39 | const line = |
| 40 | return fetch(`${ |
| 41 | method: 'POST', |
| 42 | headers: { |
| 43 | authorization: `Basic ${Buffer |
| 44 | 'content-type': 'application/x-git-receive-pack-request', |
| 45 | }, |
| 46 | body: Buffer.concat |
| 47 | }); |
| 48 | }; |
| 49 | const stillClones = |
| 50 | const target = |
| 51 | try { |
| 52 | await run('clone |
| 53 | return execFileSync('git |
| 54 | encoding: 'utf8', |
| 55 | }).trim(); |
| 56 | } finally { |
| 57 | rmSync(target, { |
| 58 | } |
| 59 | }; |
| 60 | |
| 61 | beforeAll(async () |
| 62 | ({ app, origin |
| 63 | owner = await |
| 64 | repo = ( |
| 65 | await request(app |
| 66 | .post('/api/repositories') |
| 67 | .set('cookie', |
| 68 | .send({ name: |
| 69 | .expect(201) |
| 70 | ).body.slug; |
| 71 | |
| 72 | work = mkdtempSync( |
| 73 | git('init', |
| 74 | writeFileSync(path.join |
| 75 | git('add', |
| 76 | git('commit', |
| 77 | first = git( |
| 78 | await run('push |
| 79 | }, 120_000); |
| 80 | |
| 81 | afterAll(async () |
| 82 | await app?.close |
| 83 | if (work) rmSync |
| 84 | }); |
| 85 | |
| 86 | it('refuses a ref at an object the push never carried, and the repository still clones', |
| 87 | const emptyPack = |
| 88 | Buffer.from('PACK |
| 89 | Buffer.from([0, |
| 90 | ]); |
| 91 | const response = |
| 92 | 'refs/heads/evil', |
| 93 | 'ab'.repeat(20 |
| 94 | emptyPack, |
| 95 | ); |
| 96 | |
| 97 | expect(response.ok |
| 98 | expect(await response |
| 99 | expect(await stillClones |
| 100 | }); |
| 101 | |
| 102 | it('refuses a branch at a tree', |
| 103 | const response = |
| 104 | 'refs/heads/tree', |
| 105 | git('rev-parse', |
| 106 | Buffer.alloc(0) |
| 107 | ); |
| 108 | |
| 109 | expect(response.ok |
| 110 | expect(await response |
| 111 | /ng refs\/heads\/tree |
| 112 | ); |
| 113 | expect(await stillClones |
| 114 | }); |
| 115 | |
| 116 | it('refuses a branch beneath an existing one, and the repository still clones', |
| 117 | await expect( |
| 118 | run('push', |
| 119 | ).rejects.toThrow( |
| 120 | /\[remote rejected\] main -> main\/ |
| 121 | ); |
| 122 | expect(await stillClones |
| 123 | }); |
| 124 | |
| 125 | it('still takes an ordinary thin push, and serves it', |
| 126 | writeFileSync(path.join |
| 127 | git('commit', |
| 128 | await run('push |
| 129 | |
| 130 | expect(await stillClones |
| 131 | }); |
| 132 | }); |