OverviewFilesCommitsIssuesDocsReleasesPeers

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

294 lines · javascript ref:
import test from 'brittle'

import { createRepoAccess, ensureExportMarker, PUBLIC_MARKER, DENY_RETIRED, DENY_UNMARKED, DENY_BAD_NAME, ALLOW_PUBLIC, ALLOW_LOCAL, ALLOW_READER } from '../lib/repo-access.js'

// Fake fs: a set of paths that "exist". Keeps these tests pure — no tmpdirs,
// no cleanup, and the marker path is asserted exactly.
function fakeFs (existing = []) {
  const set = new Set(existing)
  return { existsSync: (p) => set.has(p) }
}

const REPOS = '/repos'
const marker = (name) => REPOS + '/' + name + '.git/' + PUBLIC_MARKER

test('unmarked repo is denied when enforcing', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true })
  const r = access('secret')
  t.is(r.read, false, 'read denied')
  t.is(r.reason, DENY_UNMARKED)
  t.is(r.wouldDeny, true)
})

test('marked repo is allowed', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([marker('public-one')]), enforce: true })
  const r = access('public-one')
  t.is(r.read, true)
  t.is(r.reason, ALLOW_PUBLIC)
  t.is(r.wouldDeny, false)
})

test('ANONYMOUS clone of a PUBLIC repo is allowed — the point of a forge', (t) => {
  // Load-bearing, and easy to break by "tightening" the gate: a marked repo
  // must be readable with NO identity, NO session, over a remote transport.
  // Public read/clone stays open to the world; only WRITE is gated. If this
  // ever fails, the forge has stopped being a forge.
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([marker('open-source')]), enforce: true })

  const anonHttp = access('open-source', { transport: 'http' })
  t.is(anonHttp.read, true, 'anonymous HTTP clone allowed')
  t.is(anonHttp.reason, ALLOW_PUBLIC, 'allowed by the marker, not by identity')

  t.is(access('open-source', { transport: 'ssh' }).read, true, 'anonymous SSH read allowed')
  t.is(access('open-source', { transport: 'swarm' }).read, true, 'anonymous P2P read allowed')
  t.is(access('open-source', {}).read, true, 'no transport, no identity — still allowed')

  // ...and it is still only READ. Public never implies push.
  t.is(anonHttp.write, false, 'public does not grant write')
})

test('the marker path is exactly <reposDir>/<name>.git/pear-git-export-ok', (t) => {
  // Guards against a refactor that changes the layout out from under the
  // backfill script — those two MUST agree or the migration silently no-ops.
  const seen = []
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: { existsSync: (p) => { seen.push(p); return false } },
    enforce: true
  })
  access('repo-x')
  t.is(seen.length, 1)
  t.is(seen[0], '/repos/repo-x.git/pear-git-export-ok')
})

test('NOT enforcing: decision is computed but not acted on', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: false })
  const r = access('secret')
  t.is(r.read, true, 'still readable — rollout must not break the fleet')
  t.is(r.wouldDeny, true, 'but the deny is reported so it can be logged')
  t.is(r.enforced, false)
  t.is(r.reason, DENY_UNMARKED)
})

test('enforce defaults to false', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]) })
  t.is(access('anything').enforced, false, 'safe by default — opt in to enforcement')
})

test('retirement outranks a public marker', (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([marker('gone')]),
    isRetired: (n) => n === 'gone',
    enforce: true
  })
  const r = access('gone')
  t.is(r.read, false, 'retired ∧ public => denied')
  t.is(r.reason, DENY_RETIRED)
})

test('local transports read unmarked repos (host-it-privately case)', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true })
  t.is(access('secret', { transport: 'uds' }).read, true)
  t.is(access('secret', { transport: 'uds' }).reason, ALLOW_LOCAL)
  t.is(access('secret', { transport: 'local' }).read, true)
})

test('http and swarm are NOT local', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true })
  t.is(access('secret', { transport: 'http' }).read, false, 'http must not be treated as local')
  t.is(access('secret', { transport: 'swarm' }).read, false)
  t.is(access('secret', {}).read, false, 'absent transport is not local')
})

test('invalid / empty names are denied, never thrown on', (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    isValidName: (n) => /^[a-z0-9-]+$/.test(n),
    enforce: true
  })
  t.is(access('').reason, DENY_BAD_NAME)
  t.is(access(null).reason, DENY_BAD_NAME)
  t.is(access('../../etc/passwd').reason, DENY_BAD_NAME)
  t.is(access('../../etc/passwd').read, false)
})

test('an fs that throws is treated as not-public, not as a crash', (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: { existsSync: () => { throw new Error('EIO') } },
    enforce: true
  })
  const r = access('repo')
  t.is(r.read, false, 'fail closed')
  t.is(r.reason, DENY_UNMARKED)
})

test('write is never granted here — push gates keep that authority', (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([marker('pub')]), enforce: true })
  t.is(access('pub').write, false, 'this module governs READ only')
  t.is(access('pub', { transport: 'uds' }).write, false)
})

test('reposDir is required', (t) => {
  t.exception(() => createRepoAccess({}), /requires \{ reposDir \}/)
})

// ── ensureExportMarker ─────────────────────────────────────────

