/* * Famedly Matrix SDK * Copyright (C) 2019, 2020 Famedly GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:matrix/matrix.dart'; import 'fake_client.dart'; Future updateLastEvent(Event event) { if (event.room.client.getRoomById(event.room.id) == null) { event.room.client.rooms.add(event.room); } return event.room.client.handleSync( SyncUpdate( rooms: RoomsUpdate( join: { event.room.id: JoinedRoomUpdate( timeline: TimelineUpdate( events: [event], ), ), }, ), nextBatch: '', ), ); } void main() { late Client matrix; late Room room; /// All Tests related to the Event group('Room', () { Logs().level = Level.error; test('Login', () async { matrix = await getClient(); await matrix.abortSync(); }); test('Create from json', () async { final id = '!localpart:server.abc'; final membership = Membership.join; final notificationCount = 2; final highlightCount = 1; final heroes = [ '@alice:matrix.org', '@bob:example.com', '@charley:example.org', ]; room = Room( client: matrix, id: id, membership: membership, highlightCount: highlightCount, notificationCount: notificationCount, prev_batch: '', summary: RoomSummary.fromJson({ 'm.joined_member_count': 2, 'm.invited_member_count': 2, 'm.heroes': heroes, }), roomAccountData: { 'com.test.foo': BasicEvent( type: 'com.test.foo', content: {'foo': 'bar'}, ), 'm.fully_read': BasicEvent( type: 'm.fully_read', content: {'event_id': '\$event_id:example.com'}, ), }, ); room.setState( Event( room: room, eventId: '143273582443PhrSn:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), senderId: '@example:example.org', type: 'm.room.join_rules', unsigned: {'age': 1234}, content: {'join_rule': 'public'}, stateKey: '', ), ); room.setState( Event( room: room, eventId: '143273582443PhrSnY:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), senderId: matrix.userID!, type: 'm.room.member', unsigned: {'age': 1234}, content: {'membership': 'join', 'displayname': 'YOU'}, stateKey: matrix.userID!, ), ); room.setState( Event( room: room, eventId: '143273582443PhrSnA:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), senderId: '@alice:matrix.org', type: 'm.room.member', unsigned: {'age': 1234}, content: {'membership': 'join', 'displayname': 'Alice Margatroid'}, stateKey: '@alice:matrix.org', ), ); room.setState( Event( room: room, eventId: '143273582443PhrSnB:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), senderId: '@bob:example.com', type: 'm.room.member', unsigned: {'age': 1234}, content: {'membership': 'invite', 'displayname': 'Bob'}, stateKey: '@bob:example.com', ), ); room.setState( Event( room: room, eventId: '143273582443PhrSnC:example.org', originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), senderId: '@charley:example.org', type: 'm.room.member', unsigned: {'age': 1234}, content: {'membership': 'invite', 'displayname': 'Charley'}, stateKey: '@charley:example.org', ), ); final heroUsers = await room.loadHeroUsers(); expect(heroUsers.length, 3); expect(room.id, id); expect(room.membership, membership); expect(room.notificationCount, notificationCount); expect(room.highlightCount, highlightCount); expect(room.summary.mJoinedMemberCount, notificationCount); expect(room.summary.mInvitedMemberCount, 2); expect(room.summary.mHeroes, heroes); expect( room.getLocalizedDisplayname(), 'Group with Alice Margatroid, Bob, Charley', ); expect( room.getState('m.room.join_rules')?.content['join_rule'], 'public', ); expect(room.roomAccountData['com.test.foo']?.content['foo'], 'bar'); expect(room.fullyRead, '\$event_id:example.com'); room.setState( Event( senderId: '@test:example.com', type: 'm.room.canonical_alias', room: room, eventId: '123', content: {'alias': '#testalias:example.com'}, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.getLocalizedDisplayname(), 'testalias'); expect(room.canonicalAlias, '#testalias:example.com'); room.setState( Event( senderId: '@test:example.com', type: 'm.room.name', room: room, eventId: '123', content: {'name': 'testname'}, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.getLocalizedDisplayname(), 'testname'); expect(room.topic, ''); room.setState( Event( senderId: '@test:example.com', type: 'm.room.topic', room: room, eventId: '123', content: {'topic': 'testtopic'}, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.topic, 'testtopic'); expect(room.avatar, null); room.setState( Event( senderId: '@test:example.com', type: 'm.room.avatar', room: room, eventId: '123', content: {'url': 'mxc://testurl'}, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.avatar.toString(), 'mxc://testurl'); expect(room.pinnedEventIds, []); room.setState( Event( senderId: '@test:example.com', type: 'm.room.pinned_events', room: room, eventId: '123', content: { 'pinned': ['1234'], }, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.pinnedEventIds.first, '1234'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.message', room: room, eventId: '12345', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'abc'}, ), ); expect(room.lastEvent?.eventId, '12345'); expect(room.lastEvent?.body, 'abc'); expect(room.latestEventReceivedTime, room.lastEvent?.originServerTs); }); test('lastEvent is set properly', () async { await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.message', room: room, eventId: '0', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'meow'}, ), ); expect(room.lastEvent?.body, 'meow'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '1', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'cd'}, ), ); expect(room.hasNewMessages, true); expect(room.isUnreadOrInvited, false); expect(room.lastEvent?.body, 'cd'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '2', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'cdc'}, ), ); expect(room.lastEvent?.body, 'cdc'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '3', originServerTs: DateTime.now(), content: { 'm.new_content': {'msgtype': 'm.text', 'body': 'test ok'}, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '1'}, 'msgtype': 'm.text', 'body': '* test ok', }, ), ); expect(room.lastEvent?.body, 'cdc'); // because we edited the "cd" message // update even when status is sending // https://github.com/famedly/matrix-dart-sdk/pull/1852#issuecomment-2173019450 await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '4', originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'edited cdc', 'm.new_content': {'msgtype': 'm.text', 'body': 'edited cdc'}, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'}, }, unsigned: { messageSendingStatusKey: EventStatus.sending.intValue, 'transaction_id': 'messageID', }, status: EventStatus.sending, ), ); expect(room.lastEvent?.body, 'edited cdc'); // change because sent await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '5', originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'edited cdc just because', 'm.new_content': { 'msgtype': 'm.text', 'body': 'edited cdc just because', }, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'}, }, unsigned: { messageSendingStatusKey: EventStatus.sent.intValue, 'transaction_id': 'messageID', }, status: EventStatus.sent, ), ); expect(room.lastEvent?.body, 'edited cdc just because'); expect(room.lastEvent?.status, EventStatus.sent); expect(room.lastEvent?.eventId, '5'); // Status update on edits working? await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '6', unsigned: { 'transaction_id': '4', messageSendingStatusKey: EventStatus.sent.intValue, }, originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'edited cdc is back!', 'm.new_content': { 'msgtype': 'm.text', 'body': 'edited cdc is back!', }, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'}, }, stateKey: '', status: EventStatus.sent, ), ); expect(room.lastEvent?.eventId, '6'); expect(room.lastEvent?.body, 'edited cdc is back!'); expect(room.lastEvent?.status, EventStatus.sent); // Are reactions coming through? await updateLastEvent( Event( senderId: '@test:example.com', type: EventTypes.Reaction, room: room, eventId: 'lastEvent_reactions_dont_matter', originServerTs: DateTime.now(), content: { 'm.relates_to': { 'rel_type': RelationshipTypes.reaction, 'event_id': '1234', 'key': ':-)', }, }, ), ); expect(room.lastEvent?.eventId, '6'); expect(room.lastEvent?.body, 'edited cdc is back!'); expect(room.lastEvent?.status, EventStatus.sent); }); test('lastEvent when edited and deleted', () async { await room.client.handleSync( SyncUpdate( rooms: RoomsUpdate( join: { room.id: JoinedRoomUpdate( timeline: TimelineUpdate( events: [ Event( content: { 'body': 'A', 'm.mentions': {}, 'msgtype': 'm.text', }, type: 'm.room.message', eventId: 'testLastEventBeforeEdit', senderId: '@test:example.com', originServerTs: DateTime.now(), room: room, ), ], ), ), }, ), nextBatch: '', ), ); expect(room.lastEvent?.eventId, 'testLastEventBeforeEdit'); expect(room.lastEvent?.body, 'A'); await room.client.handleSync( SyncUpdate( rooms: RoomsUpdate( join: { room.id: JoinedRoomUpdate( timeline: TimelineUpdate( events: [ Event( content: { 'body': ' * A-edited', 'm.mentions': {}, 'm.new_content': { 'body': 'A-edited', 'm.mentions': {}, 'msgtype': 'm.text', }, 'm.relates_to': { 'event_id': 'testLastEventBeforeEdit', 'rel_type': 'm.replace', }, 'msgtype': 'm.text', }, type: 'm.room.message', eventId: 'testLastEventAfterEdit', senderId: '@test:example.com', originServerTs: DateTime.now(), room: room, ), ], ), ), }, ), nextBatch: '', ), ); expect(room.lastEvent?.eventId, 'testLastEventAfterEdit'); expect(room.lastEvent?.body, ' * A-edited'); await room.client.handleSync( SyncUpdate( rooms: RoomsUpdate( join: { room.id: JoinedRoomUpdate( timeline: TimelineUpdate( events: [ Event( content: {'redacts': 'testLastEventBeforeEdit'}, type: 'm.room.redaction', eventId: 'testLastEventAfterEditAndDelete', senderId: '@test:example.com', originServerTs: DateTime.now(), room: room, ), ], ), ), }, ), nextBatch: '', ), ); // We do not delete the last edited event. Manually set the last event to original redacted event. expect(room.lastEvent?.eventId, 'testLastEventBeforeEdit'); expect(room.lastEvent?.body, 'Redacted'); }); test('lastEvent when reply parent edited', () async { await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '5', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'A'}, ), ); expect(room.lastEvent?.body, 'A'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '6', originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'B', 'm.relates_to': {'rel_type': 'm.in_reply_to', 'event_id': '5'}, }, ), ); expect(room.lastEvent?.body, 'B'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '7', originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'edited A', 'm.new_content': {'msgtype': 'm.text', 'body': 'edited A'}, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '5'}, }, ), ); expect(room.lastEvent?.body, 'B'); }); test('lastEvent with deleted message', () async { await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '8', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'AA'}, stateKey: '', ), ); expect(room.lastEvent?.body, 'AA'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '9', originServerTs: DateTime.now(), content: { 'msgtype': 'm.text', 'body': 'B', 'm.relates_to': {'rel_type': 'm.in_reply_to', 'event_id': '8'}, }, stateKey: '', ), ); expect(room.lastEvent?.body, 'B'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '10', originServerTs: DateTime.now(), content: { 'type': 'm.room.redaction', 'content': {'reason': 'test'}, 'sender': '@test:example.com', 'redacts': '9', 'event_id': '10', 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, stateKey: '', ), ); expect(room.lastEvent?.eventId, '10'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '11', originServerTs: DateTime.now(), content: {'msgtype': 'm.text', 'body': 'BB'}, stateKey: '', ), ); expect(room.lastEvent?.body, 'BB'); await updateLastEvent( Event( senderId: '@test:example.com', type: 'm.room.encrypted', room: room, eventId: '12', originServerTs: DateTime.now(), content: { 'm.new_content': {'msgtype': 'm.text', 'body': 'BBB'}, 'm.relates_to': {'rel_type': 'm.replace', 'event_id': '11'}, 'msgtype': 'm.text', 'body': '* BBB', }, stateKey: '', ), ); expect(room.lastEvent?.body, '* BBB'); room.setState( Event( senderId: '@test:example.com', type: 'm.room.name', room: room, eventId: '12', originServerTs: DateTime.now(), content: {'body': 'brainfarts'}, stateKey: '', ), ); expect(room.lastEvent?.body, '* BBB'); }); test('sendReadMarker', () async { await room.setReadMarker('§1234:fakeServer.notExisting'); }); test('requestParticipants', () async { final oldParticipants = room.getParticipants(); expect(oldParticipants.length, 4); room.summary.mJoinedMemberCount = 5; final fetchedParticipants = await room.requestParticipants( const [Membership.join, Membership.invite, Membership.knock], true, true, ); final newParticipants = room.getParticipants(); expect(oldParticipants.length < newParticipants.length, true); expect(fetchedParticipants.length, newParticipants.length); }); test('calcEncryptionHealthState', () async { expect( await room.calcEncryptionHealthState(), EncryptionHealthState.unverifiedDevices, ); }); test('getEventByID', () async { final event = await room.getEventById('1234'); expect(event?.eventId, '143273582443PhrSn:example.org'); }); test('setName', () async { final eventId = await room.setName('Testname'); expect(eventId, '42'); }); test('setDescription', () async { final eventId = await room.setDescription('Testname'); expect(eventId, '42'); }); test('kick', () async { await room.kick('Testname'); }); test('ban', () async { await room.ban('Testname'); }); test('unban', () async { await room.unban('Testname'); }); test('PowerLevels', () async { room.setState( Event( senderId: '@test:example.com', type: 'm.room.power_levels', room: room, eventId: '123', content: { 'ban': 50, 'events': {'m.room.name': 100, 'm.room.power_levels': 100}, 'events_default': 0, 'invite': 50, 'kick': 50, 'notifications': {'room': 20}, 'redact': 50, 'state_default': 50, 'users': {'@test:fakeServer.notExisting': 100}, 'users_default': 10, }, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.ownPowerLevel, 100); expect(room.getPowerLevelByUserId(matrix.userID!), room.ownPowerLevel); expect(room.getPowerLevelByUserId('@nouser:example.com'), 10); expect(room.ownPowerLevel, 100); expect(room.canBan, true); expect(room.canInvite, true); expect(room.canKick, true); expect(room.canRedact, true); expect(room.canSendDefaultMessages, true); expect(room.canChangePowerLevel, true); expect(room.canSendEvent('m.room.name'), true); expect(room.canSendEvent('m.room.power_levels'), true); expect(room.canSendEvent('m.room.member'), true); room.setState( Event( senderId: '@test:example.com', type: 'm.room.power_levels', room: room, eventId: '123', content: { 'ban': 50, 'events': { 'm.room.name': 'lannaForcedMeToTestThis', 'm.room.power_levels': 100, }, 'events_default': 0, 'invite': 50, 'kick': 50, 'notifications': {'room': 20}, 'redact': 50, 'state_default': 60, 'users': {'@test:fakeServer.notExisting': 100}, 'users_default': 10, }, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.powerForChangingStateEvent('m.room.name'), 60); expect(room.powerForChangingStateEvent('m.room.power_levels'), 100); expect(room.powerForChangingStateEvent('m.room.nonExisting'), 60); room.setState( Event( senderId: '@test:example.com', type: 'm.room.power_levels', room: room, eventId: '123abc', content: { 'ban': 50, 'events': {'m.room.name': 0, 'm.room.power_levels': 100}, 'events_default': 0, 'invite': 50, 'kick': 50, 'notifications': {'room': 20}, 'redact': 50, 'state_default': 50, 'users': {}, 'users_default': 0, }, originServerTs: DateTime.now(), stateKey: '', ), ); expect(room.ownPowerLevel, 0); expect(room.canBan, false); expect(room.canInvite, false); expect(room.canKick, false); expect(room.canRedact, false); expect(room.canSendDefaultMessages, true); expect(room.canChangePowerLevel, false); expect(room.canChangeStateEvent('m.room.name'), true); expect(room.canChangeStateEvent('m.room.power_levels'), false); expect(room.canChangeStateEvent('m.room.member'), false); expect(room.canSendEvent('m.room.message'), true); final resp = await room.setPower('@test:fakeServer.notExisting', 0); expect(resp, '42'); }); test('invite', () async { await room.invite('Testname'); }); test('setPower', () async { final powerLevelMap = room.getState(EventTypes.RoomPowerLevels, '')!.content.copy(); // Request to fake api does not update anything: await room.setPower('@bob:fakeServer.notExisting', 100); // Original power level map has not changed: expect( powerLevelMap, room.getState(EventTypes.RoomPowerLevels, '')!.content.copy(), ); }); test('getParticipants', () async { var userList = room.getParticipants(); expect(userList.length, 5); // add new user room.setState( Event( senderId: '@alice:test.abc', type: 'm.room.member', room: room, eventId: '12345', originServerTs: DateTime.now(), content: {'displayname': 'alice'}, stateKey: '@alice:test.abc', ), ); userList = room.getParticipants(); expect(userList.length, 6); expect(userList[5].displayName, 'alice'); }); test('addToDirectChat', () async { await room.addToDirectChat('Testname'); }); test('getTimeline', () async { final timeline = await room.getTimeline(); expect(timeline.events.length, 17); }); test('isFederated', () { expect(room.isFederated, true); room.setState( StrippedStateEvent( type: EventTypes.RoomCreate, content: {'m.federate': false}, senderId: room.client.userID!, stateKey: '', ), ); expect(room.isFederated, false); }); test('getUserByMXID', () async { final List called = []; final List called2 = []; // ignore: deprecated_member_use_from_same_package final subscription = room.onUpdate.stream.listen((i) { called.add(i); }); final subscription2 = room.client.onRoomState.stream.listen((i) { called2.add(i.roomId); }); FakeMatrixApi.calledEndpoints.clear(); final user = await room.requestUser('@getme:example.com'); expect(FakeMatrixApi.calledEndpoints.keys, [ '/client/v3/rooms/!localpart%3Aserver.abc/state/m.room.member/%40getme%3Aexample.com', ]); expect(user?.stateKey, '@getme:example.com'); expect(user?.calcDisplayname(), 'You got me'); expect(user?.membership, Membership.knock); // Yield for the onUpdate await Future.delayed( Duration( milliseconds: 1, ), ); expect(called.length, 1); expect(called2.length, 1); FakeMatrixApi.calledEndpoints.clear(); final user2 = await room.requestUser('@getmeprofile:example.com'); expect(FakeMatrixApi.calledEndpoints.keys, [ '/client/v3/rooms/!localpart%3Aserver.abc/state/m.room.member/%40getmeprofile%3Aexample.com', '/client/v3/profile/%40getmeprofile%3Aexample.com', ]); expect(user2?.stateKey, '@getmeprofile:example.com'); expect(user2?.calcDisplayname(), 'You got me (profile)'); expect(user2?.membership, Membership.leave); // Yield for the onUpdate await Future.delayed( Duration( milliseconds: 1, ), ); expect(called.length, 2); expect(called2.length, 2); FakeMatrixApi.calledEndpoints.clear(); final userAgain = await room.requestUser('@getme:example.com'); expect(FakeMatrixApi.calledEndpoints.keys, []); expect(userAgain?.stateKey, '@getme:example.com'); expect(userAgain?.calcDisplayname(), 'You got me'); expect(userAgain?.membership, Membership.knock); // Yield for the onUpdate await Future.delayed( Duration( milliseconds: 1, ), ); expect(called.length, 2, reason: 'onUpdate should not have been called.'); expect( called2.length, 2, reason: 'onRoomState should not have been called.', ); FakeMatrixApi.calledEndpoints.clear(); final user3 = await room.requestUser('@getmeempty:example.com'); expect(FakeMatrixApi.calledEndpoints.keys, [ '/client/v3/rooms/!localpart%3Aserver.abc/state/m.room.member/%40getmeempty%3Aexample.com', '/client/v3/profile/%40getmeempty%3Aexample.com', ]); expect(user3?.stateKey, '@getmeempty:example.com'); expect(user3?.calcDisplayname(), 'You got me (empty)'); expect(user3?.membership, Membership.leave); // Yield for the onUpdate await Future.delayed( Duration( milliseconds: 1, ), ); expect(called.length, 3); expect(called2.length, 3); await subscription.cancel(); await subscription2.cancel(); }); test('setAvatar', () async { final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg'); final dynamic resp = await room.setAvatar(testFile); expect(resp, 'YUwRidLecu:example.com'); }); test('sendEvent', () async { final dynamic resp = await room.sendEvent( {'msgtype': 'm.text', 'body': 'hello world'}, txid: 'testtxid', ); expect(resp?.startsWith('\$event'), true); }); test('sendEvent', () async { FakeMatrixApi.calledEndpoints.clear(); final dynamic resp = await room.sendTextEvent('Hello world', txid: 'testtxid'); expect(resp?.startsWith('\$event'), true); final entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); final content = json.decode(entry.value.first); expect(content, { 'body': 'Hello world', 'msgtype': 'm.text', }); }); test('send edit', () async { FakeMatrixApi.calledEndpoints.clear(); final dynamic resp = await room.sendTextEvent( 'Hello world', txid: 'testtxid', editEventId: '\$otherEvent', ); expect(resp?.startsWith('\$event'), true); final entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); final content = json.decode(entry.value.first); expect(content, { 'body': '* Hello world', 'msgtype': 'm.text', 'm.new_content': { 'body': 'Hello world', 'msgtype': 'm.text', }, 'm.relates_to': { 'event_id': '\$otherEvent', 'rel_type': 'm.replace', }, }); }); test('send reply', () async { var event = Event.fromJson( { 'event_id': '\$replyEvent', 'content': { 'body': 'Blah', 'msgtype': 'm.text', }, 'type': 'm.room.message', 'sender': '@alice:example.org', }, room, ); FakeMatrixApi.calledEndpoints.clear(); var resp = await room.sendTextEvent( 'Hello world', txid: 'testtxid', inReplyTo: event, ); expect(resp?.startsWith('\$event'), true); var entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); var content = json.decode(entry.value.first); expect(content, { 'body': '> <@alice:example.org> Blah\n\nHello world', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Blah
Hello world', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }); event = Event.fromJson( { 'event_id': '\$replyEvent', 'content': { 'body': 'Blah\nbeep', 'msgtype': 'm.text', }, 'type': 'm.room.message', 'sender': '@alice:example.org', }, room, ); FakeMatrixApi.calledEndpoints.clear(); resp = await room.sendTextEvent( 'Hello world\nfox', txid: 'testtxid', inReplyTo: event, ); expect(resp?.startsWith('\$event'), true); entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); content = json.decode(entry.value.first); expect(content, { 'body': '> <@alice:example.org> Blah\n> beep\n\nHello world\nfox', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
<b>Blah</b>
beep
Hello world
fox', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }); event = Event.fromJson( { 'event_id': '\$replyEvent', 'content': { 'format': 'org.matrix.custom.html', 'formatted_body': 'heyameow', 'body': 'plaintext meow', 'msgtype': 'm.text', }, 'type': 'm.room.message', 'sender': '@alice:example.org', }, room, ); FakeMatrixApi.calledEndpoints.clear(); resp = await room.sendTextEvent( 'Hello world', txid: 'testtxid', inReplyTo: event, ); expect(resp?.startsWith('\$event'), true); entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); content = json.decode(entry.value.first); expect(content, { 'body': '> <@alice:example.org> plaintext meow\n\nHello world', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
meow
Hello world', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }); event = Event.fromJson( { 'event_id': '\$replyEvent', 'content': { 'body': 'Hey @room', 'msgtype': 'm.text', }, 'type': 'm.room.message', 'sender': '@alice:example.org', }, room, ); FakeMatrixApi.calledEndpoints.clear(); resp = await room.sendTextEvent( 'Hello world', txid: 'testtxid', inReplyTo: event, ); expect(resp?.startsWith('\$event'), true); entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); content = json.decode(entry.value.first); expect(content, { 'body': '> <@alice:example.org> Hey @\u{200b}room\n\nHello world', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hey @room
Hello world', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }); // Reply to a reply event = Event.fromJson( { 'event_id': '\$replyEvent', 'content': { 'body': '> <@alice:example.org> Hey\n\nHello world', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hey
Hello world', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }, 'type': 'm.room.message', 'sender': '@alice:example.org', }, room, ); FakeMatrixApi.calledEndpoints.clear(); resp = await room.sendTextEvent('Fox', txid: 'testtxid', inReplyTo: event); expect(resp?.startsWith('\$event'), true); entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); content = json.decode(entry.value.first); expect(content, { 'body': '> <@alice:example.org> Hello world\n\nFox', 'msgtype': 'm.text', 'format': 'org.matrix.custom.html', 'formatted_body': '
In reply to @alice:example.org
Hello world
Fox', 'm.relates_to': { 'm.in_reply_to': { 'event_id': '\$replyEvent', }, }, }); }); test('send reaction', () async { FakeMatrixApi.calledEndpoints.clear(); final dynamic resp = await room.sendReaction('\$otherEvent', '🦊', txid: 'testtxid'); expect(resp?.startsWith('\$event'), true); final entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.reaction/')); final content = json.decode(entry.value.first); expect(content, { 'm.relates_to': { 'event_id': '\$otherEvent', 'rel_type': 'm.annotation', 'key': '🦊', }, }); }); test('send location', () async { FakeMatrixApi.calledEndpoints.clear(); final body = 'Middle of the ocean'; final geoUri = 'geo:0.0,0.0'; final dynamic resp = await room.sendLocation(body, geoUri, txid: 'testtxid'); expect(resp?.startsWith('\$event'), true); final entry = FakeMatrixApi.calledEndpoints.entries .firstWhere((p) => p.key.contains('/send/m.room.message/')); final content = json.decode(entry.value.first); expect(content, { 'msgtype': 'm.location', 'body': body, 'geo_uri': geoUri, }); }); // Not working because there is no real file to test it... /*test('sendImageEvent', () async { final File testFile = File.fromUri(Uri.parse("fake/path/file.jpeg")); final dynamic resp = await room.sendImageEvent(testFile, txid: "testtxid"); expect(resp, "42"); });*/ test('sendFileEvent', () async { final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg'); final resp = await room.sendFileEvent(testFile, txid: 'testtxid'); expect(resp.toString(), '\$event10'); }); test('pushRuleState', () async { expect(room.pushRuleState, PushRuleState.mentionsOnly); ((matrix.accountData['m.push_rules']?.content['global'] as Map)['override'] as List) .add( ((matrix.accountData['m.push_rules']?.content['global'] as Map)['room'] as List)[0], ); expect(room.pushRuleState, PushRuleState.dontNotify); }); test('enableEncryption', () async { await room.enableEncryption(); }); test('Enable encryption', () async { room.setState( Event( senderId: '@alice:test.abc', type: 'm.room.encryption', room: room, eventId: '12345', originServerTs: DateTime.now(), content: { 'algorithm': AlgorithmTypes.megolmV1AesSha2, 'rotation_period_ms': 604800000, 'rotation_period_msgs': 100, }, stateKey: '', ), ); expect(room.encrypted, true); expect(room.encryptionAlgorithm, AlgorithmTypes.megolmV1AesSha2); }); test('setPushRuleState', () async { await room.setPushRuleState(PushRuleState.notify); await room.setPushRuleState(PushRuleState.dontNotify); await room.setPushRuleState(PushRuleState.mentionsOnly); await room.setPushRuleState(PushRuleState.notify); }); test('Test tag methods', () async { await room.addTag(TagType.favourite, order: 0.1); await room.removeTag(TagType.favourite); expect(room.isFavourite, false); room.roomAccountData['m.tag'] = BasicEvent.fromJson({ 'content': { 'tags': { 'm.favourite': {'order': 0.1}, 'm.wrong': {'order': 0.2}, }, }, 'type': 'm.tag', }); expect(room.tags.length, 1); expect(room.tags[TagType.favourite]?.order, 0.1); expect(room.isFavourite, true); await room.setFavourite(false); }); test('Test marked unread room', () async { await room.markUnread(true); await room.markUnread(false); expect(room.markedUnread, false); room.roomAccountData['m.marked_unread'] = BasicEvent.fromJson({ 'content': {'unread': true}, 'type': 'm.marked_unread', }); expect(room.markedUnread, true); }); test('joinRules', () async { expect(room.canChangeJoinRules, false); expect(room.joinRules, JoinRules.public); room.setState( Event.fromJson( { 'content': {'join_rule': 'invite'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'state_key': '', 'type': 'm.room.join_rules', 'unsigned': {'age': 1234}, }, room, ), ); expect(room.joinRules, JoinRules.invite); await room.setJoinRules(JoinRules.invite); }); test('guestAccess', () async { expect(room.canChangeGuestAccess, false); expect(room.guestAccess, GuestAccess.forbidden); room.setState( Event.fromJson( { 'content': {'guest_access': 'can_join'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'state_key': '', 'type': 'm.room.guest_access', 'unsigned': {'age': 1234}, }, room, ), ); expect(room.guestAccess, GuestAccess.canJoin); await room.setGuestAccess(GuestAccess.canJoin); }); test('historyVisibility', () async { expect(room.canChangeHistoryVisibility, false); expect(room.historyVisibility, null); room.setState( Event.fromJson( { 'content': {'history_visibility': 'shared'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'state_key': '', 'type': 'm.room.history_visibility', 'unsigned': {'age': 1234}, }, room, ), ); expect(room.historyVisibility, HistoryVisibility.shared); await room.setHistoryVisibility(HistoryVisibility.joined); }); test('setState', () async { // not set non-state-events try { room.setState( Event.fromJson( { 'content': {'history_visibility': 'shared'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': 'm.custom', 'unsigned': {'age': 1234}, }, room, ), ); } catch (_) {} expect(room.getState('m.custom') != null, false); // set state events room.setState( Event.fromJson( { 'content': {'history_visibility': 'shared'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'state_key': '', 'type': 'm.custom', 'unsigned': {'age': 1234}, }, room, ), ); expect(room.getState('m.custom') != null, true); // sets messages as state events try { room.setState( Event.fromJson( { 'content': {'history_visibility': 'shared'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': 'm.room.message', 'unsigned': {'age': 1234}, }, room, ), ); } catch (_) {} expect(room.getState('m.room.message') == null, true); }); test('Widgets', () { expect(room.widgets.isEmpty, true); room.states['m.widget'] = { 'test': Event.fromJson( { 'content': { 'creatorUserId': '@rxl881:matrix.org', 'data': {'title': 'Bridges Dashboard', 'dateRange': '1y'}, 'id': 'grafana_@rxl881:matrix.org_1514573757015', 'name': 'Grafana', 'type': 'm.grafana', 'url': 'https://matrix.org/grafana/whatever', 'waitForIframeLoad': true, }, 'room_id': '!foo:bar', 'event_id': '\$15104760642668662QICBu:matrix.org', 'sender': '@rxl881:matrix.org', 'state_key': 'test', 'origin_server_ts': 1432735824653, 'type': 'm.widget', }, room, ), }; expect(room.widgets.length, 1); room.states['m.widget'] = { 'test2': Event.fromJson( { 'content': { 'creatorUserId': '@rxl881:matrix.org', 'data': {'title': 'Bridges Dashboard', 'dateRange': '1y'}, 'id': 'grafana_@rxl881:matrix.org_1514573757016', 'type': 'm.grafana', 'url': 'https://matrix.org/grafana/whatever', 'waitForIframeLoad': true, }, 'room_id': '!foo:bar', 'event_id': '\$15104760642668663QICBu:matrix.org', 'sender': '@rxl881:matrix.org', 'state_key': 'test2', 'origin_server_ts': 1432735824653, 'type': 'm.widget', }, room, ), }; expect(room.widgets.length, 1); room.states['m.widget'] = { 'test3': Event.fromJson( { 'content': { 'creatorUserId': '@rxl881:matrix.org', 'data': {'title': 'Bridges Dashboard', 'dateRange': '1y'}, 'type': 'm.grafana', 'waitForIframeLoad': true, }, 'room_id': '!foo:bar', 'event_id': '\$15104760642668662QICBu:matrix.org', 'sender': '@rxl881:matrix.org', 'state_key': 'test3', 'origin_server_ts': 1432735824655, 'type': 'm.widget', }, room, ), }; expect(room.widgets.length, 0); }); test('Spaces', () async { expect(room.isSpace, false); room.states['m.room.create'] = { '': Event.fromJson( { 'content': {'type': 'm.space'}, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': 'm.room.create', 'unsigned': {'age': 1234}, 'state_key': '', }, room, ), }; expect(room.isSpace, true); expect(room.spaceParents.isEmpty, true); room.states[EventTypes.SpaceParent] = { '!1234:example.invalid': Event.fromJson( { 'content': { 'via': ['example.invalid'], }, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': EventTypes.SpaceParent, 'unsigned': {'age': 1234}, 'state_key': '!1234:example.invalid', }, room, ), }; expect(room.spaceParents.length, 1); expect(room.spaceChildren.isEmpty, true); room.states[EventTypes.SpaceChild] = { '!b:example.invalid': Event.fromJson( { 'content': { 'via': ['example.invalid'], 'order': 'b', }, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': EventTypes.SpaceChild, 'unsigned': {'age': 1234}, 'state_key': '!b:example.invalid', }, room, ), '!c:example.invalid': Event.fromJson( { 'content': { 'via': ['example.invalid'], 'order': 'c', }, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': EventTypes.SpaceChild, 'unsigned': {'age': 1234}, 'state_key': '!c:example.invalid', }, room, ), '!noorder:example.invalid': Event.fromJson( { 'content': { 'via': ['example.invalid'], }, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': EventTypes.SpaceChild, 'unsigned': {'age': 1234}, 'state_key': '!noorder:example.invalid', }, room, ), '!a:example.invalid': Event.fromJson( { 'content': { 'via': ['example.invalid'], 'order': 'a', }, 'event_id': '\$143273582443PhrSn:example.org', 'origin_server_ts': 1432735824653, 'room_id': '!jEsUZKDJdhlrceRyVU:example.org', 'sender': '@example:example.org', 'type': EventTypes.SpaceChild, 'unsigned': {'age': 1234}, 'state_key': '!a:example.invalid', }, room, ), }; expect(room.spaceChildren.length, 4); expect(room.spaceChildren[0].roomId, '!a:example.invalid'); expect(room.spaceChildren[1].roomId, '!b:example.invalid'); expect(room.spaceChildren[2].roomId, '!c:example.invalid'); expect(room.spaceChildren[3].roomId, '!noorder:example.invalid'); // TODO: Implement a more generic fake api /*await room.setSpaceChild( '!jEsUZKDJdhlrceRyVU:example.org', via: ['example.invalid'], order: '5', suggested: true, ); await room.removeSpaceChild('!1234:example.invalid');*/ }); test('getMention', () async { expect(room.getMention('@invalid'), null); expect(room.getMention('@[Alice Margatroid]'), '@alice:matrix.org'); expect(room.getMention('@[Alice Margatroid]#1667'), '@alice:matrix.org'); }); test('inviteLink', () async { // ensure we don't rerequest members room.summary.mJoinedMemberCount = 3; var matrixToLink = await room.matrixToInviteLink(); expect( matrixToLink.toString(), 'https://matrix.to/#/%23testalias%3Aexample.com', ); room.setState( Event( senderId: '@test:example.com', type: 'm.room.canonical_alias', room: room, eventId: '123', content: {'alias': ''}, originServerTs: DateTime.now(), stateKey: '', ), ); matrixToLink = await room.matrixToInviteLink(); expect( matrixToLink.toString(), 'https://matrix.to/#/!localpart%3Aserver.abc?via=fakeServer.notExisting&via=matrix.org&via=example.org', ); }); test('cancelSend because EventTooLarge in postLoaded room', () async { expect(room.partial, false); await room.sendTextEvent( 'older_event', txid: 'older_event', ); // check if persisted in db final sentEventFromDB = await matrix.database.getEventById('older_event', room); expect(sentEventFromDB?.eventId, 'older_event'); Room? roomFromDB; roomFromDB = await matrix.database.getSingleRoom(matrix, room.id); expect(roomFromDB?.lastEvent?.eventId, 'older_event'); expect(room.lastEvent?.body, 'older_event'); // status will be error here because fakeapi // but enough for us to fallback after calling cancelSend below expect(room.lastEvent?.eventId, 'older_event'); try { await room.sendTextEvent( txid: 'event_too_large', // data just bigger than maxBodySize base64Encode( List.generate(60001, (i) => Random().nextInt(256)), ), ); } catch (e) { expect(e.runtimeType, EventTooLarge); expect(room.lastEvent?.eventId, 'event_too_large'); expect(room.lastEvent?.status, EventStatus.error); roomFromDB = await matrix.database.getSingleRoom(matrix, room.id); expect(roomFromDB?.lastEvent?.eventId, 'event_too_large'); // force null because except would have caught it anyway await room.lastEvent?.cancelSend(); } // work in postLoaded room expect(room.lastEvent?.eventId, 'event_too_large'); expect( await room.lastEvent?.calcLocalizedBody(MatrixDefaultLocalizations()), 'Cancelled sending message', ); // check if persisted in db final lastEventFromDB = await matrix.database.getEventById('event_too_large', room); // null here because cancelSend removes event. expect(lastEventFromDB, null); roomFromDB = await matrix.database.getSingleRoom(matrix, room.id); expect(roomFromDB?.partial, true); expect(roomFromDB?.lastEvent?.eventId, 'event_too_large'); expect( await room.lastEvent?.calcLocalizedBody(MatrixDefaultLocalizations()), 'Cancelled sending message', ); roomFromDB = await matrix.database.getSingleRoom(matrix, room.id); await roomFromDB?.postLoad(); expect(roomFromDB?.partial, false); expect(roomFromDB?.lastEvent?.eventId, 'event_too_large'); expect( await room.lastEvent?.calcLocalizedBody(MatrixDefaultLocalizations()), 'Cancelled sending message', ); }); test('logout', () async { await matrix.logout(); }); }); }