From 6e211f5a810ec3d8af449a5db2fde8cde2345c33 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 13 Jul 2022 00:36:49 +0000 Subject: [PATCH] fix: race conditions in the SDK and its tests --- lib/encryption/encryption.dart | 4 +- lib/encryption/key_manager.dart | 25 +- lib/encryption/olm_manager.dart | 54 ++-- lib/encryption/utils/key_verification.dart | 46 ++- lib/src/client.dart | 39 ++- pubspec.yaml | 1 + test/client_test.dart | 147 +++++---- test/encryption/key_manager_test.dart | 18 +- test/encryption/key_verification_test.dart | 148 ++++++--- test/encryption/olm_manager_test.dart | 15 +- test/encryption/online_key_backup_test.dart | 5 +- test/fake_matrix_api.dart | 329 ++++++++++++-------- test/timeline_context_test.dart | 288 ++++++++++++----- test/timeline_test.dart | 173 ++++++---- test/user_test.dart | 1 + 15 files changed, 833 insertions(+), 460 deletions(-) diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index d04a5073..73e30406 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -417,10 +417,10 @@ class Encryption { } } - void dispose() { + Future dispose() async { _backgroundTasksRunning = false; keyManager.dispose(); - olmManager.dispose(); + await olmManager.dispose(); keyVerificationManager.dispose(); } } diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 7d4d7368..2182ae6f 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'package:matrix/encryption/utils/base64_unpadded.dart'; @@ -85,7 +86,7 @@ class KeyManager { _inboundGroupSessions.clear(); } - void setInboundGroupSession( + Future setInboundGroupSession( String roomId, String sessionId, String senderKey, @@ -98,7 +99,7 @@ class KeyManager { final senderClaimedKeys_ = senderClaimedKeys ?? {}; final allowedAtIndex_ = allowedAtIndex ?? >{}; final userId = client.userID; - if (userId == null) return; + if (userId == null) return Future.value(); if (!senderClaimedKeys_.containsKey('ed25519')) { final device = client.getUserDeviceKeysByCurve25519Key(senderKey); @@ -109,7 +110,7 @@ class KeyManager { final oldSession = getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false); if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) { - return; + return Future.value(); } late olm.InboundGroupSession inboundGroupSession; try { @@ -122,7 +123,7 @@ class KeyManager { } catch (e, s) { inboundGroupSession.free(); Logs().e('[LibOlm] Could not create new InboundGroupSession', e, s); - return; + return Future.value(); } final newSession = SessionKey( content: content, @@ -148,16 +149,16 @@ class KeyManager { } else { // we are gonna keep our old session newSession.dispose(); - return; + return Future.value(); } final roomInboundGroupSessions = _inboundGroupSessions[roomId] ??= {}; roomInboundGroupSessions[sessionId] = newSession; if (!client.isLogged() || client.encryption == null) { - return; + return Future.value(); } - client.database + final storeFuture = client.database ?.storeInboundGroupSession( roomId, sessionId, @@ -188,6 +189,8 @@ class KeyManager { // and finally broadcast the new session room.onSessionKeyReceived.add(sessionId); } + + return storeFuture ?? Future.value(); } SessionKey? getInboundGroupSession( @@ -503,7 +506,7 @@ class KeyManager { allowedAtIndex[device.userId]![device.curve25519Key!] = outboundGroupSession.message_index(); } - setInboundGroupSession( + await setInboundGroupSession( roomId, rawSession['session_id'], encryption.identityKey!, rawSession, allowedAtIndex: allowedAtIndex); final sess = OutboundGroupSession( @@ -612,7 +615,7 @@ class KeyManager { if (decrypted != null) { decrypted['session_id'] = sessionId; decrypted['room_id'] = roomId; - setInboundGroupSession( + await setInboundGroupSession( roomId, sessionId, decrypted['sender_key'], decrypted, forwarded: true, senderClaimedKeys: decrypted['sender_claimed_keys'] != null @@ -901,7 +904,7 @@ class KeyManager { .add(encryptedContent['sender_key']); // TODO: verify that the keys work to decrypt a message // alright, all checks out, let's go ahead and store this session - setInboundGroupSession( + await setInboundGroupSession( request.room.id, request.sessionId, request.senderKey, event.content, forwarded: true, senderClaimedKeys: { @@ -946,7 +949,7 @@ class KeyManager { event.content['sender_claimed_ed25519_key'] = sender_ed25519; } Logs().v('[KeyManager] Keeping room key'); - setInboundGroupSession( + await setInboundGroupSession( roomId, sessionId, encryptedContent['sender_key'], event.content, forwarded: false); } diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 439c370c..24529a4e 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:async/async.dart'; import 'package:canonical_json/canonical_json.dart'; import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; @@ -109,6 +110,7 @@ class OlmManager { } bool _uploadKeysLock = false; + CancelableOperation>? currentUpload; /// Generates new one time keys, signs everything and upload it to the server. Future uploadKeys({ @@ -211,13 +213,20 @@ class OlmManager { } // Workaround: Make sure we stop if we got logged out in the meantime. if (!client.isLogged()) return true; - final response = await client.uploadKeys( + final currentUpload = + this.currentUpload = CancelableOperation.fromFuture(client.uploadKeys( deviceKeys: uploadDeviceKeys ? MatrixDeviceKeys.fromJson(keysContent['device_keys']) : null, oneTimeKeys: signedOneTimeKeys, fallbackKeys: signedFallbackKeys, - ); + )); + final response = await currentUpload.valueOrCancellation(); + if (response == null) { + _uploadKeysLock = false; + return false; + } + // mark the OTKs as published and save that to datbase _olmAccount.mark_keys_as_published(); if (updateDatabase) { @@ -231,8 +240,8 @@ class OlmManager { } } - void handleDeviceOneTimeKeysCount( - Map? countJson, List? unusedFallbackKeyTypes) { + Future handleDeviceOneTimeKeysCount( + Map? countJson, List? unusedFallbackKeyTypes) async { if (!enabled) { return; } @@ -255,13 +264,13 @@ class OlmManager { final requestingKeysFrom = { client.userID!: {client.deviceID!: 'signed_curve25519'} }; - client.claimKeys(requestingKeysFrom, timeout: 10000); + await client.claimKeys(requestingKeysFrom, timeout: 10000); } // Only upload keys if they are less than half of the max or we have no unused fallback key if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) || !unusedFallbackKey) { - uploadKeys( + await uploadKeys( oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ? keyCount : null, @@ -293,7 +302,7 @@ class OlmManager { DateTime.now().millisecondsSinceEpoch); } - ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) { + Future _decryptToDeviceEvent(ToDeviceEvent event) async { if (event.type != EventTypes.Encrypted) { return event; } @@ -341,12 +350,12 @@ class OlmManager { throw DecryptException( DecryptException.decryptionFailed, e.toString()); } - updateSessionUsage(session); + await updateSessionUsage(session); break; } else if (type == 1) { try { plaintext = session.session!.decrypt(type, body); - updateSessionUsage(session); + await updateSessionUsage(session); break; } catch (_) { plaintext = null; @@ -363,16 +372,16 @@ class OlmManager { try { newSession.create_inbound_from(_olmAccount!, senderKey, body); _olmAccount!.remove_one_time_keys(newSession); - client.database?.updateClientKeys(pickledOlmAccount!); + await client.database?.updateClientKeys(pickledOlmAccount!); plaintext = newSession.decrypt(type, body); - runInRoot(() => storeOlmSession(OlmSession( + await runInRoot(() => storeOlmSession(OlmSession( key: client.userID!, identityKey: senderKey, sessionId: newSession.session_id(), session: newSession, lastReceived: DateTime.now(), ))); - updateSessionUsage(); + await updateSessionUsage(); } catch (e) { newSession.free(); throw DecryptException(DecryptException.decryptionFailed, e.toString()); @@ -479,7 +488,7 @@ class OlmManager { await loadFromDb(); } try { - event = _decryptToDeviceEvent(event); + event = await _decryptToDeviceEvent(event); if (event.type != EventTypes.Encrypted || !(await loadFromDb())) { return event; } @@ -566,14 +575,14 @@ class OlmManager { final encryptResult = sess.first.session!.encrypt(json.encode(fullPayload)); await storeOlmSession(sess.first); if (client.database != null) { - // ignore: unawaited_futures - runInRoot(() => client.database?.setLastSentMessageUserDeviceKey( - json.encode({ - 'type': type, - 'content': payload, - }), - device.userId, - device.deviceId!)); + await runInRoot( + () async => client.database?.setLastSentMessageUserDeviceKey( + json.encode({ + 'type': type, + 'content': payload, + }), + device.userId, + device.deviceId!)); } final encryptedBody = { 'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, @@ -651,7 +660,8 @@ class OlmManager { } } - void dispose() { + Future dispose() async { + await currentUpload?.cancel(); for (final sessions in olmSessions.values) { for (final sess in sessions) { sess.dispose(); diff --git a/lib/encryption/utils/key_verification.dart b/lib/encryption/utils/key_verification.dart index d21e9736..9f07cc39 100644 --- a/lib/encryption/utils/key_verification.dart +++ b/lib/encryption/utils/key_verification.dart @@ -221,6 +221,16 @@ class KeyVerification { await cancel('m.unknown_method'); return; } + + // ensure we have the other sides keys + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await client.updateUserDeviceKeys(additionalUsers: {userId}); + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await cancel('im.fluffychat.unknown_device'); + return; + } + } + setState(KeyVerificationState.askAccept); break; case 'm.key.verification.ready': @@ -248,6 +258,16 @@ class KeyVerification { await cancel('m.unknown_method'); return; } + + // ensure we have the other sides keys + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await client.updateUserDeviceKeys(additionalUsers: {userId}); + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await cancel('im.fluffychat.unknown_device'); + return; + } + } + // as both parties can send a start, the last step being "ready" is race-condition prone // as such, we better set it *before* we send our start lastStep = type; @@ -291,6 +311,16 @@ class KeyVerification { await cancel('m.unknown_method'); return; } + + // ensure we have the other sides keys + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await client.updateUserDeviceKeys(additionalUsers: {userId}); + if (client.userDeviceKeys[userId]?.deviceKeys[deviceId!] == null) { + await cancel('im.fluffychat.unknown_device'); + return; + } + } + method = _makeVerificationMethod(payload['method'], this); if (lastStep == null) { // validate the start time @@ -352,17 +382,17 @@ class KeyVerification { String? recoveryKey, String? keyOrPassphrase, bool skip = false}) async { - final next = () { + final next = () async { if (_nextAction == 'request') { - sendStart(); + await sendStart(); } else if (_nextAction == 'done') { // and now let's sign them all in the background - encryption.crossSigning.sign(_verifiedDevices); + unawaited(encryption.crossSigning.sign(_verifiedDevices)); setState(KeyVerificationState.done); } }; if (skip) { - next(); + await next(); return; } final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning); @@ -371,7 +401,7 @@ class KeyVerification { recoveryKey: recoveryKey, keyOrPassphrase: keyOrPassphrase); await handle.maybeCacheAll(); - next(); + await next(); } /// called when the user accepts an incoming verification @@ -512,9 +542,9 @@ class KeyVerification { encryption.crossSigning.signable(_verifiedDevices)) { // these keys can be signed! Let's do so if (await encryption.crossSigning.isCached()) { - // and now let's sign them all in the background - // ignore: unawaited_futures - encryption.crossSigning.sign(_verifiedDevices); + // we want to make sure the verification state is correct for the other party after this event is handled. + // Otherwise the verification dialog might be stuck in an unverified but done state for a bit. + await encryption.crossSigning.sign(_verifiedDevices); } else if (!wasUnknownSession) { askingSSSS = true; } diff --git a/lib/src/client.dart b/lib/src/client.dart index 7adb57a2..de045ba0 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1307,7 +1307,7 @@ class Client extends MatrixApi { if (isLogged()) return; } // we aren't logged in - encryption?.dispose(); + await encryption?.dispose(); encryption = null; onLoginStateChanged.add(LoginState.loggedOut); Logs().i('User is not logged in.'); @@ -1315,14 +1315,14 @@ class Client extends MatrixApi { return; } - encryption?.dispose(); + await encryption?.dispose(); try { // make sure to throw an exception if libolm doesn't exist await olm.init(); olm.get_library_version(); encryption = Encryption(client: this); } catch (_) { - encryption?.dispose(); + await encryption?.dispose(); encryption = null; } await encryption?.init(olmAccount); @@ -1408,7 +1408,7 @@ class Client extends MatrixApi { _id = accessToken = syncFilterId = homeserver = _userID = _deviceID = _deviceName = prevBatch = null; _rooms = []; - encryption?.dispose(); + await encryption?.dispose(); encryption = null; final databaseDestroyer = this.databaseDestroyer; if (databaseDestroyer != null) { @@ -1443,18 +1443,14 @@ class Client extends MatrixApi { return _sync(); } - Future _sync() async { - if (_currentSync == null) { - final _currentSync = this._currentSync = _innerSync(); - // ignore: unawaited_futures - _currentSync.whenComplete(() { - this._currentSync = null; - if (_backgroundSync && isLogged() && !_disposed) { - _sync(); - } - }); - } - await _currentSync; + Future _sync() { + final _currentSync = this._currentSync ??= _innerSync().whenComplete(() { + this._currentSync = null; + if (_backgroundSync && isLogged() && !_disposed) { + _sync(); + } + }); + return _currentSync; } /// Presence that is set on sync. @@ -2112,7 +2108,7 @@ class Client extends MatrixApi { final Map _keyQueryFailures = {}; - Future updateUserDeviceKeys() async { + Future updateUserDeviceKeys({Set? additionalUsers}) async { try { final database = this.database; if (!isLogged() || database == null) return; @@ -2120,6 +2116,7 @@ class Client extends MatrixApi { final trackedUserIds = await _getUserIdsInEncryptedRooms(); if (!isLogged()) return; trackedUserIds.add(userID!); + if (additionalUsers != null) trackedUserIds.addAll(additionalUsers); // Remove all userIds we no longer need to track the devices of. _userDeviceKeys @@ -2511,8 +2508,7 @@ class Client extends MatrixApi { ? deviceKeys.length : i + chunkSize); // and send - // ignore: unawaited_futures - sendToDeviceEncrypted(chunk, eventType, message); + await sendToDeviceEncrypted(chunk, eventType, message); } }(); } @@ -2665,14 +2661,15 @@ class Client extends MatrixApi { Future dispose({bool closeDatabase = true}) async { _disposed = true; await abortSync(); - encryption?.dispose(); + await encryption?.dispose(); encryption = null; try { if (closeDatabase) { + final database = _database; + _database = null; await database ?.close() .catchError((e, s) => Logs().w('Failed to close database: ', e, s)); - _database = null; } } catch (error, stacktrace) { Logs().w('Failed to close database: ', error, stacktrace); diff --git a/pubspec.yaml b/pubspec.yaml index 50f7332a..3ae5821d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: + async: ^2.8.0 blurhash_dart: ^1.1.0 http: ^0.13.0 mime: ^1.0.0 diff --git a/test/client_test.dart b/test/client_test.dart index 38019cdc..045b15d0 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -33,9 +33,6 @@ import 'fake_matrix_api.dart'; void main() { late Client matrix; - Future> eventUpdateListFuture; - Future> toDeviceUpdateListFuture; - // key @test:fakeServer.notExisting const pickledOlmAccount = 'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw'; @@ -48,18 +45,20 @@ void main() { /// Check if all Elements get created - matrix = Client( - 'testclient', - httpClient: FakeMatrixApi(), - databaseBuilder: getDatabase, - ); - - eventUpdateListFuture = matrix.onEvent.stream.toList(); - toDeviceUpdateListFuture = matrix.onToDeviceEvent.stream.toList(); + setUp(() async { + matrix = await getClient(); + }); var olmEnabled = true; test('Login', () async { + matrix = Client( + 'testclient', + httpClient: FakeMatrixApi(), + databaseBuilder: getDatabase, + ); + final eventUpdateListFuture = matrix.onEvent.stream.toList(); + final toDeviceUpdateListFuture = matrix.onToDeviceEvent.stream.toList(); try { await olm.init(); olm.get_library_version(); @@ -203,36 +202,7 @@ void main() { matrix.getRoomByAlias( "#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}"), null); - }); - test('recentEmoji', () async { - final emojis = matrix.recentEmojis; - - expect(emojis.length, 2); - - expect(emojis['👍️'], 1); - expect(emojis['🖇️'], 0); - - await matrix.addRecentEmoji('🦙'); - // To check if the emoji is properly added, we need to wait for a sync roundtrip - }); - - test('Logout', () async { - final loginStateFuture = matrix.onLoginStateChanged.stream.first; - await matrix.logout(); - - expect(matrix.accessToken == null, true); - expect(matrix.homeserver == null, true); - expect(matrix.userID == null, true); - expect(matrix.deviceID == null, true); - expect(matrix.deviceName == null, true); - expect(matrix.prevBatch == null, true); - - final loginState = await loginStateFuture; - expect(loginState, LoginState.loggedOut); - }); - - test('Event Update Test', () async { await matrix.onEvent.close(); final eventUpdateList = await eventUpdateListFuture; @@ -291,23 +261,48 @@ void main() { expect(eventUpdateList[12].content['type'], 'm.room.member'); expect(eventUpdateList[12].roomID, '!696r7674:example.com'); expect(eventUpdateList[12].type, EventUpdateType.inviteState); - }); - test('To Device Update Test', () async { await matrix.onToDeviceEvent.close(); - final eventUpdateList = await toDeviceUpdateListFuture; + final deviceeventUpdateList = await toDeviceUpdateListFuture; - expect(eventUpdateList.length, 2); + expect(deviceeventUpdateList.length, 2); - expect(eventUpdateList[0].type, 'm.new_device'); + expect(deviceeventUpdateList[0].type, 'm.new_device'); if (olmEnabled) { - expect(eventUpdateList[1].type, 'm.room_key'); + expect(deviceeventUpdateList[1].type, 'm.room_key'); } else { - expect(eventUpdateList[1].type, 'm.room.encrypted'); + expect(deviceeventUpdateList[1].type, 'm.room.encrypted'); } }); + test('recentEmoji', () async { + final emojis = matrix.recentEmojis; + + expect(emojis.length, 2); + + expect(emojis['👍️'], 1); + expect(emojis['🖇️'], 0); + + await matrix.addRecentEmoji('🦙'); + // To check if the emoji is properly added, we need to wait for a sync roundtrip + }); + + test('Logout', () async { + final loginStateFuture = matrix.onLoginStateChanged.stream.first; + await matrix.logout(); + + expect(matrix.accessToken == null, true); + expect(matrix.homeserver == null, true); + expect(matrix.userID == null, true); + expect(matrix.deviceID == null, true); + expect(matrix.deviceName == null, true); + expect(matrix.prevBatch == null, true); + + final loginState = await loginStateFuture; + expect(loginState, LoginState.loggedOut); + }); + test('Login', () async { matrix = Client( 'testclient', @@ -315,8 +310,6 @@ void main() { databaseBuilder: getDatabase, ); - eventUpdateListFuture = matrix.onEvent.stream.toList(); - await matrix.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), checkWellKnown: false); @@ -561,12 +554,14 @@ void main() { '{\"next_batch\":\"s82_571_2_6_39_1_2_34_1\",\"account_data\":{\"events\":[{\"type\":\"m.push_rules\",\"content\":{\"global\":{\"underride\":[{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.call.invite\"}],\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"ring\"},{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.call\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"room_member_count\",\"is\":\"2\"},{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.message\"}],\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"default\"},{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.room_one_to_one\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"room_member_count\",\"is\":\"2\"},{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.encrypted\"}],\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"default\"},{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.encrypted_room_one_to_one\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.message\"}],\"actions\":[\"notify\",{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.message\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.encrypted\"}],\"actions\":[\"notify\",{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.encrypted\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"im.vector.modular.widgets\"},{\"kind\":\"event_match\",\"key\":\"content.type\",\"pattern\":\"jitsi\"},{\"kind\":\"event_match\",\"key\":\"state_key\",\"pattern\":\"*\"}],\"actions\":[\"notify\",{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".im.vector.jitsi\",\"default\":true,\"enabled\":true}],\"sender\":[],\"room\":[],\"content\":[{\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"default\"},{\"set_tweak\":\"highlight\"}],\"pattern\":\"056d6976-fb61-47cf-86f0-147387461565\",\"rule_id\":\".m.rule.contains_user_name\",\"default\":true,\"enabled\":true}],\"override\":[{\"conditions\":[],\"actions\":[\"dont_notify\"],\"rule_id\":\".m.rule.master\",\"default\":true,\"enabled\":false},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"content.msgtype\",\"pattern\":\"m.notice\"}],\"actions\":[\"dont_notify\"],\"rule_id\":\".m.rule.suppress_notices\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.member\"},{\"kind\":\"event_match\",\"key\":\"content.membership\",\"pattern\":\"invite\"},{\"kind\":\"event_match\",\"key\":\"state_key\",\"pattern\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\"}],\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"default\"},{\"set_tweak\":\"highlight\",\"value\":false}],\"rule_id\":\".m.rule.invite_for_me\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.member\"}],\"actions\":[\"dont_notify\"],\"rule_id\":\".m.rule.member_event\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"contains_display_name\"}],\"actions\":[\"notify\",{\"set_tweak\":\"sound\",\"value\":\"default\"},{\"set_tweak\":\"highlight\"}],\"rule_id\":\".m.rule.contains_display_name\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"content.body\",\"pattern\":\"@room\"},{\"kind\":\"sender_notification_permission\",\"key\":\"room\"}],\"actions\":[\"notify\",{\"set_tweak\":\"highlight\",\"value\":true}],\"rule_id\":\".m.rule.roomnotif\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.room.tombstone\"},{\"kind\":\"event_match\",\"key\":\"state_key\",\"pattern\":\"\"}],\"actions\":[\"notify\",{\"set_tweak\":\"highlight\",\"value\":true}],\"rule_id\":\".m.rule.tombstone\",\"default\":true,\"enabled\":true},{\"conditions\":[{\"kind\":\"event_match\",\"key\":\"type\",\"pattern\":\"m.reaction\"}],\"actions\":[\"dont_notify\"],\"rule_id\":\".m.rule.reaction\",\"default\":true,\"enabled\":true}]},\"device\":{}}}]},\"presence\":{\"events\":[{\"type\":\"m.presence\",\"sender\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"presence\":\"online\",\"last_active_ago\":43,\"currently_active\":true}}]},\"device_one_time_keys_count\":{\"signed_curve25519\":66},\"org.matrix.msc2732.device_unused_fallback_key_types\":[\"signed_curve25519\"],\"device_unused_fallback_key_types\":[\"signed_curve25519\"],\"rooms\":{\"join\":{\"!MEgZosbiZqjSjbHFqI:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\":{\"timeline\":{\"events\":[{\"type\":\"m.room.member\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"membership\":\"join\",\"displayname\":\"Lars Kaiser\"},\"state_key\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"origin_server_ts\":1647296944593,\"unsigned\":{\"age\":545455},\"event_id\":\"\$mk9kFUEAKBZJgarWApLyYqOZQQocLIVV8tWp_gJEZFU\"},{\"type\":\"m.room.power_levels\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"users\":{\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\":100},\"users_default\":0,\"events\":{\"m.room.name\":50,\"m.room.power_levels\":100,\"m.room.history_visibility\":100,\"m.room.canonical_alias\":50,\"m.room.avatar\":50,\"m.room.tombstone\":100,\"m.room.server_acl\":100,\"m.room.encryption\":100},\"events_default\":0,\"state_default\":50,\"ban\":50,\"kick\":50,\"redact\":50,\"invite\":50,\"historical\":100},\"state_key\":\"\",\"origin_server_ts\":1647296944690,\"unsigned\":{\"age\":545358},\"event_id\":\"\$3wL2YgVNQzgfl8y_ksi3BPMqRs94jb_m0WRonL1HNpY\"},{\"type\":\"m.room.canonical_alias\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"alias\":\"#user-discovery:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\"},\"state_key\":\"\",\"origin_server_ts\":1647296944806,\"unsigned\":{\"age\":545242},\"event_id\":\"\$yXaVETL9F4jSN9rpRNyT_kUoctzD07n5Z4AIHziP7DQ\"},{\"type\":\"m.room.join_rules\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"join_rule\":\"public\"},\"state_key\":\"\",\"origin_server_ts\":1647296944894,\"unsigned\":{\"age\":545154},\"event_id\":\"\$jBDHhgpNqr125eWUsGVw4r7ZG2hgr0BTzzR77S-ubvY\"},{\"type\":\"m.room.history_visibility\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"history_visibility\":\"shared\"},\"state_key\":\"\",\"origin_server_ts\":1647296944965,\"unsigned\":{\"age\":545083},\"event_id\":\"\$kMessP7gAphUKW7mzOLlJT6NT8IsVGPmGir3_1uBNCE\"},{\"type\":\"m.room.name\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"name\":\"User Discovery\"},\"state_key\":\"\",\"origin_server_ts\":1647296945062,\"unsigned\":{\"age\":544986},\"event_id\":\"\$Bo9Ut_0vcr3FuxCRye4IHEMxUxIIcSwc-ePnMzx-hYU\"},{\"type\":\"m.room.member\",\"sender\":\"@test:fakeServer.notExisting\",\"content\":{\"membership\":\"join\",\"displayname\":\"1c2e5c2b-f958-45a5-9fcb-eef3969c31df\"},\"state_key\":\"@test:fakeServer.notExisting\",\"origin_server_ts\":1647296989893,\"unsigned\":{\"age\":500155},\"event_id\":\"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE\"},{\"type\":\"m.room.member\",\"sender\":\"@test:fakeServer.notExisting\",\"content\":{\"membership\":\"join\",\"displayname\":\"Some First Name Some Last Name\"},\"state_key\":\"@test:fakeServer.notExisting\",\"origin_server_ts\":1647296990076,\"unsigned\":{\"replaces_state\":\"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE\",\"prev_content\":{\"membership\":\"join\",\"displayname\":\"1c2e5c2b-f958-45a5-9fcb-eef3969c31df\"},\"prev_sender\":\"@test:fakeServer.notExisting\",\"age\":499972},\"event_id\":\"\$3Ut97nFBgOtsrnRPW-pqr28z7ETNMttj7GcjkIv4zWw\"},{\"type\":\"m.room.member\",\"sender\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"membership\":\"join\",\"displayname\":\"056d6976-fb61-47cf-86f0-147387461565\"},\"state_key\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"origin_server_ts\":1647297489154,\"unsigned\":{\"age\":894},\"event_id\":\"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0\"},{\"type\":\"m.room.member\",\"sender\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"membership\":\"join\",\"displayname\":\"Another User\"},\"state_key\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"origin_server_ts\":1647297489290,\"unsigned\":{\"replaces_state\":\"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0\",\"prev_content\":{\"membership\":\"join\",\"displayname\":\"056d6976-fb61-47cf-86f0-147387461565\"},\"prev_sender\":\"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"age\":758},\"event_id\":\"\$dtQblqCbjr3TGc3WmrQ4YTkHaXJ2PcO0TAYDr9K7iQc\"}],\"prev_batch\":\"t2-62_571_2_6_39_1_2_34_1\",\"limited\":true},\"state\":{\"events\":[{\"type\":\"m.room.create\",\"sender\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\",\"content\":{\"m.federate\":false,\"room_version\":\"9\",\"creator\":\"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de\"},\"state_key\":\"\",\"origin_server_ts\":1647296944511,\"unsigned\":{\"age\":545537},\"event_id\":\"\$PAWKKULBVOLnqfrAAtXZz8tHEPXXjgRVbJJLifwQWbE\"}]},\"account_data\":{\"events\":[]},\"ephemeral\":{\"events\":[]},\"unread_notifications\":{\"notification_count\":0,\"highlight_count\":0},\"summary\":{\"m.joined_member_count\":3,\"m.invited_member_count\":0},\"org.matrix.msc2654.unread_count\":0}}}}'))); final profile = await client.fetchOwnProfile(); expect(profile.displayName, 'Some First Name Some Last Name'); + await client.dispose(closeDatabase: true); }); test('sendToDeviceEncrypted', () async { if (!olmEnabled) { return; } FakeMatrixApi.calledEndpoints.clear(); + await matrix.sendToDeviceEncrypted( matrix.userDeviceKeys['@alice:example.com']!.deviceKeys.values .toList(), @@ -716,6 +711,8 @@ void main() { // send raccoon --> fail // send bunny --> all sent final client = await getClient(); + await client.abortSync(); + FakeMatrixApi.failToDevice = true; final foxContent = { '@fox:example.org': { @@ -741,26 +738,42 @@ void main() { await client .sendToDevice('foxies', 'floof_txnid', foxContent) .catchError((e) => null); // ignore the error + + await FakeMatrixApi.firstWhereValue( + '/client/v3/sendToDevice/foxies/floof_txnid'); + FakeMatrixApi.calledEndpoints.clear(); + await client .sendToDevice('raccoon', 'raccoon_txnid', raccoonContent) .catchError((e) => null); - FakeMatrixApi.failToDevice = false; + + await FakeMatrixApi.firstWhereValue( + '/client/v3/sendToDevice/foxies/floof_txnid'); + FakeMatrixApi.calledEndpoints.clear(); + FakeMatrixApi.failToDevice = false; + await client.sendToDevice('bunny', 'bunny_txnid', bunnyContent); - expect( - json.decode(FakeMatrixApi - .calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid'] - ?[0])['messages'], - foxContent); - expect( - json.decode(FakeMatrixApi.calledEndpoints[ - '/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0])['messages'], - raccoonContent); - expect( - json.decode(FakeMatrixApi - .calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid'] - ?[0])['messages'], - bunnyContent); + + await FakeMatrixApi.firstWhereValue( + '/client/v3/sendToDevice/foxies/floof_txnid'); + await FakeMatrixApi.firstWhereValue( + '/client/v3/sendToDevice/bunny/bunny_txnid'); + final foxcall = FakeMatrixApi + .calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid']?[0]; + expect(foxcall != null, true); + expect(json.decode(foxcall)['messages'], foxContent); + + final racooncall = FakeMatrixApi + .calledEndpoints['/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0]; + expect(racooncall != null, true); + expect(json.decode(racooncall)['messages'], raccoonContent); + + final bunnycall = FakeMatrixApi + .calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid']?[0]; + expect(bunnycall != null, true); + expect(json.decode(bunnycall)['messages'], bunnyContent); + await client.dispose(closeDatabase: true); }); test('startDirectChat', () async { @@ -830,8 +843,6 @@ void main() { }, }); expect(matrix.ignoredUsers, ['@charley:stupid.abc']); - }); - test('ignoredUsers', () async { await matrix.ignoreUser('@charley2:stupid.abc'); await matrix.unignoreUser('@charley:stupid.abc'); }); @@ -988,5 +999,9 @@ void main() { ?.getEventById('143273582443PhrSn:example.org', event!.room); expect(storedEvent2?.eventId, event?.eventId); }); + + tearDown(() { + matrix.dispose(closeDatabase: true); + }); }); } diff --git a/test/encryption/key_manager_test.dart b/test/encryption/key_manager_test.dart index 260e6677..f22cf5de 100644 --- a/test/encryption/key_manager_test.dart +++ b/test/encryption/key_manager_test.dart @@ -288,9 +288,8 @@ void main() { .getInboundGroupSession(roomId, sessionId, senderKey) != null, false); - client.encryption!.keyManager + await client.encryption!.keyManager .setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); - await Future.delayed(Duration(milliseconds: 10)); expect( client.encryption!.keyManager .getInboundGroupSession(roomId, sessionId, senderKey) != @@ -395,7 +394,7 @@ void main() { 'sender_key': senderKey, 'sender_claimed_ed25519_key': client.fingerprintKey, }; - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); expect( @@ -421,7 +420,7 @@ void main() { 'sender_key': senderKey, 'sender_claimed_ed25519_key': client.fingerprintKey, }; - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); expect( @@ -447,7 +446,7 @@ void main() { 'sender_key': senderKey, 'sender_claimed_ed25519_key': client.fingerprintKey, }; - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); expect( @@ -473,7 +472,7 @@ void main() { 'sender_key': senderKey, 'sender_claimed_ed25519_key': client.fingerprintKey, }; - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); expect( @@ -499,7 +498,7 @@ void main() { 'sender_key': senderKey, 'sender_claimed_ed25519_key': client.fingerprintKey, }; - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); expect( @@ -539,8 +538,9 @@ void main() { .remove('JLAFKJWSCS'); // Alice adds her device with same device ID but different keys - final oldResp = FakeMatrixApi.api['POST']?['/client/v3/keys/query'](null); - FakeMatrixApi.api['POST']?['/client/v3/keys/query'] = (_) { + final oldResp = + FakeMatrixApi.currentApi?.api['POST']?['/client/v3/keys/query'](null); + FakeMatrixApi.currentApi?.api['POST']?['/client/v3/keys/query'] = (_) { oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = { 'user_id': '@alice:example.com', 'device_id': 'JLAFKJWSCS', diff --git a/test/encryption/key_verification_test.dart b/test/encryption/key_verification_test.dart index d1304d0d..82eaf0bc 100644 --- a/test/encryption/key_verification_test.dart +++ b/test/encryption/key_verification_test.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'package:matrix/matrix.dart'; @@ -59,11 +60,22 @@ EventUpdate getLastSentEvent(KeyVerification req) { ); } -void main() { +void main() async { + var olmEnabled = true; + try { + await olm.init(); + olm.get_library_version(); + } catch (e) { + olmEnabled = false; + Logs().w('[LibOlm] Failed to load LibOlm', e); + } + Logs().i('[LibOlm] Enabled: $olmEnabled'); + + final dynamic skip = olmEnabled ? false : 'olm library not available'; + /// All Tests related to the ChatTime group('Key Verification', () { Logs().level = Level.error; - var olmEnabled = true; // key @othertest:fakeServer.notExisting const otherPickledOlmAccount = @@ -72,21 +84,11 @@ void main() { late Client client1; late Client client2; - test('setupClient', () async { - try { - await olm.init(); - olm.get_library_version(); - } catch (e) { - olmEnabled = false; - Logs().w('[LibOlm] Failed to load LibOlm', e); - } - Logs().i('[LibOlm] Enabled: $olmEnabled'); - if (!olmEnabled) return; - + setUp(() async { client1 = await getClient(); client2 = Client( 'othertestclient', - httpClient: FakeMatrixApi(), + httpClient: FakeMatrixApi.currentApi!, databaseBuilder: getDatabase, ); await client2.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), @@ -109,9 +111,12 @@ void main() { KeyVerificationMethod.numbers }; }); + tearDown(() async { + await client1.dispose(closeDatabase: true); + await client2.dispose(closeDatabase: true); + }); test('Run emoji / number verification', () async { - if (!olmEnabled) return; // for a full run we test in-room verification in a cleartext room // because then we can easily intercept the payloads and inject in the other client FakeMatrixApi.calledEndpoints.clear(); @@ -122,15 +127,17 @@ void main() { await client1.userDeviceKeys[client2.userID]!.startVerification( newDirectChatEnableEncryption: false, ); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); - late KeyVerification req2; + final comp = Completer(); final sub = client2.onKeyVerificationRequest.stream.listen((req) { - req2 = req; + comp.complete(req); }); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); - await Future.delayed(Duration(milliseconds: 10)); + final req2 = await comp.future; await sub.cancel(); expect( @@ -141,27 +148,37 @@ void main() { // send ready FakeMatrixApi.calledEndpoints.clear(); await req2.acceptVerification(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready')); evt = getLastSentEvent(req2); expect(req2.state, KeyVerificationState.waitingAccept); // send start FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start')); evt = getLastSentEvent(req1); // send accept FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.accept')); evt = getLastSentEvent(req2); // send key FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req1); // send key FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req2); // receive last key @@ -196,13 +213,22 @@ void main() { await req1.acceptSas(); evt = getLastSentEvent(req1); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac')); expect(req1.state, KeyVerificationState.waitingSas); // send mac FakeMatrixApi.calledEndpoints.clear(); await req2.acceptSas(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac')); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.done')); evt = getLastSentEvent(req2); + FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.done')); expect(req1.state, KeyVerificationState.done); expect(req2.state, KeyVerificationState.done); @@ -219,7 +245,6 @@ void main() { }); test('ask SSSS start', () async { - if (!olmEnabled) return; client1.userDeviceKeys[client1.userID]!.masterKey! .setDirectVerified(true); await client1.encryption!.ssss.clearCache(); @@ -227,7 +252,8 @@ void main() { .startVerification(newDirectChatEnableEncryption: false); expect(req1.state, KeyVerificationState.askSSSS); await req1.openSSSS(recoveryKey: ssssKey); - await Future.delayed(Duration(seconds: 1)); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); expect(req1.state, KeyVerificationState.waitingAccept); await req1.cancel(); @@ -235,7 +261,6 @@ void main() { }); test('ask SSSS end', () async { - if (!olmEnabled) return; FakeMatrixApi.calledEndpoints.clear(); // make sure our master key is *not* verified to not triger SSSS for now client1.userDeviceKeys[client1.userID]!.masterKey! @@ -245,41 +270,53 @@ void main() { .setDirectVerified(true); final req1 = await client1.userDeviceKeys[client2.userID]! .startVerification(newDirectChatEnableEncryption: false); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); - late KeyVerification req2; + final comp = Completer(); final sub = client2.onKeyVerificationRequest.stream.listen((req) { - req2 = req; + comp.complete(req); }); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); - await Future.delayed(Duration(milliseconds: 10)); + final req2 = await comp.future; await sub.cancel(); // send ready FakeMatrixApi.calledEndpoints.clear(); await req2.acceptVerification(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready')); evt = getLastSentEvent(req2); expect(req2.state, KeyVerificationState.waitingAccept); // send start FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start')); evt = getLastSentEvent(req1); // send accept FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.accept')); evt = getLastSentEvent(req2); // send key FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req1); // send key FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req2); // receive last key @@ -313,6 +350,8 @@ void main() { await req1.acceptSas(); evt = getLastSentEvent(req1); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac')); expect(req1.state, KeyVerificationState.waitingSas); // send mac @@ -320,12 +359,16 @@ void main() { await req2.acceptSas(); evt = getLastSentEvent(req2); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac')); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.done')); + FakeMatrixApi.calledEndpoints.clear(); expect(req1.state, KeyVerificationState.askSSSS); expect(req2.state, KeyVerificationState.done); await req1.openSSSS(recoveryKey: ssssKey); - await Future.delayed(Duration(milliseconds: 10)); expect(req1.state, KeyVerificationState.done); client1.encryption!.ssss = MockSSSS(client1.encryption!); @@ -334,34 +377,35 @@ void main() { await req1.maybeRequestSSSSSecrets(); await Future.delayed(Duration(milliseconds: 10)); expect((client1.encryption!.ssss as MockSSSS).requestedSecrets, true); - // delay for 12 seconds to be sure no other tests clear the ssss cache - await Future.delayed(Duration(seconds: 12)); await client1.encryption!.keyVerificationManager.cleanup(); await client2.encryption!.keyVerificationManager.cleanup(); }); test('reject verification', () async { - if (!olmEnabled) return; FakeMatrixApi.calledEndpoints.clear(); // make sure our master key is *not* verified to not triger SSSS for now client1.userDeviceKeys[client1.userID]!.masterKey! .setDirectVerified(false); final req1 = await client1.userDeviceKeys[client2.userID]! .startVerification(newDirectChatEnableEncryption: false); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); - late KeyVerification req2; + final comp = Completer(); final sub = client2.onKeyVerificationRequest.stream.listen((req) { - req2 = req; + comp.complete(req); }); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); - await Future.delayed(Duration(milliseconds: 10)); + final req2 = await comp.future; await sub.cancel(); FakeMatrixApi.calledEndpoints.clear(); await req2.rejectVerification(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.cancel')); evt = getLastSentEvent(req2); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); expect(req1.state, KeyVerificationState.error); @@ -372,48 +416,59 @@ void main() { }); test('reject sas', () async { - if (!olmEnabled) return; FakeMatrixApi.calledEndpoints.clear(); // make sure our master key is *not* verified to not triger SSSS for now client1.userDeviceKeys[client1.userID]!.masterKey! .setDirectVerified(false); final req1 = await client1.userDeviceKeys[client2.userID]! .startVerification(newDirectChatEnableEncryption: false); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); var evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); - late KeyVerification req2; + final comp = Completer(); final sub = client2.onKeyVerificationRequest.stream.listen((req) { - req2 = req; + comp.complete(req); }); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); - await Future.delayed(Duration(milliseconds: 10)); + final req2 = await comp.future; await sub.cancel(); // send ready FakeMatrixApi.calledEndpoints.clear(); await req2.acceptVerification(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready')); evt = getLastSentEvent(req2); expect(req2.state, KeyVerificationState.waitingAccept); // send start FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.start')); evt = getLastSentEvent(req1); // send accept FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.accept')); evt = getLastSentEvent(req2); // send key FakeMatrixApi.calledEndpoints.clear(); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req1); // send key FakeMatrixApi.calledEndpoints.clear(); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.key')); evt = getLastSentEvent(req2); // receive last key @@ -421,8 +476,12 @@ void main() { await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); await req1.acceptSas(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac')); FakeMatrixApi.calledEndpoints.clear(); await req2.rejectSas(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.cancel')); evt = getLastSentEvent(req2); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); expect(req1.state, KeyVerificationState.error); @@ -433,22 +492,23 @@ void main() { }); test('other device accepted', () async { - if (!olmEnabled) return; FakeMatrixApi.calledEndpoints.clear(); // make sure our master key is *not* verified to not triger SSSS for now client1.userDeviceKeys[client1.userID]!.masterKey! .setDirectVerified(false); final req1 = await client1.userDeviceKeys[client2.userID]! .startVerification(newDirectChatEnableEncryption: false); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message')); final evt = getLastSentEvent(req1); expect(req1.state, KeyVerificationState.waitingAccept); - late KeyVerification req2; + final comp = Completer(); final sub = client2.onKeyVerificationRequest.stream.listen((req) { - req2 = req; + comp.complete(req); }); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); - await Future.delayed(Duration(milliseconds: 10)); + final req2 = await comp.future; await sub.cancel(); await client2.encryption!.keyVerificationManager @@ -473,14 +533,10 @@ void main() { expect(req2.state, KeyVerificationState.error); await req2.cancel(); + await FakeMatrixApi.firstWhere((e) => e.startsWith( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.cancel')); await client1.encryption!.keyVerificationManager.cleanup(); await client2.encryption!.keyVerificationManager.cleanup(); }); - - test('dispose client', () async { - if (!olmEnabled) return; - await client1.dispose(closeDatabase: true); - await client2.dispose(closeDatabase: true); - }); - }); + }, skip: skip); } diff --git a/test/encryption/olm_manager_test.dart b/test/encryption/olm_manager_test.dart index 0a222e5d..28714270 100644 --- a/test/encryption/olm_manager_test.dart +++ b/test/encryption/olm_manager_test.dart @@ -33,7 +33,7 @@ void main() { late Client client; - test('setupClient', () async { + setUp(() async { try { await olm.init(); olm.get_library_version(); @@ -42,9 +42,10 @@ void main() { Logs().w('[LibOlm] Failed to load LibOlm', e); } Logs().i('[LibOlm] Enabled: $olmEnabled'); - if (!olmEnabled) return; + if (!olmEnabled) return Future.value(); client = await getClient(); + return Future.value(); }); test('signatures', () async { @@ -89,10 +90,11 @@ void main() { test('handleDeviceOneTimeKeysCount', () async { if (!olmEnabled) return; + FakeMatrixApi.calledEndpoints.clear(); client.encryption!.olmManager .handleDeviceOneTimeKeysCount({'signed_curve25519': 20}, null); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload'); expect( FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), true); @@ -100,14 +102,15 @@ void main() { FakeMatrixApi.calledEndpoints.clear(); client.encryption!.olmManager .handleDeviceOneTimeKeysCount({'signed_curve25519': 70}, null); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload') + .timeout(Duration(milliseconds: 50), onTimeout: () => ''); expect( FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), false); FakeMatrixApi.calledEndpoints.clear(); client.encryption!.olmManager.handleDeviceOneTimeKeysCount(null, []); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload'); expect( FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), true); @@ -116,7 +119,7 @@ void main() { FakeMatrixApi.calledEndpoints.clear(); client.encryption!.olmManager .handleDeviceOneTimeKeysCount(null, ['signed_curve25519']); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload'); expect( FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), true); diff --git a/test/encryption/online_key_backup_test.dart b/test/encryption/online_key_backup_test.dart index 8f8c7945..a966545e 100644 --- a/test/encryption/online_key_backup_test.dart +++ b/test/encryption/online_key_backup_test.dart @@ -93,13 +93,14 @@ void main() { 'sender_claimed_ed25519_key': client.fingerprintKey, }; FakeMatrixApi.calledEndpoints.clear(); - client.encryption!.keyManager.setInboundGroupSession( + await client.encryption!.keyManager.setInboundGroupSession( roomId, sessionId, senderKey, sessionPayload, forwarded: true); - await Future.delayed(Duration(milliseconds: 500)); var dbSessions = await client.database!.getInboundGroupSessionsToUpload(); expect(dbSessions.isNotEmpty, true); await client.encryption!.keyManager.backgroundTasks(); + await FakeMatrixApi.firstWhereValue( + '/client/v3/room_keys/keys?version=5'); final payload = FakeMatrixApi .calledEndpoints['/client/v3/room_keys/keys?version=5']!.first; dbSessions = await client.database!.getInboundGroupSessionsToUpload(); diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index 4d44380e..eec6d180 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'dart:core'; import 'dart:math'; import 'package:http/http.dart'; -import 'package:http/testing.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -35,120 +35,209 @@ Map decodeJson(dynamic data) { return data; } -class FakeMatrixApi extends MockClient { - static final calledEndpoints = >{}; - static int eventCounter = 0; - static sdk.Client? client; - static bool failToDevice = false; +class FakeMatrixApi extends BaseClient { + static Map> get calledEndpoints => + currentApi!._calledEndpoints; + static int get eventCounter => currentApi!._eventCounter; + static set eventCounter(int c) { + currentApi!._eventCounter = c; + } - FakeMatrixApi() - : super((request) async { - // Collect data from Request - var action = request.url.path; - if (request.url.path.contains('/_matrix')) { - action = request.url.path.split('/_matrix').last + - '?' + - request.url.query; - } + static set client(sdk.Client? c) { + currentApi?._client = c; + } - if (action.endsWith('?')) { - action = action.substring(0, action.length - 1); - } - if (action.endsWith('?server_name')) { - // This can be removed after matrix_api_lite is released with: - // https://gitlab.com/famedly/libraries/matrix_api_lite/-/merge_requests/16 - action = action.substring(0, action.length - 12); - } - if (action.endsWith('/')) { - action = action.substring(0, action.length - 1); - } - final method = request.method; - final dynamic data = - method == 'GET' ? request.url.queryParameters : request.body; - dynamic res = {}; - var statusCode = 200; + static set failToDevice(bool fail) { + currentApi?._failToDevice = fail; + } - //print('$method request to $action with Data: $data'); + static set trace(bool t) { + currentApi?._trace = t; + } - // Sync requests with timeout - if (data is Map && data['timeout'] is String) { - await Future.delayed(Duration(seconds: 5)); - } + final _calledEndpoints = >{}; + int _eventCounter = 0; + sdk.Client? _client; + bool _failToDevice = false; + bool _trace = false; + final _apiCallStream = StreamController.broadcast(); - if (request.url.origin != 'https://fakeserver.notexisting') { - return Response( - 'Not found...', 404); - } + static FakeMatrixApi? currentApi; - // Call API - if (!calledEndpoints.containsKey(action)) { - calledEndpoints[action] = []; - } - calledEndpoints[action]?.add(data); - final act = api[method]?[action]; - if (act != null) { - res = act(data); - if (res is Map && res.containsKey('errcode')) { - if (res['errcode'] == 'M_NOT_FOUND') { - statusCode = 404; - } else { - statusCode = 405; - } - } - } else if (method == 'PUT' && - action.contains('/client/v3/sendToDevice/')) { - res = {}; - if (failToDevice) { - statusCode = 500; - } - } else if (method == 'GET' && - action.contains('/client/v3/rooms/') && - action.contains('/state/m.room.member/') && - !action.endsWith('%40alicyy%3Aexample.com')) { - res = {'displayname': ''}; - } else if (method == 'PUT' && - action.contains( - '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/')) { - res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'}; - } else if (method == 'PUT' && - action.contains( - '/client/v3/rooms/!1234%3AfakeServer.notExisting/state/')) { - res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'}; - } else if (action.contains('/client/v3/sync')) { - res = { - 'next_batch': DateTime.now().millisecondsSinceEpoch.toString(), - }; - } else if (method == 'PUT' && - client != null && - action.contains('/account_data/') && - !action.contains('/room/')) { - final type = Uri.decodeComponent(action.split('/').last); - final syncUpdate = sdk.SyncUpdate( - nextBatch: '', - accountData: [ - sdk.BasicEvent(content: decodeJson(data), type: type) - ], - ); - if (client?.database != null) { - await client?.database?.transaction(() async { - await client?.handleSync(syncUpdate); - }); - } else { - await client?.handleSync(syncUpdate); - } - res = {}; - } else { - res = { - 'errcode': 'M_UNRECOGNIZED', - 'error': 'Unrecognized request' - }; - statusCode = 405; - } + static Future firstWhereValue(String value) { + return firstWhere((v) => v == value); + } - return Response.bytes(utf8.encode(json.encode(res)), statusCode); + static Future firstWhere(bool Function(String element) test) { + for (final e in currentApi!._calledEndpoints.entries) { + if (e.value.isNotEmpty && test(e.key)) { + return Future.value(e.key); + } + } + + final completer = Completer(); + StreamSubscription? sub; + sub = currentApi!._apiCallStream.stream.listen((action) { + if (test(action)) { + sub?.cancel(); + completer.complete(action); + } + }); + return completer.future; + } + + FutureOr mockIntercept(Request request) async { + // Collect data from Request + var action = request.url.path; + if (request.url.path.contains('/_matrix')) { + action = + request.url.path.split('/_matrix').last + '?' + request.url.query; + } + + // ignore: avoid_print + if (_trace) print('called $action'); + + if (action.endsWith('?')) { + action = action.substring(0, action.length - 1); + } + if (action.endsWith('?server_name')) { + // This can be removed after matrix_api_lite is released with: + // https://gitlab.com/famedly/libraries/matrix_api_lite/-/merge_requests/16 + action = action.substring(0, action.length - 12); + } + if (action.endsWith('/')) { + action = action.substring(0, action.length - 1); + } + final method = request.method; + final dynamic data = + method == 'GET' ? request.url.queryParameters : request.body; + dynamic res = {}; + var statusCode = 200; + + //print('\$method request to $action with Data: $data'); + + // Sync requests with timeout + if (data is Map && data['timeout'] is String) { + await Future.delayed(Duration(seconds: 5)); + } + + if (request.url.origin != 'https://fakeserver.notexisting') { + return Response( + 'Not found...', 404); + } + + // Call API + (_calledEndpoints[action] ??= []).add(data); + final act = api[method]?[action]; + if (act != null) { + res = act(data); + if (res is Map && res.containsKey('errcode')) { + if (res['errcode'] == 'M_NOT_FOUND') { + statusCode = 404; + } else { + statusCode = 405; + } + } + } else if (method == 'PUT' && action.contains('/client/v3/sendToDevice/')) { + res = {}; + if (_failToDevice) { + statusCode = 500; + } + } else if (method == 'GET' && + action.contains('/client/v3/rooms/') && + action.contains('/state/m.room.member/') && + !action.endsWith('%40alicyy%3Aexample.com')) { + res = {'displayname': ''}; + } else if (method == 'PUT' && + action.contains( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/send/')) { + res = {'event_id': '\$event${_eventCounter++}'}; + } else if (method == 'PUT' && + action.contains( + '/client/v3/rooms/!1234%3AfakeServer.notExisting/state/')) { + res = {'event_id': '\$event${_eventCounter++}'}; + } else if (action.contains('/client/v3/sync')) { + res = { + 'next_batch': DateTime.now().millisecondsSinceEpoch.toString(), + }; + } else if (method == 'PUT' && + _client != null && + action.contains('/account_data/') && + !action.contains('/room/')) { + final type = Uri.decodeComponent(action.split('/').last); + final syncUpdate = sdk.SyncUpdate( + nextBatch: '', + accountData: [sdk.BasicEvent(content: decodeJson(data), type: type)], + ); + if (_client?.database != null) { + await _client?.database?.transaction(() async { + await _client?.handleSync(syncUpdate); }); + } else { + await _client?.handleSync(syncUpdate); + } + res = {}; + } else { + res = {'errcode': 'M_UNRECOGNIZED', 'error': 'Unrecognized request'}; + statusCode = 405; + } - static Map messagesResponsePast = { + unawaited(Future.delayed(Duration(milliseconds: 1)).then((_) async { + _apiCallStream.add(action); + })); + return Response.bytes(utf8.encode(json.encode(res)), statusCode); + } + + @override + Future send(BaseRequest baseRequest) async { + final bodyStream = baseRequest.finalize(); + final bodyBytes = await bodyStream.toBytes(); + final request = Request(baseRequest.method, baseRequest.url) + ..persistentConnection = baseRequest.persistentConnection + ..followRedirects = baseRequest.followRedirects + ..maxRedirects = baseRequest.maxRedirects + ..headers.addAll(baseRequest.headers) + ..bodyBytes = bodyBytes + ..finalize(); + + final response = await mockIntercept(request); + return StreamedResponse( + ByteStream.fromBytes(response.bodyBytes), response.statusCode, + contentLength: response.contentLength, + request: baseRequest, + headers: response.headers, + isRedirect: response.isRedirect, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase); + } + + FakeMatrixApi() { + currentApi = this; + api['POST']?['/client/v3/keys/device_signing/upload'] = (var reqI) { + if (_client != null) { + final jsonBody = decodeJson(reqI); + for (final keyType in { + 'master_key', + 'self_signing_key', + 'user_signing_key' + }) { + if (jsonBody[keyType] != null) { + final key = + sdk.CrossSigningKey.fromJson(jsonBody[keyType], _client!); + _client!.userDeviceKeys[_client!.userID!]?.crossSigningKeys + .removeWhere((k, v) => v.usage.contains(key.usage.first)); + _client!.userDeviceKeys[_client!.userID!] + ?.crossSigningKeys[key.publicKey!] = key; + } + } + // and generate a fake sync + _client!.handleSync(sdk.SyncUpdate(nextBatch: '')); + } + return {}; + }; + } + + static const Map messagesResponsePast = { 'start': 't47429-4392820_219380_26003_2265', 'end': 't47409-4357353_219380_26003_2265', 'chunk': [ @@ -206,7 +295,7 @@ class FakeMatrixApi extends MockClient { ], 'state': [], }; - static Map messagesResponseFuture = { + static const Map messagesResponseFuture = { 'start': 't456', 'end': 't789', 'chunk': [ @@ -264,7 +353,7 @@ class FakeMatrixApi extends MockClient { ], 'state': [], }; - static Map messagesResponseFutureEnd = { + static const Map messagesResponseFutureEnd = { 'start': 't789', 'end': null, 'chunk': [], @@ -856,7 +945,7 @@ class FakeMatrixApi extends MockClient { } }; - static final Map> api = { + final Map> api = { 'GET': { '/path/to/auth/error': (var req) => { 'errcode': 'M_FORBIDDEN', @@ -2177,28 +2266,6 @@ class FakeMatrixApi extends MockClient { '/client/v3/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {}, '/client/v3/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {}, '/client/v3/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {}, - '/client/v3/keys/device_signing/upload': (var reqI) { - if (client != null) { - final jsonBody = decodeJson(reqI); - for (final keyType in { - 'master_key', - 'self_signing_key', - 'user_signing_key' - }) { - if (jsonBody[keyType] != null) { - final key = - sdk.CrossSigningKey.fromJson(jsonBody[keyType], client!); - client!.userDeviceKeys[client!.userID!]?.crossSigningKeys - .removeWhere((k, v) => v.usage.contains(key.usage.first)); - client!.userDeviceKeys[client!.userID!] - ?.crossSigningKeys[key.publicKey!] = key; - } - } - // and generate a fake sync - client!.handleSync(sdk.SyncUpdate(nextBatch: '')); - } - return {}; - }, '/client/v3/keys/signatures/upload': (var reqI) => {'failures': {}}, '/client/v3/room_keys/version': (var reqI) => {'version': '5'}, }, diff --git a/test/timeline_context_test.dart b/test/timeline_context_test.dart index 20f3ee7d..20d6d3e3 100644 --- a/test/timeline_context_test.dart +++ b/test/timeline_context_test.dart @@ -16,28 +16,55 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:matrix/matrix.dart'; import 'package:matrix/src/models/timeline_chunk.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import 'fake_client.dart'; +import 'fake_matrix_api.dart'; void main() { group('Timeline context', () { Logs().level = Level.error; final roomID = '!1234:example.com'; - final testTimeStamp = DateTime.now().millisecondsSinceEpoch; + var testTimeStamp = 0; var updateCount = 0; final insertList = []; final changeList = []; final removeList = []; var olmEnabled = true; + final countStream = StreamController.broadcast(); + Future waitForCount(int count) { + if (updateCount == count) { + return Future.value(updateCount); + } + + final completer = Completer(); + + StreamSubscription? sub; + sub = countStream.stream.listen((newCount) { + if (newCount == count) { + sub?.cancel(); + completer.complete(count); + } + }); + + return completer.future.timeout(Duration(seconds: 1), + onTimeout: () async { + throw TimeoutException( + 'Failed to wait for updateCount == $count, current == $updateCount', + Duration(seconds: 1)); + }); + } + late Client client; late Room room; late Timeline timeline; - test('create stuff', () async { + setUp(() async { try { await olm.init(); olm.get_library_version(); @@ -56,21 +83,33 @@ void main() { chunk: TimelineChunk(events: [], nextBatch: 't456', prevBatch: 't123'), onUpdate: () { updateCount++; + countStream.add(updateCount); }, onInsert: insertList.add, onChange: changeList.add, onRemove: removeList.add, ); - expect(timeline.isFragmentedTimeline, true); expect(timeline.allowNewEvent, false); + updateCount = 0; + insertList.clear(); + changeList.clear(); + removeList.clear(); + + await client.abortSync(); + testTimeStamp = DateTime.now().millisecondsSinceEpoch; }); + tearDown(() => client.dispose(closeDatabase: true).onError((e, s) {})); + test('Request future', () async { timeline.events.clear(); + FakeMatrixApi.calledEndpoints.clear(); + await timeline.requestFuture(); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/messages?from=t456&dir=f')); expect(updateCount, 3); expect(insertList, [0, 1, 2]); @@ -86,13 +125,12 @@ void main() { /// We send a message in a fragmented timeline, it didn't reached the end so we shouldn't be displayed. test('Send message not displayed', () async { - updateCount = 0; - await room.sendTextEvent('test', txid: '1234'); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/1234')); expect(updateCount, 0); - expect(insertList, [0, 1, 2]); + expect(insertList, []); expect(insertList.length, timeline.events.length); // expect no new events to have been added @@ -111,20 +149,21 @@ void main() { }, )); // just assume that it was on the server for this call but not for the following. - await Future.delayed(Duration(milliseconds: 50)); - expect(updateCount, 0); - expect(insertList, [0, 1, 2]); + expect(insertList, []); expect(timeline.events.length, - 3); // we still expect the timeline to contain the same numbre of elements + 0); // we still expect the timeline to contain the same numbre of elements }); test('Request future end of timeline', () async { + FakeMatrixApi.calledEndpoints.clear(); + await timeline.requestFuture(); await timeline.requestFuture(); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/messages?from=t789&dir=f')); - expect(updateCount, 3); + expect(updateCount, 6); expect(insertList, [0, 1, 2]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org'); @@ -137,10 +176,15 @@ void main() { }); test('Send message', () async { + FakeMatrixApi.calledEndpoints.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); await room.sendTextEvent('test', txid: '1234'); - await Future.delayed(Duration(milliseconds: 50)); - expect(updateCount, 5); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/1234')); + + expect(updateCount, 8); expect(insertList, [0, 1, 2, 0]); expect(insertList.length, timeline.events.length); final eventId = timeline.events[0].eventId; @@ -161,9 +205,9 @@ void main() { }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(9); - expect(updateCount, 6); + expect(updateCount, 9); expect(insertList, [0, 1, 2, 0]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].eventId, eventId); @@ -171,7 +215,10 @@ void main() { }); test('Send message with error', () async { - updateCount = 0; + await timeline.requestFuture(); + await timeline.requestFuture(); + await waitForCount(6); + client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -184,22 +231,31 @@ void main() { 'origin_server_ts': testTimeStamp }, )); - await Future.delayed(Duration(milliseconds: 50)); - expect(updateCount, 1); - await room.sendTextEvent('test', txid: 'errortxid'); - await Future.delayed(Duration(milliseconds: 50)); - - expect(updateCount, 3); - await room.sendTextEvent('test', txid: 'errortxid2'); - await Future.delayed(Duration(milliseconds: 50)); - await room.sendTextEvent('test', txid: 'errortxid3'); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(updateCount, 7); - expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2]); + + FakeMatrixApi.calledEndpoints.clear(); + + await room.sendTextEvent('test', txid: 'errortxid'); + + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/errortxid')); + + await waitForCount(9); + expect(updateCount, 9); + await room.sendTextEvent('test', txid: 'errortxid2'); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/errortxid2')); + await room.sendTextEvent('test', txid: 'errortxid3'); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/errortxid3')); + + expect(updateCount, 13); + expect(insertList, [0, 1, 2, 0, 0, 1, 2]); expect(insertList.length, timeline.events.length); - expect(changeList, [0, 0, 0, 1, 2]); + expect(changeList, [0, 1, 2]); expect(removeList, []); expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[1].status, EventStatus.error); @@ -207,21 +263,51 @@ void main() { }); test('Remove message', () async { - updateCount = 0; + await timeline.requestFuture(); + await timeline.requestFuture(); + // send a failed message + client.onEvent.add(EventUpdate( + type: EventUpdateType.timeline, + roomID: roomID, + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': EventStatus.sending.intValue, + 'event_id': 'abc', + 'origin_server_ts': testTimeStamp + }, + )); + await waitForCount(7); + await timeline.events[0].remove(); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); + expect(updateCount, 8); - expect(updateCount, 1); - - expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2]); - expect(changeList, [0, 0, 0, 1, 2]); + expect(insertList, [0, 1, 2, 0]); + expect(changeList, []); expect(removeList, [0]); - expect(timeline.events.length, 7); - expect(timeline.events[0].status, EventStatus.error); + expect(timeline.events.length, 3); + expect(timeline.events[0].status, EventStatus.synced); }); test('getEventById', () async { + await timeline.requestFuture(); + await timeline.requestFuture(); + client.onEvent.add(EventUpdate( + type: EventUpdateType.timeline, + roomID: roomID, + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': EventStatus.sending.intValue, + 'event_id': 'abc', + 'origin_server_ts': testTimeStamp + }, + )); + await waitForCount(7); var event = await timeline.getEventById('abc'); expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'}); @@ -240,7 +326,8 @@ void main() { test('Resend message', () async { timeline.events.clear(); - updateCount = 0; + await timeline.requestFuture(); + await timeline.requestFuture(); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -254,32 +341,46 @@ void main() { 'unsigned': {'transaction_id': 'newresend'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.error); + + FakeMatrixApi.calledEndpoints.clear(); + await timeline.events[0].sendAgain(); - await Future.delayed(Duration(milliseconds: 50)); + await FakeMatrixApi.firstWhere((a) => a.startsWith( + '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/newresend')); - expect(updateCount, 3); + expect(updateCount, 9); - expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2, 0]); - expect(changeList, [0, 0, 0, 1, 2, 0, 0]); - expect(removeList, [0]); - expect(timeline.events.length, 1); + expect(insertList, [0, 1, 2, 0]); + expect(changeList, [0, 0]); + expect(removeList, []); + expect(timeline.events.length, 4); expect(timeline.events[0].status, EventStatus.sent); }); test('Clear cache on limited timeline', () async { - client.onSync.add( + FakeMatrixApi.calledEndpoints.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); + await client.handleSync( SyncUpdate( nextBatch: '1234', rooms: RoomsUpdate( join: { roomID: JoinedRoomUpdate( - timeline: TimelineUpdate( - limited: true, - prevBatch: 'blah', - ), + timeline: + TimelineUpdate(limited: true, prevBatch: 'blah', events: [ + MatrixEvent( + eventId: '\$somerandomfox', + type: 'm.room.message', + content: {'msgtype': 'm.text', 'body': 'Testcase'}, + senderId: '@alice:example.com', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(testTimeStamp), + ), + ]), unreadNotifications: UnreadNotificationCounts( highlightCount: 0, notificationCount: 0, @@ -289,12 +390,14 @@ void main() { ), ), ); - await Future.delayed(Duration(milliseconds: 50)); - expect(timeline.events.isEmpty, true); + await waitForCount(7); + expect(timeline.events.length, 1); }); test('sort errors on top', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -319,13 +422,15 @@ void main() { 'origin_server_ts': testTimeStamp + 5 }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[1].status, EventStatus.synced); }); test('sending event to failed update', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -338,9 +443,9 @@ void main() { 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.sending); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -353,11 +458,13 @@ void main() { 'origin_server_ts': testTimeStamp }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.error); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); }); test('setReadMarker', () async { + await timeline.requestFuture(); + await timeline.requestFuture(); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -370,7 +477,8 @@ void main() { 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); + room.notificationCount = 1; await timeline.setReadMarker(); expect(room.notificationCount, 0); @@ -378,6 +486,9 @@ void main() { test('sending an event and the http request finishes first, 0 -> 1 -> 2', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); + client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -390,9 +501,9 @@ void main() { 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.sending); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -406,9 +517,9 @@ void main() { 'unsigned': {'transaction_id': 'transaction'} }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.sent); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -422,13 +533,16 @@ void main() { 'unsigned': {'transaction_id': 'transaction'} }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(9); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); }); test('sending an event where the sync reply arrives first, 0 -> 2 -> 1', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); + client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -444,9 +558,9 @@ void main() { }, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.sending); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -462,9 +576,9 @@ void main() { }, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -480,12 +594,15 @@ void main() { }, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(9); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); }); test('sending an event 0 -> -1 -> 2', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); + client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -498,9 +615,9 @@ void main() { 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.sending); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -513,9 +630,9 @@ void main() { 'unsigned': {'transaction_id': 'transaction'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.error); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -529,12 +646,15 @@ void main() { 'unsigned': {'transaction_id': 'transaction'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(9); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); }); test('sending an event 0 -> 2 -> -1', () async { timeline.events.clear(); + await timeline.requestFuture(); + await timeline.requestFuture(); + client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -547,9 +667,9 @@ void main() { 'origin_server_ts': DateTime.now().millisecondsSinceEpoch, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(7); expect(timeline.events[0].status, EventStatus.sending); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -563,9 +683,9 @@ void main() { 'unsigned': {'transaction_id': 'transaction'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(8); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -578,9 +698,9 @@ void main() { 'unsigned': {'transaction_id': 'transaction'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(9); expect(timeline.events[0].status, EventStatus.synced); - expect(timeline.events.length, 1); + expect(timeline.events.length, 4); }); test('logout', () async { await client.logout(); diff --git a/test/timeline_test.dart b/test/timeline_test.dart index c864735d..181aebfe 100644 --- a/test/timeline_test.dart +++ b/test/timeline_test.dart @@ -16,28 +16,57 @@ * along with this program. If not, see . */ +import 'dart:async'; +import 'dart:math'; + import 'package:matrix/matrix.dart'; import 'package:matrix/src/models/timeline_chunk.dart'; import 'package:test/test.dart'; import 'package:olm/olm.dart' as olm; import 'fake_client.dart'; +import 'fake_matrix_api.dart'; void main() { group('Timeline', () { Logs().level = Level.error; final roomID = '!1234:example.com'; - final testTimeStamp = DateTime.now().millisecondsSinceEpoch; + var testTimeStamp = 0; var updateCount = 0; final insertList = []; final changeList = []; final removeList = []; var olmEnabled = true; + var currentPoison = 0; + + final countStream = StreamController.broadcast(); + Future waitForCount(int count) { + if (updateCount == count) { + return Future.value(updateCount); + } + + final completer = Completer(); + + StreamSubscription? sub; + sub = countStream.stream.listen((newCount) { + if (newCount == count) { + sub?.cancel(); + completer.complete(count); + } + }); + + return completer.future.timeout(Duration(seconds: 1), + onTimeout: () async { + throw TimeoutException( + 'Failed to wait for updateCount == $count, current == $updateCount', + Duration(seconds: 1)); + }); + } late Client client; late Room room; late Timeline timeline; - test('create stuff', () async { + setUp(() async { try { await olm.init(); olm.get_library_version(); @@ -49,24 +78,40 @@ void main() { client = await getClient(); client.sendMessageTimeoutSeconds = 5; + final poison = Random().nextInt(2 ^ 32); + currentPoison = poison; + room = Room( id: roomID, client: client, prev_batch: '1234', roomAccountData: {}); timeline = Timeline( room: room, chunk: TimelineChunk(events: []), onUpdate: () { + if (poison != currentPoison) return; updateCount++; + countStream.add(updateCount); }, onInsert: insertList.add, onChange: changeList.add, onRemove: removeList.add, ); - }); - test('Create', () async { await client.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), checkWellKnown: false); + await client.abortSync(); + updateCount = 0; + insertList.clear(); + changeList.clear(); + removeList.clear(); + + await client.abortSync(); + testTimeStamp = DateTime.now().millisecondsSinceEpoch; + }); + + tearDown(() => client.dispose(closeDatabase: true).onError((e, s) {})); + + test('Create', () async { client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -94,7 +139,7 @@ void main() { expect(timeline.sub != null, true); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(2); expect(updateCount, 2); expect(insertList, [0, 0]); @@ -143,7 +188,7 @@ void main() { }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(3); expect(updateCount, 3); expect(insertList, [0, 0, 0]); @@ -157,10 +202,9 @@ void main() { test('Send message', () async { await room.sendTextEvent('test', txid: '1234'); - await Future.delayed(Duration(milliseconds: 50)); - - expect(updateCount, 5); - expect(insertList, [0, 0, 0, 0]); + await waitForCount(2); + expect(updateCount, 2); + expect(insertList, [0]); expect(insertList.length, timeline.events.length); final eventId = timeline.events[0].eventId; expect(eventId.startsWith('\$event'), true); @@ -180,16 +224,52 @@ void main() { }, )); - await Future.delayed(Duration(milliseconds: 50)); - - expect(updateCount, 6); - expect(insertList, [0, 0, 0, 0]); + await waitForCount(3); + expect(updateCount, 3); + expect(insertList, [0]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].eventId, eventId); expect(timeline.events[0].status, EventStatus.synced); }); test('Send message with error', () async { + client.onEvent.add(EventUpdate( + type: EventUpdateType.timeline, + roomID: roomID, + content: { + 'type': 'm.room.message', + 'content': { + 'msgtype': 'm.text', + 'body': 'Testcase should not show up in Sync' + }, + 'sender': '@alice:example.com', + 'status': EventStatus.sending.intValue, + 'event_id': 'abc', + 'origin_server_ts': testTimeStamp + }, + )); + await waitForCount(1); + + await room.sendTextEvent('test', txid: 'errortxid'); + await waitForCount(3); + + await room.sendTextEvent('test', txid: 'errortxid2'); + await waitForCount(5); + await room.sendTextEvent('test', txid: 'errortxid3'); + await waitForCount(7); + + expect(updateCount, 7); + expect(insertList, [0, 0, 1, 2]); + expect(insertList.length, timeline.events.length); + expect(changeList, [0, 1, 2]); + expect(removeList, []); + expect(timeline.events[0].status, EventStatus.error); + expect(timeline.events[1].status, EventStatus.error); + expect(timeline.events[2].status, EventStatus.error); + }); + + test('Remove message', () async { + // send a failed message client.onEvent.add(EventUpdate( type: EventUpdateType.timeline, roomID: roomID, @@ -202,43 +282,32 @@ void main() { 'origin_server_ts': testTimeStamp }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(1); - expect(updateCount, 7); - await room.sendTextEvent('test', txid: 'errortxid'); - await Future.delayed(Duration(milliseconds: 50)); - - expect(updateCount, 9); - await room.sendTextEvent('test', txid: 'errortxid2'); - await Future.delayed(Duration(milliseconds: 50)); - await room.sendTextEvent('test', txid: 'errortxid3'); - await Future.delayed(Duration(milliseconds: 50)); - - expect(updateCount, 13); - expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2]); - expect(insertList.length, timeline.events.length); - expect(changeList, [2, 0, 0, 0, 1, 2]); - expect(removeList, []); - expect(timeline.events[0].status, EventStatus.error); - expect(timeline.events[1].status, EventStatus.error); - expect(timeline.events[2].status, EventStatus.error); - }); - - test('Remove message', () async { await timeline.events[0].remove(); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(2); - expect(updateCount, 14); - - expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2]); - expect(changeList, [2, 0, 0, 0, 1, 2]); + expect(insertList, [0]); + expect(changeList, []); expect(removeList, [0]); - expect(timeline.events.length, 7); - expect(timeline.events[0].status, EventStatus.error); + expect(timeline.events.length, 0); }); test('getEventById', () async { + client.onEvent.add(EventUpdate( + type: EventUpdateType.timeline, + roomID: roomID, + content: { + 'type': 'm.room.message', + 'content': {'msgtype': 'm.text', 'body': 'Testcase'}, + 'sender': '@alice:example.com', + 'status': EventStatus.sending.intValue, + 'event_id': 'abc', + 'origin_server_ts': testTimeStamp + }, + )); + await waitForCount(1); var event = await timeline.getEventById('abc'); expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'}); @@ -270,17 +339,17 @@ void main() { 'unsigned': {'transaction_id': 'newresend'}, }, )); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(1); expect(timeline.events[0].status, EventStatus.error); await timeline.events[0].sendAgain(); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(3); - expect(updateCount, 17); + expect(updateCount, 3); - expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2, 0]); - expect(changeList, [2, 0, 0, 0, 1, 2, 0, 0]); - expect(removeList, [0]); + expect(insertList, [0]); + expect(changeList, [0, 0]); + expect(removeList, []); expect(timeline.events.length, 1); expect(timeline.events[0].status, EventStatus.sent); }); @@ -290,10 +359,10 @@ void main() { expect(timeline.canRequestHistory, true); await room.requestHistory(); - await Future.delayed(Duration(milliseconds: 50)); + await waitForCount(3); - expect(updateCount, 20); - expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 1, 2]); + expect(updateCount, 3); + expect(insertList, [0, 1, 2]); expect(timeline.events.length, 3); expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org'); expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org'); diff --git a/test/user_test.dart b/test/user_test.dart index 491f5caa..bead02cd 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -163,6 +163,7 @@ void main() { expect(user2.mentionFragments, {'@Bob', '@Bob#1542'}); }); test('dispose client', () async { + await Future.delayed(Duration(milliseconds: 50)); await client.dispose(closeDatabase: true); }); });