OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / resolve-pusher.test.js

93 lines · javascript ref:
/**
 * resolvePusher: map a push's credentials (session cookie/bearer, or a PAT in
 * HTTP Basic) to the canonical userId recorded in the push-index.
 *
 * REGRESSION: the PAT branch read `.userId` straight off resolveToken's return,
 * but resolveToken resolves to { user, scope, identityId } — not a User. That
 * yielded undefined, resolvePusher returned null, and recordPush was skipped
 * for EVERY token-authenticated push. Pushes succeeded while the push-index
 * stayed empty and profiles showed "Repos pushed (0)", so HTTPS push activity
 * was invisible on the daemon. Caught in production 2026-07-29 only by reading
 * the upstream Apache access log.
 */

import test from 'brittle'
import b4a from 'b4a'

import { makeResolvePusher } from '../lib/handlers/repos.js'

const TOKEN = 'pgv3_' + 'a'.repeat(48)

// Minimal registry stub shaped like the real IdentityRegistry surface that
// resolveToken + getUserByIdentity touch.
function makeRegistry ({ tokenUserId = null, sessionUserId = null } = {}) {
  const tokenIdentityId = 'tok:' + hashOf(TOKEN)
  return {
    async getIdentity (id) {
      if (id === tokenIdentityId && tokenUserId) {
        return { identityId: id, method: 'access-token', credentials: { scope: 'write' } }
      }
      return null
    },
    async isRevoked () { return false },
    async getUserByIdentity (id) {
      if (id === tokenIdentityId && tokenUserId) return { userId: tokenUserId }
      if (sessionUserId && id === 'sessionpub') return { userId: sessionUserId }
      return null
    }
  }
}

// resolveToken hashes the secret with hypercore-crypto; mirror that so the stub
// registry answers on the same identityId the resolver will look up.
function hashOf (secret) {
  // eslint-disable-next-line no-undef
  const crypto = globalThis.__pgv3TestCrypto
  return b4a.toString(crypto.hash(b4a.from(secret, 'utf8')), 'hex')
}

function basicReq (secret) {
  return {
    headers: {
      authorization: 'Basic ' + b4a.toString(b4a.from('git:' + secret, 'utf8'), 'base64')
    }
  }
}

test('resolvePusher: PAT in HTTP Basic resolves to the owning userId', async (t) => {
  globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default

  const svc = {
    sessions: null,
    identityRegistry: makeRegistry({ tokenUserId: 'user-abc' })
  }
  const resolvePusher = makeResolvePusher(svc)

  const userId = await resolvePusher(basicReq(TOKEN))
  t.is(userId, 'user-abc', 'PAT push is attributed to the token owner')
})

test('resolvePusher: browser session still resolves (the branch that worked)', async (t) => {
  globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default

  const svc = {
    // SessionStore contract: extractIdentity does sessions.get(token).
    sessions: { get: (tok) => (tok === 'sess-token' ? { publicKey: 'sessionpub' } : null) },
    identityRegistry: makeRegistry({ sessionUserId: 'user-session' })
  }
  const resolvePusher = makeResolvePusher(svc)

  const req = { headers: { authorization: 'Bearer sess-token' } }
  t.is(await resolvePusher(req), 'user-session', 'session push attributed to its user')
})

test('resolvePusher: unknown / absent credentials stay unattributed', async (t) => {
  globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default

  const svc = { sessions: null, identityRegistry: makeRegistry({}) }
  const resolvePusher = makeResolvePusher(svc)

  t.is(await resolvePusher(basicReq(TOKEN)), null, 'unknown token → null')
  t.is(await resolvePusher({ headers: {} }), null, 'no credentials → null')
})