// follow-payload.test.js — unit tests for the discovery payload codec.
import test from 'brittle'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import {
encode, decode,
encodeB64, decodeB64,
formatUrl, parseUrl,
VERSION
} from '../lib/services/follow-payload.js'
const hex32 = () => b4a.toString(crypto.randomBytes(32), 'hex')
test('repo payload — encode/decode roundtrip with all fields', (t) => {
const input = {
kind: 'repo',
sourceServerKey: hex32(),
name: 'my-repo',
issueBootstrapKey: hex32(),
repoEventLogKey: hex32()
}
const buf = encode(input)
t.ok(b4a.isBuffer(buf), 'returns a buffer')
t.is(buf[0], VERSION, 'first byte is version')
const out = decode(buf)
t.is(out.version, VERSION)
t.is(out.kind, 'repo')
t.is(out.sourceServerKey, input.sourceServerKey.toLowerCase())
t.is(out.topicSalt, 'pear-git-v3', 'default topicSalt applied')
t.is(out.name, input.name)
t.is(out.issueBootstrapKey, input.issueBootstrapKey.toLowerCase())
t.is(out.repoEventLogKey, input.repoEventLogKey.toLowerCase())
})
test('repo payload — optional fields nullable', (t) => {
const input = {
kind: 'repo',
sourceServerKey: hex32(),
name: 'no-event-log'
// no issueBootstrapKey, no repoEventLogKey
}
const out = decode(encode(input))
t.is(out.issueBootstrapKey, null)
t.is(out.repoEventLogKey, null)
})
test('org payload — encode/decode roundtrip with all fields', (t) => {
const input = {
kind: 'org',
sourceServerKey: hex32(),
name: 'acme',
orgsBootstrapKey: hex32(),
orgEventLogKey: hex32(),
orgIssueBootstrapKey: hex32(),
repoEventLogKey: hex32()
}
const out = decode(encode(input))
t.is(out.kind, 'org')
t.is(out.sourceServerKey, input.sourceServerKey.toLowerCase())
t.is(out.name, input.name)
t.is(out.orgsBootstrapKey, input.orgsBootstrapKey.toLowerCase())
t.is(out.orgEventLogKey, input.orgEventLogKey.toLowerCase())
t.is(out.orgIssueBootstrapKey, input.orgIssueBootstrapKey.toLowerCase())
t.is(out.repoEventLogKey, input.repoEventLogKey.toLowerCase())
})
test('org payload — required fields enforced', (t) => {
t.exception(() => encode({
kind: 'org',
sourceServerKey: hex32(),
name: 'acme'
// missing orgsBootstrapKey + orgEventLogKey
}), /orgsBootstrapKey is required/)
})
test('encode — kind required', (t) => {
t.exception(() => encode({ sourceServerKey: hex32(), name: 'x' }), /kind/)
})
test('encode — name required', (t) => {
t.exception(() => encode({ kind: 'repo', sourceServerKey: hex32() }), /name is required/)
})
test('encode — sourceServerKey required and 32 bytes', (t) => {
t.exception(
() => encode({ kind: 'repo', name: 'x' }),
/sourceServerKey is required/
)
t.exception(
() => encode({ kind: 'repo', name: 'x', sourceServerKey: 'short' }),
/must be 32 bytes/
)
})
test('decode — refuses unknown version', (t) => {
const buf = encode({ kind: 'repo', sourceServerKey: hex32(), name: 'x' })
buf[0] = 99
t.exception(() => decode(buf), /unknown follow-payload version/)
})
test('decode — refuses unknown kind', (t) => {
const buf = encode({ kind: 'repo', sourceServerKey: hex32(), name: 'x' })
buf[1] = 99
t.exception(() => decode(buf), /unknown follow-payload kind/)
})
test('b64 roundtrip', (t) => {
const input = { kind: 'repo', sourceServerKey: hex32(), name: 'b64-test' }
const s = encodeB64(input)
t.ok(typeof s === 'string' && s.length > 0)
t.absent(s.includes('='), 'no base64 padding')
const out = decodeB64(s)
t.is(out.name, 'b64-test')
})
test('URL form — format + parse roundtrip (repo)', (t) => {
const srv = hex32()
const url = formatUrl({ kind: 'repo', sourceServerKey: srv, name: 'foo' })
t.is(url, 'punch+follow://' + srv + '/repo/foo')
const p = parseUrl(url)
t.is(p.kind, 'repo')
t.is(p.sourceServerKey, srv)
t.is(p.name, 'foo')
})
test('URL form — name with reserved chars URL-encoded', (t) => {
const srv = hex32()
const url = formatUrl({ kind: 'org', sourceServerKey: srv, name: 'my org/with slash' })
const p = parseUrl(url)
t.is(p.kind, 'org')
t.is(p.name, 'my org/with slash')
})
test('URL form — rejects garbage', (t) => {
t.exception(() => parseUrl('https://example.com'), /not a punch\+follow/)
t.exception(() => parseUrl(''), /not a punch\+follow/)
t.exception(() => parseUrl('punch+follow://shortkey/repo/foo'), /not a punch\+follow/)
})
test('default topicSalt is pear-git-v3', (t) => {
const out = decode(encode({ kind: 'repo', sourceServerKey: hex32(), name: 'x' }))
t.is(out.topicSalt, 'pear-git-v3')
})
test('custom topicSalt survives roundtrip', (t) => {
const out = decode(encode({
kind: 'repo',
sourceServerKey: hex32(),
name: 'x',
topicSalt: 'custom-network'
}))
t.is(out.topicSalt, 'custom-network')
})