import 'fake-indexeddb/auto'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { xml } from '@xmpp/client'; import { OmemoDB } from '../src/crypto/omemo-db.ts'; import { acceptDeviceListEvent, beginDeviceConversation, cachedDeviceList, DEVICE_LIST_MAX_AGE, invalidateDeviceLists, SEND_BURST_GAP } from '../src/crypto/omemo.ts'; import { buildDeviceList, deviceListEvent } from '../src/crypto/omemo-wire.ts'; const ns = 'eu.siacs.conversations.axolotl'; const result = (ids: number[]) => xml('pubsub', {xmlns:'http://jabber.org/protocol/pubsub'}, xml('items', {node:ns+'.devicelist'}, xml('item', {id:'current'}, buildDeviceList(ids)))); test('device events require the direct bare publisher and a valid legacy payload, including explicit empty', () => { const event = (from: string, list = buildDeviceList([42])) => xml('message', {from}, xml('event', {xmlns:'http://jabber.org/protocol/pubsub#event'}, xml('items', {node:ns+'.devicelist'}, xml('item', {id:'current'}, list)))); assert.deepEqual(deviceListEvent(event('peer@example.org')), {peer:'peer@example.org', ids:[42]}); assert.deepEqual(deviceListEvent(event('peer@example.org', buildDeviceList([]))), {peer:'peer@example.org', ids:[]}); for (const from of ['', 'peer@example.org/resource', 'example.org', 'other peer@example.org']) assert.equal(deviceListEvent(event(from)), undefined); for (const id of ['0', '-1', 'NaN', '2147483648']) assert.equal(deviceListEvent(event('peer@example.org', xml('list', {xmlns:ns}, xml('device', {id})))), undefined); assert.equal(deviceListEvent(event('peer@example.org', xml('list', {xmlns:'wrong'}))), undefined); assert.equal(deviceListEvent(xml('message', {from:'forwarder@example.org'}, xml('forwarded', null, event('peer@example.org')))), undefined); }); test('freshness, non-roster bursts, reconnect, coalescing, failures and account isolation', async (t) => { const db = await OmemoDB.open('freshness@example.org'); const other = await OmemoDB.open('another@example.org'); const peer = 'non-roster@example.org'; let calls = 0, ids = [42], fail = false; const xmpp: any = {iqCaller:{get:async () => { calls++; if(fail) throw Error('offline'); return result(ids); }}}; let now = Date.now(); t.mock.method(Date, 'now', () => now); try { assert.deepEqual(await cachedDeviceList(xmpp, db, peer), [42]); assert.equal(calls, 1); await Promise.all(Array.from({length:8}, () => cachedDeviceList(xmpp, db, peer, {forSend:true}))); assert.equal(calls, 2); // first send refreshes even a UI-warmed non-roster list await cachedDeviceList(xmpp, db, peer, {forSend:true}); assert.equal(calls, 2); now += SEND_BURST_GAP; ids = [42,43]; assert.deepEqual(await cachedDeviceList(xmpp, db, peer, {forSend:true}), ids); assert.equal(calls, 3); beginDeviceConversation(xmpp, db, peer); await cachedDeviceList(xmpp, db, peer, {forSend:true}); assert.equal(calls, 4); await acceptDeviceListEvent(xmpp, db, peer, [43], true); assert.deepEqual(await cachedDeviceList(xmpp, db, peer), [43]); assert.equal(calls, 4); now += DEVICE_LIST_MAX_AGE; ids = [44]; assert.deepEqual(await cachedDeviceList(xmpp, db, peer), [44]); assert.equal(calls, 5); invalidateDeviceLists(xmpp); ids = [45]; assert.deepEqual(await cachedDeviceList(xmpp, db, peer), [45]); assert.equal(calls, 6); await cachedDeviceList(xmpp, other, peer); assert.equal(calls, 7); fail = true; await assert.rejects(cachedDeviceList(xmpp, db, peer, {force:true,forSend:true}), /offline/); assert.deepEqual((await db.getCachedDevices(peer))?.deviceIds, [45]); fail = false; await acceptDeviceListEvent(xmpp, db, peer, [], true); await assert.rejects(cachedDeviceList(xmpp, db, peer, {forSend:true}), /no current encryption devices/); const malformed: any = {iqCaller:{get:async () => xml('pubsub', null, xml('items', null, xml('item')))}}; await assert.rejects(cachedDeviceList(malformed, db, peer), /Invalid/); assert.deepEqual((await db.getCachedDevices(peer))?.deviceIds, []); } finally { db.close(); other.close(); } }); test('an event wins against an older concurrent fetch without discarding ratchets', async () => { const db = await OmemoDB.open('racing@example.org'); let resolve!: (value: any) => void; const xmpp: any = {iqCaller:{get:() => new Promise(r => {resolve = r;})}}; try { await db.putSession('peer:42:legacy', 'keep ratchet'); const fetch = cachedDeviceList(xmpp, db, 'peer@example.org'); while (!resolve) await new Promise(r => setTimeout(r, 1)); await acceptDeviceListEvent(xmpp, db, 'peer@example.org', [43], true); resolve(result([42])); assert.deepEqual(await fetch, [43]); assert.equal(await db.getSession('peer:42:legacy'), 'keep ratchet'); } finally {db.close();} });