OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / tests / test-admin-channel.mjs

304 lines · javascript ref:
/**
 * test-admin-channel.mjs — MetadataBase Autobase sync tests
 *
 * Tests:
 *  1. meta.key returns independent identity key info
 *  2. meta.get reads from Autobase after set
 *  3. room.create with roomType creates correct type
 *  4. room.create without roomType creates Default room
 *  5. meta.set with null field removes it
 *  6. rapid meta.set calls don't corrupt (last-write-wins)
 *  7. meta.set for multiple rooms stores separately
 *  8. meta.get for unknown roomId returns empty object
 *  9. meta.get is consistent after multiple operations
 * 10. meta.invite returns 64-char hex pairing key
 * 11. meta.join rejects invalid key, self-join is no-op
 *
 * Requires: keet-daemon running on /tmp/keet-daemon.sock
 * Usage: bare tests/test-admin-channel.mjs
 */

import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import b4a from 'b4a'

const SOCKET = '/tmp/keet-daemon.sock'
let rpcId = 0
let passed = 0
let failed = 0

function connect () {
  const pipe = new Pipe(SOCKET)
  const framed = new FramedStream(pipe)
  const pending = new Map()

  framed.on('data', (buf) => {
    try {
      const msg = JSON.parse(b4a.toString(buf))
      const cb = pending.get(msg.id)
      if (cb) {
        pending.delete(msg.id)
        if (msg.ok !== undefined && msg.data !== undefined) msg.result = msg.data
        cb(msg)
      }
    } catch {}
  })

  function call (method, params = {}, timeoutMs = 15000) {
    return new Promise((resolve, reject) => {
      const id = ++rpcId
      const timeout = setTimeout(() => {
        pending.delete(id)
        reject(new Error(`RPC timeout: ${method}`))
      }, timeoutMs)
      pending.set(id, (msg) => {
        clearTimeout(timeout)
        if (msg.error) reject(new Error(msg.error.message || msg.error))
        else resolve(msg.result)
      })
      const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params })
      framed.write(b4a.from(payload))
    })
  }

  function close () { framed.destroy(); pipe.destroy() }
  return { call, close }
}

function assert (condition, label) {
  if (condition) { console.log(`  OK: ${label}`); passed++ }
  else { console.log(`  FAIL: ${label}`); failed++ }
}

