OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / issues-read-does-not-mint.test.js

102 lines · javascript ref:
// A READ of the issue tracker must not CREATE a repo.
//
// IssueRegistry.getForRepo(name) mints a tracker autobase on first reference,
// and RefReconcilerManager.ensureFor(name) runs `git init --bare` alongside it.
// The issues read path called getForRepo directly, so asking "what issues does
// repo X have?" for a repo that does not exist MADE it — permanently, and then
// served, announced and replicated it.
//
// lib/repo-name.js added name validation for the malformed class (`--help`,
// `undefined`, traversal) and states plainly that it cannot catch the rest:
// `create`, `list`, `new`, `help` and `comment` are VALID repo names,
// indistinguishable from a repo legitimately called that. All five exist as
// empty repos on the live fleet, on BOTH backends, because a read minted them.
// Its stated fix is the one here — require the repo to already exist.
//
// Writes still auto-create on purpose: filing the first issue against a repo
// whose tracker has not been opened yet is a legitimate flow.

import test from 'brittle'
import process from '#node:process'
import { mkdtempSync, existsSync, 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 makeNode (t) {
  const node = await startPearGit({
    storage: tmp('pgv3-nomint-'), uds: join(tmp('pgv3-nomint-s-'), 'svc.sock'),
    http: { port: 0 }, swarm: false, autoImportSSH: false
  })
  t.teardown(async () => { try { await node.close() } catch {} })
  const client = await Client.discover({ uds: node.svc.uds.path })
  t.teardown(async () => { try { await client.close() } catch {} })
  return { node, client }
}

const barePath = (node, name) => join(node.svc._reposDir, name + '.git')

test('reading issues for a nonexistent repo does NOT create it', async (t) => {
  const { node, client } = await makeNode(t)

  // `create` is the exact shape that slipped past name validation and became a
  // real repo on the fleet: a perfectly valid name that was actually a mistyped
  // subcommand.
  for (const method of ['list-issues', 'issue-counts']) {
    let err = null
    try { await client.call(method, { repo: 'create' }) } catch (e) { err = e }
    t.ok(err, method + ' refused a nonexistent repo')
    t.ok(/not found/.test(err.message || ''), method + ': ' + err.message)
  }

  t.absent(existsSync(barePath(node, 'create')), 'no bare repo was minted on disk')
  t.absent(node.svc.issueRegistry.listRepos().includes('create'), 'no tracker was minted')
})

test('get-issue on a nonexistent repo does not create it either', async (t) => {
  const { node, client } = await makeNode(t)
  let err = null
  try { await client.call('get-issue', { repo: 'help', id: 1 }) } catch (e) { err = e }
  t.ok(err && /not found/.test(err.message), 'refused: ' + (err && err.message))
  t.absent(existsSync(barePath(node, 'help')), 'no bare repo minted')
})

test('reads still work for a repo that DOES exist', async (t) => {
  const { node, client } = await makeNode(t)
  mkdirSync(node.svc._reposDir, { recursive: true })
  execFileSync('git', ['init', '--bare', '-q', barePath(node, 'real-repo')])

  const counts = await client.call('issue-counts', { repo: 'real-repo' })
  t.ok(counts, 'issue-counts answers for a real repo')
  const list = await client.call('list-issues', { repo: 'real-repo' })
  t.ok(list, 'list-issues answers for a real repo')
})

test('a WRITE still auto-creates — that flow is deliberate', async (t) => {
  const { node, client } = await makeNode(t)

  // Filing the first issue against a repo whose tracker has not been opened is
  // legitimate; only reads were minting by accident.
  const created = await client.call('create-issue', {
    repo: 'brand-new', title: 'first', author: 'tester'
  })
  t.ok(created, 'create-issue succeeded against an unopened repo')
  t.ok(node.svc.issueRegistry.listRepos().includes('brand-new'), 'and its tracker was created')
})

test('malformed names are still rejected before anything is touched', async (t) => {
  const { node, client } = await makeNode(t)
  for (const bad of ['--help', 'undefined', '../etc']) {
    let err = null
    try { await client.call('list-issues', { repo: bad }) } catch (e) { err = e }
    t.ok(err, JSON.stringify(bad) + ' rejected')
    t.absent(existsSync(barePath(node, bad)), JSON.stringify(bad) + ' minted nothing')
  }
})