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('& <'), '& <')
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')
})