function writableFs (existing = []) {
  const set = new Set(existing)
  return {
    existsSync: (p) => set.has(p),
    writeFileSync: (p) => set.add(p),
    _has: (p) => set.has(p),
    _size: () => set.size
  }
}

test('ensureExportMarker writes to the SAME path the gate reads', (t) => {
  // The whole rollout hinges on these agreeing. If they diverged, creation
  // would mark repos with a file the gate ignores and every new repo would be
  // invisible the moment enforcement turned on.
  const f = writableFs()
  t.is(ensureExportMarker(REPOS, 'newrepo', f), true, 'wrote')
  t.is(f._has(marker('newrepo')), true, 'exactly the path the gate checks')

  const access = createRepoAccess({ reposDir: REPOS, fs: f, enforce: true })
  t.is(access('newrepo').read, true, 'and the gate now allows it')
})

test('ensureExportMarker is idempotent', (t) => {
  const f = writableFs([marker('r')])
  t.is(ensureExportMarker(REPOS, 'r', f), false, 'already marked — no write')
  t.is(f._size(), 1, 'nothing added')
})

test('ensureExportMarker never throws on a failing fs', (t) => {
  // A marker is not worth failing a clone or a pull over.
  const f = {
    existsSync: () => false,
    writeFileSync: () => { throw new Error('EROFS') }
  }
  t.is(ensureExportMarker(REPOS, 'r', f), false, 'reports failure, does not throw')
})

test('ensureExportMarker ignores missing arguments', (t) => {
  const f = writableFs()
  t.is(ensureExportMarker(null, 'r', f), false)
  t.is(ensureExportMarker(REPOS, null, f), false)
  t.is(f._size(), 0, 'wrote nothing')
})

// ── .async — identity-aware variant ────────────────────────────

test('async: an authorized reader may read an unmarked repo', async (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    enforce: true,
    isReader: async () => true
  })
  t.is(access('priv').read, false, 'sync form still denies')
  const r = await access.async('priv', { identity: { publicKey: 'pk' } })
  t.is(r.read, true)
  t.is(r.reason, ALLOW_READER)
  t.is(r.wouldDeny, false)
})

test('async: a non-reader is still denied', async (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true, isReader: async () => false })
  const r = await access.async('priv', {})
  t.is(r.read, false)
  t.is(r.reason, DENY_UNMARKED)
})

test('async: isReader receives the repo name and ctx', async (t) => {
  const seen = []
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    enforce: true,
    isReader: async (name, ctx) => { seen.push({ name, ctx }); return false }
  })
  await access.async('repo-y', { identity: { publicKey: 'abc' }, transport: 'http' })
  t.is(seen.length, 1)
  t.is(seen[0].name, 'repo-y')
  t.is(seen[0].ctx.identity.publicKey, 'abc')
})

test('async: retirement is NOT rescuable by identity', async (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    isRetired: () => true,
    enforce: true,
    isReader: async () => true
  })
  const r = await access.async('gone', {})
  t.is(r.read, false, 'no identity resurrects a tombstoned repo')
  t.is(r.reason, DENY_RETIRED)
})

test('async: an invalid name is NOT rescuable by identity', async (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    isValidName: (n) => /^[a-z-]+$/.test(n),
    enforce: true,
    isReader: async () => true
  })
  const r = await access.async('../../etc/passwd', {})
  t.is(r.read, false)
  t.is(r.reason, DENY_BAD_NAME)
})

test('async: isReader THROWING fails closed', async (t) => {
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([]),
    enforce: true,
    isReader: async () => { throw new Error('tracker unreachable') }
  })
  const r = await access.async('priv', {})
  t.is(r.read, false, 'an unreachable tracker must not open a private repo')
  t.is(r.reason, DENY_UNMARKED)
})

test('async: a non-boolean truthy from isReader does not grant', async (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true, isReader: async () => 'yes' })
  t.is((await access.async('priv', {})).read, false, 'strict true required')
})

test('async: no isReader configured behaves exactly like the sync form', async (t) => {
  const access = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: true })
  const r = await access.async('priv', {})
  t.is(r.read, false)
  t.is(r.reason, DENY_UNMARKED)
})

test('async: a public repo short-circuits without consulting isReader', async (t) => {
  let called = 0
  const access = createRepoAccess({
    reposDir: REPOS,
    fs: fakeFs([marker('pub')]),
    enforce: true,
    isReader: async () => { called++; return true }
  })
  const r = await access.async('pub', {})
  t.is(r.reason, ALLOW_PUBLIC)
  t.is(called, 0, 'no tracker lookup for a public repo')
})

test('async: observe mode still allows, but reports the identity outcome', async (t) => {
  const denied = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: false, isReader: async () => false })
  const r1 = await denied.async('priv', {})
  t.is(r1.read, true, 'observe never breaks the request')
  t.is(r1.wouldDeny, true, 'but reports it would have denied')

  const allowed = createRepoAccess({ reposDir: REPOS, fs: fakeFs([]), enforce: false, isReader: async () => true })
  const r2 = await allowed.async('priv', {})
  t.is(r2.wouldDeny, false, 'an authorized reader would not have been denied')
})