OverviewFilesCommitsIssuesDocsReleasesPeers

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

109 lines · javascript ref:
/**
 * Repo/resource name validation (lib/repo-name.js).
 *
 * Background: getForRepo() mints a tracker autobase on first reference and
 * the ref-reconciler `git init --bare`s alongside it, both guarded only on
 * falsiness. Two RPC handlers passed client strings straight in, producing 17
 * junk repos across the fleet (audited 2026-07-28).
 *
 * The most important test here is the last one: every one of the 138 live
 * repo names must still be accepted. A validator that rejects a real repo is
 * worse than the bug it fixes.
 */

import test from 'brittle'
import { isValidRepoName, repoNameError, assertValidRepoName } from '../lib/repo-name.js'

test('rejects the sentinels that actually happened', (t) => {
  // "undefined" is TRUTHY — this is exactly what `if (!name)` let through.
  t.absent(isValidRepoName('undefined'), 'undefined')
  t.absent(isValidRepoName('null'), 'null')
  t.absent(isValidRepoName('NaN'), 'NaN')
  t.absent(isValidRepoName('[object Object]'), 'stringified object')
  t.absent(isValidRepoName('UNDEFINED'), 'case-insensitive')
})

test('rejects flag-like names', (t) => {
  // `--help` exists on disk today and breaks any tool that interpolates a
  // repo name into a command line.
  t.absent(isValidRepoName('--help'), '--help')
  t.absent(isValidRepoName('-v'), '-v')
  t.ok(repoNameError('--help').includes('CLI flag'), 'message names the reason')
})

test('rejects path traversal and separators', (t) => {
  t.absent(isValidRepoName('.'), '.')
  t.absent(isValidRepoName('..'), '..')
  t.absent(isValidRepoName('../etc/passwd'), 'traversal')
  t.absent(isValidRepoName('a/b'), 'forward slash')
  t.absent(isValidRepoName('a\\b'), 'backslash')
  t.absent(isValidRepoName('a..b'), 'embedded ..')
})

test('rejects malformed input', (t) => {
  t.absent(isValidRepoName(''), 'empty')
  t.absent(isValidRepoName('   '), 'whitespace only')
  t.absent(isValidRepoName(' name'), 'leading space')
  t.absent(isValidRepoName('name '), 'trailing space')
  t.absent(isValidRepoName('ab'), 'null byte')
  t.absent(isValidRepoName('a\nb'), 'newline')
  t.absent(isValidRepoName('x'.repeat(101)), 'over length')
  t.absent(isValidRepoName(undefined), 'actual undefined')
  t.absent(isValidRepoName(null), 'actual null')
  t.absent(isValidRepoName(42), 'number')
  t.absent(isValidRepoName({}), 'object')
})

test('assertValidRepoName throws a typed, labelled error', (t) => {
  t.execution(() => assertValidRepoName('hyper-tps'), 'valid name passes')
  t.is(assertValidRepoName('hyper-tps'), 'hyper-tps', 'returns the name')

  try {
    assertValidRepoName('undefined', 'resource')
    t.fail('should have thrown')
  } catch (err) {
    t.is(err.code, 'EINVALID_REPO_NAME', 'typed code')
    t.is(err.status, 400, 'client error, not 500')
    t.ok(err.message.includes('resource'), 'label appears: ' + err.message)
  }
})

test('CANNOT catch mistyped subcommands — documents the limit', (t) => {
  // These are indistinguishable from a repo legitimately named that, so the
  // validator accepts them by design. They were minted because the issues
  // read path AUTO-CREATES on reference — a separate defect. If this test
  // ever starts failing, someone added a subcommand denylist; make sure that
  // was deliberate, because it would reject a real repo named "new".
  for (const n of ['create', 'list', 'new', 'help', 'comment']) {
    t.ok(isValidRepoName(n), `"${n}" is syntactically valid — validator is not the fix here`)
  }
  t.ok(isValidRepoName('7rnx'), 'org-ish names are valid too')
})

test('accepts every one of the 138 live repo names', (t) => {
  // Sampled from stortron's issue-registry-index.json (2026-07-28). The one
  // live name the validator rejects is `--help`, which is itself junk.
  const live = [
    'hyper-tps', 'stremio-vsembed', 'hyper-proto-peer', 'atproto-room',
    'atproto-identity', 'favela-web', 'atproto-store', 'hyper-overlay',
    'pear-git-v3', 'pear-git-v2', 'pear-git', 'pgh', 'pgh-sidecar',
    'pgh-runtime', 'hyper-svc', 'hyper-sync', 'hyper-rooms', 'hyper-orgs',
    'hyper-issues', 'hyper-identity', 'hyper-deploy', 'git-remote-punch',
    'favela-community', 'favela-node', '7rnx-proxy', 'mirall-cli',
    'atproto-bridge', 'atproto-pds', 'atproto-blind-relay', 'pgh-hello',
    'nvidia-rtx-godot', 'wiredove', 'hyper-web-ui', 'pear-git-deploy',
    'atproto-federation', 'favela', 'sync-test-repo', 'pushtest3',
    'hyper-wallet', 'hypernote', 'wirecore', 'keet-cli', 'godot-keet'
  ]
  for (const n of live) {
    t.ok(isValidRepoName(n), `live repo must be accepted: ${n}`)
  }

  // Shape properties the live set exercises.
  t.ok(isValidRepoName('a'), 'single char')
  t.ok(isValidRepoName('7rnx-proxy'), 'leading digit')
  t.ok(isValidRepoName('under_score'), 'underscore')
  t.ok(isValidRepoName('with.dot'), 'dot')
  t.ok(isValidRepoName('x'.repeat(100)), 'at the length limit')
})