import assert from 'node:assert/strict'; import { after, before, test } from 'node:test'; import { importRepo, pushChanges } from '../src/tangled/git.ts'; import { TangledError } from '../src/tangled/index.ts'; import { startGitServer, type GitServer } from './git-server.ts'; const password = 'eyJhbGciOiJFUzI1NksifQ.eyJseG0iOiJvcmcudGFuZ2xlZC5naXQucmVjZWl2ZVBhY2sifQ.c2ln'; const text = (bytes: Uint8Array | undefined) => new TextDecoder().decode(bytes); const bytes = (s: string) => new TextEncoder().encode(s); const author = { name: 'Nib Tester', email: 'nib@example.com', timestamp: 1_790_000_000 }; let server: GitServer; let seedCommit: string; before(async () => { server = await startGitServer({ name: 'did:plc:testrepo', password, seed: { 'README.md': 'hello\n', 'src/main.ts': 'console.log(1);\n', 'src/old.ts': 'old\n', 'lib/only.ts': 'only\n', 'bin/run.sh': '#!/bin/sh\necho run\n', }, executable: ['bin/run.sh'], }); seedCommit = server.git('rev-parse', 'main'); }); after(() => server.close()); test('importRepo fetches the default branch tip and its files', async () => { const repo = await importRepo({ cloneUrl: server.url }); assert.equal(repo.commit, seedCommit); assert.equal(repo.ref, 'main'); assert.deepEqual([...repo.files.keys()].sort(), ['README.md', 'bin/run.sh', 'lib/only.ts', 'src/main.ts', 'src/old.ts']); assert.equal(text(repo.files.get('src/main.ts')), 'console.log(1);\n'); assert.equal(text(repo.files.get('bin/run.sh')), '#!/bin/sh\necho run\n'); }); test('pushChanges without a valid token is rejected and leaves the branch alone', async () => { await assert.rejects( pushChanges({ repo: { cloneUrl: server.url }, base: { commit: seedCommit, ref: 'main' }, files: new Map([['README.md', bytes('nope\n')]]), message: 'should not land', author, token: 'wrong.token.value', }), (error: unknown) => error instanceof TangledError && error.code === 'auth-failed', ); assert.equal(server.git('rev-parse', 'main'), seedCommit); const pushes = server.requests.filter((r) => r.path.includes('git-receive-pack')); assert.deepEqual(pushes.map((r) => [r.status, r.authorized]), [[401, false], [401, false]]); }); test('pushChanges commits an overlay on the base and pushes it with the token as the Basic password', async () => { server.requests.length = 0; let minted = 0; const pushed = await pushChanges({ repo: { cloneUrl: server.url }, base: { commit: seedCommit, ref: 'main' }, files: new Map([ ['src/main.ts', bytes('console.log(2);\n')], ['docs/new.md', bytes('# New\n')], ]), removed: ['src/old.ts', 'lib/only.ts'], message: 'Edit from nibling\n', author, token: async () => { minted += 1; return password; }, }); assert.equal(pushed.ref, 'main'); assert.equal(server.git('rev-parse', 'main'), pushed.commit); assert.equal(server.git('rev-parse', 'main^'), seedCommit); assert.equal(server.git('show', 'main:src/main.ts'), 'console.log(2);'); assert.equal(server.git('show', 'main:docs/new.md'), '# New'); assert.equal(server.git('show', 'main:README.md'), 'hello'); assert.equal(server.git('ls-tree', '-r', '--name-only', 'main'), ['README.md', 'bin/run.sh', 'docs/new.md', 'src/main.ts'].join('\n')); assert.match(server.git('ls-tree', 'main', 'bin/run.sh'), /^100755 blob /); assert.equal(server.git('log', '-1', '--format=%an <%ae> %at%n%B', 'main'), 'Nib Tester 1790000000\nEdit from nibling'); assert.equal(server.git('fsck', '--strict', '--no-dangling'), ''); assert.equal(minted, 1); const pushes = server.requests.filter((r) => r.path.includes('git-receive-pack')); assert.deepEqual( pushes.map((r) => [r.method, r.status, r.authorized]), [['GET', 401, false], ['GET', 200, true], ['POST', 200, true]], ); const reimported = await importRepo({ cloneUrl: server.url }); assert.equal(reimported.commit, pushed.commit); assert.equal(text(reimported.files.get('src/main.ts')), 'console.log(2);\n'); assert.equal(reimported.files.has('src/old.ts'), false); }); test('pushChanges from a stale base fails with remote-moved', async () => { await assert.rejects( pushChanges({ repo: { cloneUrl: server.url }, base: { commit: seedCommit, ref: 'main' }, files: new Map([['README.md', bytes('stale\n')]]), message: 'stale', author, token: password, }), (error: unknown) => error instanceof TangledError && error.code === 'remote-moved', ); }); test('pushChanges refuses a file whose path runs through another file', async () => { const tip = server.git('rev-parse', 'main'); await assert.rejects( pushChanges({ repo: { cloneUrl: server.url }, base: { commit: tip, ref: 'main' }, files: new Map([['README.md/inner', bytes('x')]]), message: 'bad', author, token: password, }), (error: unknown) => error instanceof TangledError && error.code === 'bad-change', ); assert.equal(server.git('rev-parse', 'main'), tip); });