OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / bridge-matrix / test / matrix-client.test.js

277 lines · javascript ref:
import test from 'brittle'
import { MatrixClient, stripHtml } from '../lib/matrix-client.js'

test('MatrixClient constructor - defaults', (t) => {
  const client = new MatrixClient()
  t.is(client.homeserver, 'matrix.org')
  t.is(client.userId, null)
  t.is(client.accessToken, null)
  t.is(client._syncing, false)
  t.is(client._txnId, 0)
})

test('MatrixClient constructor - custom options', (t) => {
  const onMsg = () => {}
  const client = new MatrixClient({
    homeserver: 'example.org',
    userId: '@bot:example.org',
    accessToken: 'syt_token123',
    onMessage: onMsg
  })
  t.is(client.homeserver, 'example.org')
  t.is(client.userId, '@bot:example.org')
  t.is(client.accessToken, 'syt_token123')
  t.is(client._onMessage, onMsg)
})

test('MatrixClient - stopSync sets flag', (t) => {
  const client = new MatrixClient()
  client._syncing = true
  client.stopSync()
  t.is(client._syncing, false)
})

test('MatrixClient - display name cache', async (t) => {
  const client = new MatrixClient()

  // Pre-populate cache
  client._displayNameCache.set('@alice:matrix.org', 'Alice')

  const name = await client.getDisplayName('@alice:matrix.org')
  t.is(name, 'Alice')
})

test('MatrixClient - event dedup', (t) => {
  const client = new MatrixClient()

  client._seenEvents.add('$event1')
  client._seenEvents.add('$event2')

  t.is(client._seenEvents.has('$event1'), true)
  t.is(client._seenEvents.has('$event3'), false)
  t.is(client._seenEvents.size, 2)
})

test('MatrixClient - event dedup prunes at limit', (t) => {
  const client = new MatrixClient()
  client._maxSeenEvents = 10

  // Fill past limit
  for (let i = 0; i < 15; i++) {
    client._seenEvents.add('$event' + i)
  }

  t.is(client._seenEvents.size, 15) // not pruned until _handleEvent
})

test('MatrixClient - _handleEvent skips own messages', async (t) => {
  let called = false
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: () => { called = true }
  })

  await client._handleEvent('!room:matrix.org', {
    type: 'm.room.message',
    sender: '@bot:matrix.org',
    event_id: '$own1',
    content: { msgtype: 'm.text', body: 'my own msg' }
  })

  t.is(called, false, 'should not relay own messages')
})

test('MatrixClient - _handleEvent relays others messages', async (t) => {
  let received = null
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: (roomId, sender, text, eventId) => {
      received = { roomId, sender, text, eventId }
    }
  })

  // Pre-populate display name cache
  client._displayNameCache.set('@alice:matrix.org', 'Alice')

  await client._handleEvent('!room:matrix.org', {
    type: 'm.room.message',
    sender: '@alice:matrix.org',
    event_id: '$msg1',
    content: { msgtype: 'm.text', body: 'Hello!' }
  })

  t.ok(received)
  t.is(received.roomId, '!room:matrix.org')
  t.is(received.sender, 'Alice')
  t.is(received.text, 'Hello!')
  t.is(received.eventId, '$msg1')
})

test('MatrixClient - _handleEvent dedup by event_id', async (t) => {
  let count = 0
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: () => { count++ }
  })
  client._displayNameCache.set('@alice:matrix.org', 'Alice')

  const event = {
    type: 'm.room.message',
    sender: '@alice:matrix.org',
    event_id: '$dup1',
    content: { msgtype: 'm.text', body: 'Hello!' }
  }

  await client._handleEvent('!room:matrix.org', event)
  await client._handleEvent('!room:matrix.org', event)
  await client._handleEvent('!room:matrix.org', event)

  t.is(count, 1, 'only relayed once despite 3 deliveries')
})

