OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms / test / invitations.test.js

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