OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / tests / test-topic-dm.mjs

297 lines · javascript ref:
/**
 * test-topic-dm.mjs — Validates topic + DM sharing routes
 *
 * Tests:
 *  1. topic.announceAll announces discoverable rooms
 *  2. topic.unannounce removes a room from DHT
 *  3. topic.announce + topic.search round-trip
 *  4. dm.share posts _hs:room-share message
 *  5. dm.shares reads shares back
 *  6. dm.share missing params throws
 *  7. meta.set + topic.announce integration (discoverable required)
 *  8. topic.announce rejects non-discoverable rooms
 *
 * Requires: keet-daemon running on /tmp/keet-daemon.sock
 * Usage: bare tests/test-topic-dm.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

// --- RPC client ---

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++
  }
}

// --- Tests ---

async function run () {
  console.log('=== Topic + DM Route Tests ===\n')

  const client = connect()

  // Get rooms
  let rooms
  try {
    rooms = await client.call('rooms')
  } catch (err) {
    console.log('FAIL: Cannot list rooms:', err.message)
    client.close()
    Bare.exit(1)
  }

  if (!rooms || rooms.length === 0) {
    console.log('SKIP: No rooms available')
    client.close()
    Bare.exit(0)
  }

  // Use dedicated test room (never pollute real 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) testRoom = rooms[0]
  const roomId = testRoom.roomId || testRoom.id
  console.log(`Test room: "${testRoom.title}" (${roomId.slice(0, 12)}...)`)

  // Find a DM room (for dm.share/dm.shares tests)
  let dmRoom = null
  try {
    const dmList = await client.call('dm.list')
    if (Array.isArray(dmList) && dmList.length > 0) {
      dmRoom = dmList[0]
    }
  } catch {}

  if (dmRoom) {
    const dmRoomId = dmRoom.roomId || dmRoom.id
    console.log(`DM room: "${dmRoom.title || dmRoom.name || 'DM'}" (${dmRoomId.slice(0, 12)}...)`)
  } else {
    console.log('INFO: No DM rooms found — DM tests will be skipped')
  }
  console.log('')

  // === TOPIC TESTS ===

  // T1: topic.announce rejects non-discoverable rooms
  console.log('--- T1: topic.announce rejects non-discoverable ---')
  try {
    // First ensure room is NOT discoverable
    await client.call('meta.set', {
      roomId,
      metadata: { discoverable: false, topics: [] }
    })
    await client.call('topic.announce', { roomId })
    console.log('  FAIL: should have thrown for non-discoverable room')
    failed++
  } catch (err) {
    assert(
      err.message.includes('not discoverable') || err.message.includes('No topics'),
      `correctly rejects: ${err.message}`
    )
  }

  // T2: meta.set + topic.announce integration
  console.log('\n--- T2: meta.set + topic.announce ---')
  try {
    // Make discoverable with topics
    await client.call('meta.set', {
      roomId,
      metadata: { discoverable: true, topics: ['test-topic-xyz'] }
    })
    const result = await client.call('topic.announce', { roomId })
    assert(result !== null, 'topic.announce returns result')
    assert(result.ok === true, 'topic.announce ok is true')
    assert(Array.isArray(result.topics), 'result has topics array')
    assert(result.topics.includes('test-topic-xyz'), 'announced topic matches')
  } catch (err) {
    console.log(`  FAIL: topic.announce threw: ${err.message}`)
    failed++
  }

  // T3: topic.search finds announced room (with DHT delay tolerance)
  console.log('\n--- T3: topic.search for announced topic ---')
  try {
    // DHT may need a moment to propagate
    const result = await client.call('topic.search', { query: 'test-topic-xyz' })
    assert(Array.isArray(result), 'topic.search returns array')
    // Note: on local DHT, announced room should be findable
    if (result.length > 0) {
      const found = result.find(r => r.roomId === roomId)
      if (found) {
        assert(true, 'found our announced room in search results')
        assert(found.title !== undefined, 'search result has title')
        assert(Array.isArray(found.topics), 'search result has topics')
      } else {
        console.log(`  INFO: ${result.length} results but our room not found (DHT propagation delay)`)
      }
    } else {
      console.log('  INFO: 0 results (DHT propagation delay — expected for local test)')
    }
  } catch (err) {
    console.log(`  FAIL: topic.search threw: ${err.message}`)
    failed++
  }

  // T4: topic.unannounce
  console.log('\n--- T4: topic.unannounce ---')
  try {
    const result = await client.call('topic.unannounce', { roomId })
    assert(result !== null, 'topic.unannounce returns result')
    assert(result.ok === true, 'topic.unannounce ok is true')
  } catch (err) {
    console.log(`  FAIL: topic.unannounce threw: ${err.message}`)
    failed++
  }

  // T5: topic.announceAll
  console.log('\n--- T5: topic.announceAll ---')
  try {
    const result = await client.call('topic.announceAll', {}, 60000)
    assert(result !== null && typeof result === 'object', 'topic.announceAll returns object')
    assert(typeof result.announced === 'number', 'result has announced count')
    console.log(`  INFO: Announced ${result.announced} discoverable rooms`)
  } catch (err) {
    console.log(`  FAIL: topic.announceAll threw: ${err.message}`)
    failed++
  }

  // === DM TESTS ===

  if (dmRoom) {
    const dmRoomId = dmRoom.roomId || dmRoom.id

    // T6: dm.share posts room-share message
    console.log('\n--- T6: dm.share ---')
    try {
      const result = await client.call('dm.share', {
        dmRoomId,
        roomId,
        title: testRoom.title || 'Test Room',
        topics: ['testing'],
        inviteToken: 'test-dm-invite-123'
      })
      assert(result !== null && result !== undefined, 'dm.share returns result')
    } catch (err) {
      console.log(`  FAIL: dm.share threw: ${err.message}`)
      failed++
    }

    // T7: dm.shares reads shares back
    console.log('\n--- T7: dm.shares ---')
    try {
      const result = await client.call('dm.shares', { dmRoomId })
      assert(Array.isArray(result), 'dm.shares returns array')
      const found = result.find(s => s.roomId === roomId)
      assert(found !== undefined, 'shared room found in results')
      if (found) {
        assert(found.title === (testRoom.title || 'Test Room'), 'share title matches')
        assert(found.inviteToken === 'test-dm-invite-123', 'share inviteToken matches')
        assert(found.sharedAt !== null, 'sharedAt timestamp set')
      }
    } catch (err) {
      console.log(`  FAIL: dm.shares threw: ${err.message}`)
      failed++
    }
  } else {
    console.log('\n--- T6-T7: SKIPPED (no DM rooms) ---')
  }

  // T8: dm.share missing params throws
  console.log('\n--- T8: dm.share missing params ---')
  try {
    await client.call('dm.share', { roomId })
    console.log('  FAIL: should have thrown for missing dmRoomId')
    failed++
  } catch (err) {
    assert(err.message.includes('dmRoomId'), 'error mentions missing dmRoomId')
  }

  // === CLEANUP ===
  console.log('\n--- Cleanup ---')
  try {
    await client.call('meta.set', {
      roomId,
      metadata: { discoverable: false, topics: [] }
    })
    await client.call('topic.unannounce', { roomId })
    console.log('  OK: Cleaned up test metadata + announcements')
  } 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)
})