OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / create-repo.test.js

120 lines · javascript ref:
import test from 'brittle'
import { mkdtempSync, existsSync, readFileSync, mkdirSync, rmSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'

import { syncHandlers } from '../lib/services/sync.js'

// `create-repo` collapses the four-command hosting dance (init --bare /
// symbolic-ref HEAD / scan / push / scan) into one daemon call. The failure
// modes it removes are both silent: a skipped symbolic-ref leaves clones with
// no checked-out branch, and a skipped scan leaves the repo unserved.

function fakeSvc (reposDir) {
  const calls = { scan: 0, serve: 0 }
  return {
    _reposDir: reposDir,
    _sync: {
      calls,
      async scan () { calls.scan++; return new Map() },
      async serve () { calls.serve++ }
    }
  }
}

function handler (svc) {
  const h = syncHandlers(svc).find((x) => x.name === 'create-repo')
  if (!h) throw new Error('create-repo handler not registered')
  return h.fn
}

const UDS = { transport: 'uds' }

function freshDir (tag) {
  const root = mkdtempSync(join(tmpdir(), 'pgv3-mkrepo-' + tag + '-'))
  const repos = join(root, 'git-repos')
  mkdirSync(repos, { recursive: true })
  return { root, repos }
}

test('create-repo inits a bare repo, sets HEAD, and serves it', async (t) => {
  const { root, repos } = freshDir('ok')
  const svc = fakeSvc(repos)
  const res = await handler(svc)(UDS, { name: 'myapp', branch: 'main' })

  const dir = join(repos, 'myapp.git')
  t.is(res.name, 'myapp', 'returns the name')
  t.is(res.branch, 'main', 'returns the branch')
  t.is(res.path, dir, 'returns the bare repo path')
  t.ok(res.serving, 'reports serving')
  t.ok(existsSync(dir), 'bare repo created')
  t.ok(existsSync(join(dir, 'HEAD')), 'HEAD exists')
  t.is(readFileSync(join(dir, 'HEAD'), 'utf8').trim(), 'ref: refs/heads/main', 'HEAD points at the branch')
  // No `.git/index` / worktree — it must be BARE, or pushes to it fail.
  t.absent(existsSync(join(dir, '.git')), 'is a bare repo, not a worktree')
  t.is(svc._sync.calls.scan, 1, 'scanned (registers + installs post-receive hook)')
  t.is(svc._sync.calls.serve, 1, 'served (announced on the swarm)')
  rmSync(root, { recursive: true, force: true })
})

test('create-repo defaults the branch to main', async (t) => {
  const { root, repos } = freshDir('dflt')
  const res = await handler(fakeSvc(repos))(UDS, { name: 'nobranch' })
  t.is(res.branch, 'main', 'defaulted to main')
  t.is(readFileSync(join(repos, 'nobranch.git', 'HEAD'), 'utf8').trim(), 'ref: refs/heads/main', 'HEAD set anyway')
  rmSync(root, { recursive: true, force: true })
})

test('create-repo honours a non-default branch', async (t) => {
  const { root, repos } = freshDir('br')
  const res = await handler(fakeSvc(repos))(UDS, { name: 'legacy', branch: 'master' })
  t.is(res.branch, 'master', 'branch echoed')
  t.is(readFileSync(join(repos, 'legacy.git', 'HEAD'), 'utf8').trim(), 'ref: refs/heads/master', 'HEAD points at master')
  rmSync(root, { recursive: true, force: true })
})

test('create-repo refuses to clobber an existing repo', async (t) => {
  const { root, repos } = freshDir('dup')
  const svc = fakeSvc(repos)
  await handler(svc)(UDS, { name: 'twice' })
  await t.exception(() => handler(svc)(UDS, { name: 'twice' }), /already exists/, 'second create rejected')
  rmSync(root, { recursive: true, force: true })
})

// The name lands on the filesystem under reposDir AND is announced as a swarm
// repo id, so it is validated rather than trusted.
test('create-repo rejects path traversal and separators in the name', async (t) => {
  const { root, repos } = freshDir('bad')
  const fn = handler(fakeSvc(repos))
  for (const bad of ['../escape', 'a/b', '/abs', '.hidden', '', 'x/../../y']) {
    await t.exception(() => fn(UDS, { name: bad }), /invalid repo name/, `rejected ${JSON.stringify(bad)}`)
  }
  rmSync(root, { recursive: true, force: true })
})

test('create-repo rejects a malformed branch name', async (t) => {
  const { root, repos } = freshDir('badbr')
  const fn = handler(fakeSvc(repos))
  await t.exception(() => fn(UDS, { name: 'ok1', branch: '--upload-pack=evil' }), /invalid branch name/, 'rejected flag-like branch')
  await t.exception(() => fn(UDS, { name: 'ok2', branch: '../../etc/passwd' }), /invalid branch name/, 'rejected traversal in branch')
  // An empty branch is not malformed — it falls back to the default.
  const res = await fn(UDS, { name: 'ok3', branch: '' })
  t.is(res.branch, 'main', 'empty branch falls back to main rather than erroring')
  rmSync(root, { recursive: true, force: true })
})

test('create-repo requires auth on non-uds transports', async (t) => {
  const { root, repos } = freshDir('auth')
  const fn = handler(fakeSvc(repos))
  await t.exception(() => fn({ transport: 'http' }, { name: 'nope' }), /authentication required/, 'anonymous http rejected')
  // an identified http caller is allowed
  const res = await fn({ transport: 'http', identity: { publicKey: 'ab' } }, { name: 'yes' })
  t.is(res.name, 'yes', 'identified caller allowed')
  rmSync(root, { recursive: true, force: true })
})

test('create-repo fails clearly when the daemon has no sync service', async (t) => {
  const svc = { _reposDir: '/tmp', _sync: null }
  await t.exception(() => handler(svc)(UDS, { name: 'x' }), /sync service not active/, 'clear error, not a crash')
})