import 'package:flutter_test/flutter_test.dart'; import 'package:matrix/matrix.dart'; import 'package:warp/matrix/sliding_sync.dart'; const me = '@alice:example.org'; Map member(String userId, String membership, {int ts = 1}) => { 'type': 'm.room.member', 'state_key': userId, 'sender': userId, 'event_id': '\$m$ts$membership', 'origin_server_ts': ts, 'content': {'membership': membership}, }; Map message(String body, {int ts = 2}) => { 'type': 'm.room.message', 'sender': '@bob:example.org', 'event_id': '\$msg$ts', 'origin_server_ts': ts, 'content': {'msgtype': 'm.text', 'body': body}, }; void main() { group('SlidingToken', () { test('no token is a new connection', () { final t = SlidingToken.parse(null); expect(t.pos, isNull); expect(t.toDeviceSince, isNull); }); test('a /sync token is a new connection', () { expect(SlidingToken.parse('s72594_4483_1934').pos, isNull); }); test('round trip', () { final t = SlidingToken.parse(const SlidingToken('42', 'td7').toString()); expect(t.pos, '42'); expect(t.toDeviceSince, 'td7'); }); test('a token without a to-device position', () { final t = SlidingToken.parse(const SlidingToken('3', null).toString()); expect(t.pos, '3'); expect(t.toDeviceSince, isNull); }); }); group('slidingRequest', () { test('asks for both lists over the window, with the extensions', () { final body = slidingRequest(window: 20, toDeviceSince: 'x', connId: 'c'); final lists = body['lists'] as Map; expect((lists['rooms'] as Map)['ranges'], [ [0, 19], ]); expect(((lists['dms'] as Map)['filters'] as Map)['is_dm'], true); final ext = body['extensions'] as Map; expect((ext['to_device'] as Map)['since'], 'x'); expect((ext['e2ee'] as Map)['enabled'], true); }); }); group('slidingToSyncJson', () { test('a joined room becomes a join with state, timeline and counts', () { final json = slidingToSyncJson( { 'pos': '1', 'rooms': { '!a:x': { 'required_state': [member(me, 'join')], 'timeline': [message('hi')], 'prev_batch': 'p1', 'limited': true, 'joined_count': 2, 'notification_count': 3, 'highlight_count': 1, 'heroes': [ {'user_id': '@bob:example.org', 'displayname': 'Bob'}, ], }, }, }, userId: me, nextBatch: 'nb', ); final sync = SyncUpdate.fromJson(json); expect(sync.nextBatch, 'nb'); final room = sync.rooms!.join!['!a:x']!; expect(room.state!.single.type, 'm.room.member'); expect(room.timeline!.events!.single.content['body'], 'hi'); expect(room.timeline!.limited, true); expect(room.timeline!.prevBatch, 'p1'); expect(room.unreadNotifications!.notificationCount, 3); expect(room.unreadNotifications!.highlightCount, 1); expect(room.summary!.mJoinedMemberCount, 2); expect(room.summary!.mHeroes, ['@bob:example.org']); }); test('invite and knock state', () { final json = slidingToSyncJson( { 'rooms': { '!i:x': { 'invite_state': [ { 'type': 'm.room.member', 'state_key': me, 'sender': '@bob:example.org', 'content': {'membership': 'invite'}, }, ], }, '!k:x': {'knock_state': []}, }, }, userId: me, nextBatch: 'nb', ); final sync = SyncUpdate.fromJson(json); expect(sync.rooms!.invite!['!i:x']!.inviteState!.single.stateKey, me); expect(sync.rooms!.knock!.containsKey('!k:x'), true); expect(sync.rooms!.join, isNull); }); test('a room the user left is a leave', () { final json = slidingToSyncJson( { 'rooms': { '!l:x': { 'required_state': [member(me, 'join', ts: 1)], 'timeline': [member(me, 'leave', ts: 5)], }, }, }, userId: me, nextBatch: 'nb', ); final sync = SyncUpdate.fromJson(json); expect(sync.rooms!.leave!.containsKey('!l:x'), true); expect(sync.rooms!.join, isNull); }); test('a rejoin after a leave is a join', () { final json = slidingToSyncJson( { 'rooms': { '!r:x': { 'timeline': [ member(me, 'leave', ts: 1), member(me, 'join', ts: 2), ], }, }, }, userId: me, nextBatch: 'nb', ); expect(SyncUpdate.fromJson(json).rooms!.join!.containsKey('!r:x'), true); }); test('extensions: to-device, keys, account data, typing, receipts', () { final json = slidingToSyncJson( { 'rooms': { '!a:x': {'timeline': [message('hi')]}, }, 'extensions': { 'to_device': { 'next_batch': 't2', 'events': [ { 'type': 'm.room_key_request', 'sender': '@bob:example.org', 'content': {}, }, ], }, 'e2ee': { 'device_lists': { 'changed': ['@bob:example.org'], 'left': [], }, 'device_one_time_keys_count': {'signed_curve25519': 50}, 'device_unused_fallback_key_types': ['signed_curve25519'], }, 'account_data': { 'global': [ {'type': 'm.direct', 'content': {}}, ], 'rooms': { '!a:x': [ {'type': 'm.fully_read', 'content': {'event_id': '\$x'}}, ], }, }, 'typing': { 'rooms': { // A room with no other news still gets its typing list. '!quiet:x': { 'type': 'm.typing', 'content': { 'user_ids': ['@bob:example.org'], }, }, }, }, 'receipts': { 'rooms': { '!a:x': {'type': 'm.receipt', 'content': {}}, }, }, }, }, userId: me, nextBatch: 'nb', ); final sync = SyncUpdate.fromJson(json); expect(sync.toDevice!.single.type, 'm.room_key_request'); expect(sync.deviceLists!.changed, ['@bob:example.org']); expect(sync.deviceOneTimeKeysCount!['signed_curve25519'], 50); expect(sync.deviceUnusedFallbackKeyTypes, ['signed_curve25519']); expect(sync.accountData!.single.type, 'm.direct'); final a = sync.rooms!.join!['!a:x']!; expect(a.accountData!.single.type, 'm.fully_read'); expect(a.ephemeral!.single.type, 'm.receipt'); final quiet = sync.rooms!.join!['!quiet:x']!; expect(quiet.ephemeral!.single.type, 'm.typing'); }); test('an empty response is just a token', () { final json = slidingToSyncJson( {'pos': '9', 'rooms': {}}, userId: me, nextBatch: 'nb', ); expect(json, {'next_batch': 'nb'}); }); }); }