OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / retire-junk-names.test.js

99 lines · javascript ref:
// Malformed repos must be RETIRABLE.
//
// assertValidRepoName exists to stop malformed names being CREATED
// (lib/repo-name.js). Applying it unchanged to the retire path meant a
// malformed repo could never be REMOVED — the live fleet carries real repos
// called `--help` (rejected: starts with "-") and `undefined` (rejected:
// stringified sentinel), and both were untombstonable. The guard against
// making them also protected them.
//
// Retirement is cleanup, so a malformed name is accepted when that repo
// actually exists here — and refused when it does not, so nobody can pollute
// the replicated event log with tombstones for names that were never repos.
// Traversal and control characters stay refused either way: the name reaches
// the filesystem, and "it exists" is not a reason to relax containment.

import test from 'brittle'
import process from '#node:process'
import { mkdtempSync, mkdirSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { Client } from 'hyper-svc'

import { startPearGit } from '../index.js'
import { execFileSync } from '../lib/_compat-exec.js'

process.env.PGV3_USE_RAW_IDENTITY = '1'
const tmp = (p) => mkdtempSync(join(tmpdir(), p))

async function setup (t, junkNames = []) {
  const node = await startPearGit({
    storage: tmp('pgv3-retire-'), uds: join(tmp('pgv3-retire-s-'), 'svc.sock'),
    http: { port: 0 }, swarm: false, autoImportSSH: false
  })
  t.teardown(async () => { try { await node.close() } catch {} })

  // Materialize the junk exactly as it exists on the fleet: a real bare repo
  // whose NAME the validator rejects.
  mkdirSync(node.svc._reposDir, { recursive: true })
  for (const n of junkNames) {
    execFileSync('git', ['init', '--bare', '-q', join(node.svc._reposDir, n + '.git')])
  }

  const client = await Client.discover({ uds: node.svc.uds.path })
  t.teardown(async () => { try { await client.close() } catch {} })
  return { node, client }
}

test('a malformed repo that EXISTS can be retired', async (t) => {
  const { client } = await setup(t, ['--help', 'undefined'])

  for (const name of ['--help', 'undefined']) {
    const res = await client.call('retire-repo', { name })
    t.is(res.retired, name, name + ' tombstoned')
    t.ok(/NOT removed/.test(res.note || ''), 'and the response says bytes were not removed')
  }

  const { retired } = await client.call('list-retired-repos', {})
  t.ok(retired.includes('--help') && retired.includes('undefined'),
    'both appear in the replicated tombstone set')
})

test('a malformed name that does NOT exist is still refused', async (t) => {
  const { client } = await setup(t)
  // Otherwise anyone could append tombstones for arbitrary junk.
  await t.exception(
    client.call('retire-repo', { name: '--never-existed' }),
    /malformed names may only be retired when the repo actually exists/,
    'refused with the reason'
  )
})

test('traversal and control characters are refused even if they exist', async (t) => {
  const { client } = await setup(t)
  for (const bad of ['../etc', 'a/b', 'xy']) {
    await t.exception(client.call('retire-repo', { name: bad }), /unsafe repo name/,
      JSON.stringify(bad) + ' refused')
  }
})

test('well-formed names are unaffected', async (t) => {
  const { client } = await setup(t, ['normal-repo'])
  const res = await client.call('retire-repo', { name: 'normal-repo' })
  t.is(res.retired, 'normal-repo', 'ordinary retire still works')
})

test('unretire accepts the same names, so a junk retire is reversible', async (t) => {
  const { client } = await setup(t, ['--help'])
  await client.call('retire-repo', { name: '--help' })
  const back = await client.call('unretire-repo', { name: '--help' })
  t.is(back.unretired, '--help', 'un-retirable too — a mistake is not permanent')
  const { retired } = await client.call('list-retired-repos', {})
  t.absent(retired.includes('--help'), 'and it left the tombstone set')
})

test('retire still refuses a missing name outright', async (t) => {
  const { client } = await setup(t)
  await t.exception(client.call('retire-repo', {}), /requires \{ name \}/)
})