// repo-follow-payload-rpc.test.js — proves the source-side RPC emits
// a payload that decodes back to the source's tracker keyHex + identity.
// Also exercises the auth gate: HTTP transport refused.
//
// Mocks svc directly — no daemon spin-up needed.
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync } from '#node:fs'
import { tmpdir } from '#node:os'
import { join } from '#node:path'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { IssueRegistry } from '../lib/issue-registry.js'
import { decode, decodeB64 } from '../lib/services/follow-payload.js'
import { repoHandlers } from '../lib/handlers/repos.js'
function makeStore () {
return new Corestore(mkdtempSync(join(tmpdir(), 'pgv3-rfp-')))
}
function findHandler (handlers, name) {
return handlers.find((h) => h.name === name)
}
async function makeMockSvc (t, repoName, opts = {}) {
const store = makeStore()
const issueRegistry = new IssueRegistry(store)
if (repoName) await issueRegistry.getForRepo(repoName) // open so tracker has a keyHex
const identityKp = crypto.keyPair()
const repoEventLogKey = b4a.toString(crypto.randomBytes(32), 'hex')
const svc = {
identity: {
publicKey: identityKp.publicKey,
publicKeyHex: b4a.toString(identityKp.publicKey, 'hex')
},
bootstrapKeys: { repoEventLog: repoEventLogKey },
issueRegistry,
_sync: {
info (name) {
if (opts.unknownRepo === name) return null
if (name === repoName) {
return { name, branchCount: 1, head: 'abc123', refs: {} }
}
return null
}
}
}
t.teardown(async () => {
try { await issueRegistry.close() } catch {}
try { await store.close() } catch {}
})
return { svc, identityKp, repoEventLogKey }
}
test('get-repo returns punchUrl built from identity + name', async (t) => {
const { svc } = await makeMockSvc(t, 'foo')
const handlers = repoHandlers(svc)
const h = findHandler(handlers, 'get-repo')
t.ok(h, 'get-repo registered')
const out = await h.fn({ transport: 'uds' }, { name: 'foo' })
t.is(out.name, 'foo')
t.is(out.punchUrl, 'punch://' + svc.identity.publicKeyHex + '/foo')
t.ok(out.info, 'includes info block')
})
test('get-repo missing repo throws', async (t) => {
const { svc } = await makeMockSvc(t, 'foo', { unknownRepo: 'foo' })
const h = findHandler(repoHandlers(svc), 'get-repo')
await t.exception(h.fn({ transport: 'uds' }, { name: 'foo' }), /repo not found/)
})
test('repo-follow-payload over UDS returns decodable payload + URL', async (t) => {
const { svc } = await makeMockSvc(t, 'bar')
const h = findHandler(repoHandlers(svc), 'repo-follow-payload')
t.ok(h, 'repo-follow-payload registered')
const out = await h.fn({ transport: 'uds' }, { name: 'bar' })
t.is(out.name, 'bar')
t.is(out.sourceServerKey, svc.identity.publicKeyHex)
t.ok(out.payload, 'payload b64 present')
t.ok(out.url.startsWith('punch+follow://'), 'URL is punch+follow scheme')
// Decode and verify contents
const decoded = decodeB64(out.payload)
t.is(decoded.kind, 'repo')
t.is(decoded.sourceServerKey, svc.identity.publicKeyHex)
t.is(decoded.name, 'bar')
t.ok(decoded.issueBootstrapKey, 'tracker key in payload')
t.is(decoded.issueBootstrapKey, out.issueBootstrapKey, 'tracker key matches RPC return')
t.is(decoded.repoEventLogKey, svc.bootstrapKeys.repoEventLog)
})
test('repo-follow-payload over HTTP throws EAUTH', async (t) => {
const { svc } = await makeMockSvc(t, 'baz')
const h = findHandler(repoHandlers(svc), 'repo-follow-payload')
try {
await h.fn({ transport: 'http' }, { name: 'baz' })
t.fail('should have thrown')
} catch (err) {
t.is(err.code, 'EAUTH')
t.is(err.status, 403)
}
})
test('repo-follow-payload over swarm works (matches dump-keys model)', async (t) => {
const { svc } = await makeMockSvc(t, 'qux')
const h = findHandler(repoHandlers(svc), 'repo-follow-payload')
const out = await h.fn({ transport: 'swarm' }, { name: 'qux' })
t.is(out.name, 'qux')
})
test('repo-follow-payload without tracker still emits payload (key=null)', async (t) => {
// Build an svc whose issueRegistry refuses to open (strict bootstrap).
const store = makeStore()
const issueRegistry = new IssueRegistry(store, { requireBootstrap: true })
const identityKp = crypto.keyPair()
const svc = {
identity: {
publicKey: identityKp.publicKey,
publicKeyHex: b4a.toString(identityKp.publicKey, 'hex')
},
bootstrapKeys: { repoEventLog: null },
issueRegistry,
_sync: { info: () => ({ name: 'no-tracker' }) }
}
t.teardown(async () => {
try { await issueRegistry.close() } catch {}
try { await store.close() } catch {}
})
const h = findHandler(repoHandlers(svc), 'repo-follow-payload')
const out = await h.fn({ transport: 'uds' }, { name: 'no-tracker' })
t.is(out.issueBootstrapKey, null, 'tracker absent OK')
t.is(out.repoEventLogKey, null, 'event log absent OK')
const decoded = decode(b4a.from(out.payload, 'base64url'))
t.is(decoded.kind, 'repo')
t.is(decoded.name, 'no-tracker')
})