// Unit tests for the shared bearer-invite primitive (issue #35).
// hyper-rooms/lib/invitations.js — used by hyper-orgs `org invite` and
// hyper-issues `repo invite`.
import test from 'brittle'
import b4a from 'b4a'
import { encodeInvite, decodeInvite, newSeed, isExpired, ROLES } from '../index.js'
const KEY = 'a'.repeat(64) // 32-byte hex bootstrap key
test('round-trip preserves all fields', (t) => {
const seed = newSeed()
const invite = encodeInvite({
bootstrapKey: KEY,
kind: 'org',
name: 'alpha',
roles: ['contributor', 'maintainer'],
seed,
expiresAt: 1799999999999,
maxUses: 5
})
const d = decodeInvite(invite)
t.is(d.bootstrapKey, KEY)
t.is(d.kind, 'org')
t.is(d.name, 'alpha')
t.alike(d.roles, ['contributor', 'maintainer'])
t.is(d.seed, b4a.toString(seed, 'hex'))
t.is(d.expiresAt, 1799999999999)
t.is(d.maxUses, 5)
})
test('accepts a Buffer bootstrapKey', (t) => {
const buf = b4a.from(KEY, 'hex')
const d = decodeInvite(encodeInvite({ bootstrapKey: buf, kind: 'repo' }))
t.is(d.bootstrapKey, KEY)
t.is(d.kind, 'repo')
})
test('defaults: kind=room, roles=[reader], maxUses=1, no seed/expiry', (t) => {
const d = decodeInvite(encodeInvite({ bootstrapKey: KEY }))
t.is(d.kind, 'room')
t.alike(d.roles, ['reader'])
t.is(d.maxUses, 1)
t.is(d.seed, null)
t.is(d.expiresAt, null)
})
test('each invite is unique (nonce)', (t) => {
const a = encodeInvite({ bootstrapKey: KEY })
const b = encodeInvite({ bootstrapKey: KEY })
t.not(a, b, 'two invites for the same key differ')
})
test('encode rejects missing bootstrapKey', (t) => {
t.exception(() => encodeInvite({ kind: 'org' }), /requires bootstrapKey/)
})
test('encode rejects wrong-length bootstrapKey', (t) => {
t.exception(() => encodeInvite({ bootstrapKey: 'deadbeef' }), /32 bytes/)
})
test('encode rejects unknown role', (t) => {
t.exception(() => encodeInvite({ bootstrapKey: KEY, roles: ['superuser'] }), /unknown role/)
})
test('decode rejects non-invite / foreign / malformed strings', (t) => {
t.exception(() => decodeInvite('not-an-invite'), /valid room invite/)
t.exception(() => decodeInvite(''), /valid room invite/)
t.exception(() => decodeInvite('roominv1:!!!not-base64-json'), /malformed invite/)
})
test('isExpired honours expiresAt', (t) => {
const past = decodeInvite(encodeInvite({ bootstrapKey: KEY, expiresAt: 1000 }))
const future = decodeInvite(encodeInvite({ bootstrapKey: KEY, expiresAt: Date.now() + 1e6 }))
const none = decodeInvite(encodeInvite({ bootstrapKey: KEY }))
t.ok(isExpired(past), 'past expiry is expired')
t.absent(isExpired(future), 'future expiry is not expired')
t.absent(isExpired(none), 'no expiry never expires')
})
test('ROLES has the expected ladder', (t) => {
t.alike(ROLES, ['reader', 'contributor', 'maintainer', 'admin'])
})