async function run () {
  console.log('=== MetadataBase Autobase Sync Tests ===\n')
  const client = connect()

  // Find or create dedicated test room (never pollute real rooms)
  const rooms = await client.call('rooms')
  let testRoom = rooms.find(r => r.title === '_test-suite')
  if (!testRoom) {
    await client.call('room.create', { title: '_test-suite' })
    await new Promise(r => setTimeout(r, 2000))
    const updated = await client.call('rooms')
    testRoom = updated.find(r => r.title === '_test-suite')
  }
  if (!testRoom) {
    console.log('FAIL: Could not find or create _test-suite room')
    client.close()
    Bare.exit(1)
  }
  const roomId = testRoom.roomId || testRoom.id
  // Second test room: pick any other room
  const chatRooms = rooms.filter(r => {
    const id = r.roomId || r.id
    return id !== roomId
  })
  const secondRoom = chatRooms.length > 0 ? chatRooms[0] : null
  console.log(`Test room: "${testRoom.title}" (${roomId.slice(0, 12)}...)\n`)

  const marker = 'regression-' + Date.now()

  // === T1: meta.key returns independent identity key ===
  console.log('--- T1: meta.key returns independent identity key ---')
  try {
    const keyResult = await client.call('meta.key')
    assert(keyResult.key !== null, 'meta.key returns bootstrap key')
    assert(keyResult.writable === true, 'local peer is writable')
    assert(keyResult.localKey !== null, 'has local writer key')
    assert(typeof keyResult.key === 'string' && keyResult.key.length === 64, 'key is 64-char hex')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T2: meta.get reads Autobase after set ===
  console.log('\n--- T2: meta.get reads Autobase after set ---')
  try {
    await client.call('meta.set', {
      roomId,
      metadata: { topics: [marker], discoverable: true, category: 'test' }
    })
    const meta = await client.call('meta.get', { roomId })
    assert(meta.topics?.includes(marker), 'Autobase has marker topic')
    assert(meta.discoverable === true, 'discoverable preserved')
    assert(meta.category === 'test', 'category preserved')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T3: room.create with roomType ===
  console.log('\n--- T3: room.create with roomType=1 (Broadcast) ---')
  let broadcastId = null
  try {
    const result = await client.call('room.create', { title: '_test-broadcast-' + Date.now(), roomType: '1' })
    broadcastId = typeof result === 'string' ? result : (result?.roomId || result?.id || result)
    assert(broadcastId !== null, 'room.create returned roomId')

    // keet-core internal: digest.settings.roomType 2 = Broadcast
    const info = await client.call('room', { roomId: broadcastId })
    const rt = info.digest?.settings?.roomType
    assert(rt === 2, `roomType is Broadcast (digest=${rt})`)
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T4: room.create without roomType ===
  console.log('\n--- T4: room.create without roomType (Default) ---')
  let defaultId = null
  try {
    const result = await client.call('room.create', { title: '_test-default-' + Date.now() })
    defaultId = typeof result === 'string' ? result : (result?.roomId || result?.id || result)
    assert(defaultId !== null, 'room.create returned roomId')

    // keet-core internal: digest.settings.roomType 1 = Default
    const info = await client.call('room', { roomId: defaultId })
    const rt = info.digest?.settings?.roomType
    assert(rt === 1, `roomType is Default (digest=${rt})`)
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T5: meta.set null removes field ===
  console.log('\n--- T5: meta.set null removes field ---')
  try {
    await client.call('meta.set', { roomId, metadata: { category: null } })
    const meta = await client.call('meta.get', { roomId })
    assert(meta.category === undefined, 'null field removed')
    assert(meta.topics?.includes(marker), 'other fields preserved')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T6: rapid meta.set (last-write-wins) ===
  console.log('\n--- T6: rapid meta.set last-write-wins ---')
  try {
    // Fire 3 rapid sets
    await client.call('meta.set', { roomId, metadata: { topics: ['first'] } })
    await client.call('meta.set', { roomId, metadata: { topics: ['second'] } })
    await client.call('meta.set', { roomId, metadata: { topics: ['third'] } })

    const meta = await client.call('meta.get', { roomId })
    assert(meta.topics?.includes('third'), 'last write wins')
    assert(!meta.topics?.includes('first'), 'earlier writes overwritten')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T7: meta.set for multiple rooms ===
  console.log('\n--- T7: multiple rooms store separately ---')
  if (secondRoom) {
    const secondId = secondRoom.roomId || secondRoom.id
    try {
      const marker2 = 'multi-' + Date.now()
      await client.call('meta.set', { roomId, metadata: { topics: [marker2] } })
      await client.call('meta.set', { roomId: secondId, metadata: { topics: [marker2 + '-b'] } })

      const meta1 = await client.call('meta.get', { roomId })
      const meta2 = await client.call('meta.get', { roomId: secondId })
      assert(meta1.topics?.includes(marker2), 'room 1 has its marker')
      assert(meta2.topics?.includes(marker2 + '-b'), 'room 2 has its marker')
      assert(!meta1.topics?.includes(marker2 + '-b'), 'room 1 does not have room 2 marker')
    } catch (err) {
      console.log(`  FAIL: ${err.message}`)
      failed++
    }
  } else {
    console.log('  SKIP: only 1 chat room available')
  }

  // === T8: meta.get for unknown roomId ===
  console.log('\n--- T8: meta.get unknown roomId returns {} ---')
  try {
    const meta = await client.call('meta.get', { roomId: 'nonexistent-room-id-12345' })
    assert(typeof meta === 'object', 'returns object')
    assert(Object.keys(meta).length === 0, 'empty object for unknown room')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T9: meta.get consistency ===
  console.log('\n--- T9: meta.get consistent after operations ---')
  try {
    const meta = await client.call('meta.get', { roomId })
    assert(meta.topics !== undefined, 'has topics after all operations')
    assert(typeof meta === 'object', 'returns object not array')
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T10: meta.invite returns 64-char hex key ===
  console.log('\n--- T10: meta.invite returns pairing key ---')
  let inviteKey = null
  try {
    const result = await client.call('meta.invite')
    assert(typeof result === 'object', 'meta.invite returns object')
    assert(typeof result.key === 'string', 'has key field')
    assert(result.key.length === 64, 'key is 64-char hex')
    inviteKey = result.key
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === T11: meta.join validation and self-join ===
  console.log('\n--- T11: meta.join validation + self-join ---')
  try {
    // Invalid key should fail
    let joinFailed = false
    try {
      await client.call('meta.join', { key: 'too-short' })
    } catch {
      joinFailed = true
    }
    assert(joinFailed, 'meta.join rejects invalid key')

    // Join own key should be a no-op (already a writer)
    if (inviteKey) {
      const result = await client.call('meta.join', { key: inviteKey })
      assert(typeof result === 'object', 'self-join returns object')
      assert(result.key === inviteKey || result.ok !== undefined, 'self-join does not error')
    }
  } catch (err) {
    console.log(`  FAIL: ${err.message}`)
    failed++
  }

  // === Cleanup ===
  console.log('\n--- Cleanup ---')
  try {
    await client.call('meta.set', { roomId, metadata: { topics: [], discoverable: false, category: null } })
    if (secondRoom) {
      const secondId = secondRoom.roomId || secondRoom.id
      await client.call('meta.set', { roomId: secondId, metadata: { topics: [], discoverable: false } })
    }
    // Leave test rooms (broadcast + default)
    if (broadcastId) {
      try { await client.call('room.leave', { roomId: broadcastId }) } catch {}
    }
    if (defaultId) {
      try { await client.call('room.leave', { roomId: defaultId }) } catch {}
    }
    console.log('  OK: Cleaned up test metadata + rooms')
  } catch (err) {
    console.log(`  WARN: Cleanup failed: ${err.message}`)
  }

  // Summary
  console.log(`\n=== RESULT: ${passed} passed, ${failed} failed ===`)
  client.close()
  setTimeout(() => Bare.exit(failed > 0 ? 1 : 0), 500)
}

run().catch(err => {
  console.log('FATAL:', err.message)
  Bare.exit(1)
})