OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / follow-payload.test.js

157 lines · javascript ref:
// 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')
})