test('MatrixClient - _handleEvent ignores non-message events', async (t) => {
  let called = false
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: () => { called = true }
  })

  await client._handleEvent('!room:matrix.org', {
    type: 'm.room.member',
    sender: '@alice:matrix.org',
    event_id: '$state1',
    content: { membership: 'join' }
  })

  t.is(called, false, 'should ignore state events')
})

test('MatrixClient - _handleEvent ignores events with no content', async (t) => {
  let called = false
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: () => { called = true }
  })

  await client._handleEvent('!room:matrix.org', {
    type: 'm.room.message',
    sender: '@alice:matrix.org',
    event_id: '$nocontent1',
    content: null
  })

  t.is(called, false, 'should ignore events with no content')
})

test('MatrixClient - _handleEvent uses plain body over formatted', async (t) => {
  let received = null
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: (roomId, sender, text) => { received = text }
  })
  client._displayNameCache.set('@alice:matrix.org', 'Alice')

  await client._handleEvent('!room:matrix.org', {
    type: 'm.room.message',
    sender: '@alice:matrix.org',
    event_id: '$formatted1',
    content: {
      msgtype: 'm.text',
      body: 'Hello **world**',
      format: 'org.matrix.custom.html',
      formatted_body: 'Hello <strong>world</strong>'
    }
  })

  t.is(received, 'Hello **world**', 'uses plain body text, not HTML')
})

test('MatrixClient - txnId increments', (t) => {
  const client = new MatrixClient()
  t.is(client._txnId, 0)
  // Note: can't easily test sendMessage without a server, but
  // verify the counter is accessible
  client._txnId++
  t.is(client._txnId, 1)
})

test('stripHtml in matrix-client module', (t) => {
  t.is(stripHtml('<p>Hello</p>'), 'Hello')
  t.is(stripHtml('&amp; &lt;'), '& <')
  t.is(stripHtml('Hello<br>World'), 'Hello\nWorld')
})

test('sync response parsing - extract messages from rooms.join', async (t) => {
  // Simulate what _handleEvent would do with a sync response
  const messages = []
  const client = new MatrixClient({
    userId: '@bot:matrix.org',
    onMessage: (roomId, sender, text, eventId) => {
      messages.push({ roomId, sender, text, eventId })
    }
  })
  client._displayNameCache.set('@alice:matrix.org', 'Alice')
  client._displayNameCache.set('@bob:matrix.org', 'Bob')

  // Simulate sync response structure
  const syncResponse = {
    next_batch: 's1234',
    rooms: {
      join: {
        '!room:matrix.org': {
          timeline: {
            events: [
              {
                type: 'm.room.message',
                sender: '@alice:matrix.org',
                event_id: '$ev1',
                content: { msgtype: 'm.text', body: 'First message' }
              },
              {
                type: 'm.room.message',
                sender: '@bob:matrix.org',
                event_id: '$ev2',
                content: { msgtype: 'm.text', body: 'Second message' }
              }
            ]
          }
        }
      }
    }
  }

  // Process like the sync loop would
  const joined = syncResponse.rooms.join
  for (const [roomId, roomData] of Object.entries(joined)) {
    const events = roomData.timeline?.events || []
    for (const event of events) {
      await client._handleEvent(roomId, event)
    }
  }

  t.is(messages.length, 2)
  t.is(messages[0].sender, 'Alice')
  t.is(messages[0].text, 'First message')
  t.is(messages[1].sender, 'Bob')
  t.is(messages[1].text, 'Second message')
})

test('login response parsing', (t) => {
  // Verify the client can process a login response structure
  const client = new MatrixClient()

  // Simulate what login() does with the response
  const loginResponse = {
    user_id: '@bot:matrix.org',
    access_token: 'syt_long_token_here',
    device_id: 'ABCDEF',
    home_server: 'matrix.org'
  }

  client.accessToken = loginResponse.access_token
  client.userId = loginResponse.user_id

  t.is(client.accessToken, 'syt_long_token_here')
  t.is(client.userId, '@bot:matrix.org')
})