fix: race conditions in the SDK and its tests

This commit is contained in:
Nicolas Werner 2022-07-13 00:36:49 +00:00
parent 0b031476b8
commit 6e211f5a81
15 changed files with 833 additions and 460 deletions

View File

@ -417,10 +417,10 @@ class Encryption {
} }
} }
void dispose() { Future<void> dispose() async {
_backgroundTasksRunning = false; _backgroundTasksRunning = false;
keyManager.dispose(); keyManager.dispose();
olmManager.dispose(); await olmManager.dispose();
keyVerificationManager.dispose(); keyVerificationManager.dispose();
} }
} }

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:matrix/encryption/utils/base64_unpadded.dart'; import 'package:matrix/encryption/utils/base64_unpadded.dart';
@ -85,7 +86,7 @@ class KeyManager {
_inboundGroupSessions.clear(); _inboundGroupSessions.clear();
} }
void setInboundGroupSession( Future<void> setInboundGroupSession(
String roomId, String roomId,
String sessionId, String sessionId,
String senderKey, String senderKey,
@ -98,7 +99,7 @@ class KeyManager {
final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{}; final senderClaimedKeys_ = senderClaimedKeys ?? <String, String>{};
final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{}; final allowedAtIndex_ = allowedAtIndex ?? <String, Map<String, int>>{};
final userId = client.userID; final userId = client.userID;
if (userId == null) return; if (userId == null) return Future.value();
if (!senderClaimedKeys_.containsKey('ed25519')) { if (!senderClaimedKeys_.containsKey('ed25519')) {
final device = client.getUserDeviceKeysByCurve25519Key(senderKey); final device = client.getUserDeviceKeysByCurve25519Key(senderKey);
@ -109,7 +110,7 @@ class KeyManager {
final oldSession = final oldSession =
getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false); getInboundGroupSession(roomId, sessionId, senderKey, otherRooms: false);
if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) { if (content['algorithm'] != AlgorithmTypes.megolmV1AesSha2) {
return; return Future.value();
} }
late olm.InboundGroupSession inboundGroupSession; late olm.InboundGroupSession inboundGroupSession;
try { try {
@ -122,7 +123,7 @@ class KeyManager {
} catch (e, s) { } catch (e, s) {
inboundGroupSession.free(); inboundGroupSession.free();
Logs().e('[LibOlm] Could not create new InboundGroupSession', e, s); Logs().e('[LibOlm] Could not create new InboundGroupSession', e, s);
return; return Future.value();
} }
final newSession = SessionKey( final newSession = SessionKey(
content: content, content: content,
@ -148,16 +149,16 @@ class KeyManager {
} else { } else {
// we are gonna keep our old session // we are gonna keep our old session
newSession.dispose(); newSession.dispose();
return; return Future.value();
} }
final roomInboundGroupSessions = final roomInboundGroupSessions =
_inboundGroupSessions[roomId] ??= <String, SessionKey>{}; _inboundGroupSessions[roomId] ??= <String, SessionKey>{};
roomInboundGroupSessions[sessionId] = newSession; roomInboundGroupSessions[sessionId] = newSession;
if (!client.isLogged() || client.encryption == null) { if (!client.isLogged() || client.encryption == null) {
return; return Future.value();
} }
client.database final storeFuture = client.database
?.storeInboundGroupSession( ?.storeInboundGroupSession(
roomId, roomId,
sessionId, sessionId,
@ -188,6 +189,8 @@ class KeyManager {
// and finally broadcast the new session // and finally broadcast the new session
room.onSessionKeyReceived.add(sessionId); room.onSessionKeyReceived.add(sessionId);
} }
return storeFuture ?? Future.value();
} }
SessionKey? getInboundGroupSession( SessionKey? getInboundGroupSession(
@ -503,7 +506,7 @@ class KeyManager {
allowedAtIndex[device.userId]![device.curve25519Key!] = allowedAtIndex[device.userId]![device.curve25519Key!] =
outboundGroupSession.message_index(); outboundGroupSession.message_index();
} }
setInboundGroupSession( await setInboundGroupSession(
roomId, rawSession['session_id'], encryption.identityKey!, rawSession, roomId, rawSession['session_id'], encryption.identityKey!, rawSession,
allowedAtIndex: allowedAtIndex); allowedAtIndex: allowedAtIndex);
final sess = OutboundGroupSession( final sess = OutboundGroupSession(
@ -612,7 +615,7 @@ class KeyManager {
if (decrypted != null) { if (decrypted != null) {
decrypted['session_id'] = sessionId; decrypted['session_id'] = sessionId;
decrypted['room_id'] = roomId; decrypted['room_id'] = roomId;
setInboundGroupSession( await setInboundGroupSession(
roomId, sessionId, decrypted['sender_key'], decrypted, roomId, sessionId, decrypted['sender_key'], decrypted,
forwarded: true, forwarded: true,
senderClaimedKeys: decrypted['sender_claimed_keys'] != null senderClaimedKeys: decrypted['sender_claimed_keys'] != null
@ -901,7 +904,7 @@ class KeyManager {
.add(encryptedContent['sender_key']); .add(encryptedContent['sender_key']);
// TODO: verify that the keys work to decrypt a message // TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session // alright, all checks out, let's go ahead and store this session
setInboundGroupSession( await setInboundGroupSession(
request.room.id, request.sessionId, request.senderKey, event.content, request.room.id, request.sessionId, request.senderKey, event.content,
forwarded: true, forwarded: true,
senderClaimedKeys: { senderClaimedKeys: {
@ -946,7 +949,7 @@ class KeyManager {
event.content['sender_claimed_ed25519_key'] = sender_ed25519; event.content['sender_claimed_ed25519_key'] = sender_ed25519;
} }
Logs().v('[KeyManager] Keeping room key'); Logs().v('[KeyManager] Keeping room key');
setInboundGroupSession( await setInboundGroupSession(
roomId, sessionId, encryptedContent['sender_key'], event.content, roomId, sessionId, encryptedContent['sender_key'], event.content,
forwarded: false); forwarded: false);
} }

View File

@ -18,6 +18,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:async/async.dart';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -109,6 +110,7 @@ class OlmManager {
} }
bool _uploadKeysLock = false; bool _uploadKeysLock = false;
CancelableOperation<Map<String, int>>? currentUpload;
/// Generates new one time keys, signs everything and upload it to the server. /// Generates new one time keys, signs everything and upload it to the server.
Future<bool> uploadKeys({ Future<bool> uploadKeys({
@ -211,13 +213,20 @@ class OlmManager {
} }
// Workaround: Make sure we stop if we got logged out in the meantime. // Workaround: Make sure we stop if we got logged out in the meantime.
if (!client.isLogged()) return true; if (!client.isLogged()) return true;
final response = await client.uploadKeys( final currentUpload =
this.currentUpload = CancelableOperation.fromFuture(client.uploadKeys(
deviceKeys: uploadDeviceKeys deviceKeys: uploadDeviceKeys
? MatrixDeviceKeys.fromJson(keysContent['device_keys']) ? MatrixDeviceKeys.fromJson(keysContent['device_keys'])
: null, : null,
oneTimeKeys: signedOneTimeKeys, oneTimeKeys: signedOneTimeKeys,
fallbackKeys: signedFallbackKeys, fallbackKeys: signedFallbackKeys,
); ));
final response = await currentUpload.valueOrCancellation();
if (response == null) {
_uploadKeysLock = false;
return false;
}
// mark the OTKs as published and save that to datbase // mark the OTKs as published and save that to datbase
_olmAccount.mark_keys_as_published(); _olmAccount.mark_keys_as_published();
if (updateDatabase) { if (updateDatabase) {
@ -231,8 +240,8 @@ class OlmManager {
} }
} }
void handleDeviceOneTimeKeysCount( Future<void> handleDeviceOneTimeKeysCount(
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) { Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) async {
if (!enabled) { if (!enabled) {
return; return;
} }
@ -255,13 +264,13 @@ class OlmManager {
final requestingKeysFrom = { final requestingKeysFrom = {
client.userID!: {client.deviceID!: 'signed_curve25519'} 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 // 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) || if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ||
!unusedFallbackKey) { !unusedFallbackKey) {
uploadKeys( await uploadKeys(
oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
? keyCount ? keyCount
: null, : null,
@ -293,7 +302,7 @@ class OlmManager {
DateTime.now().millisecondsSinceEpoch); DateTime.now().millisecondsSinceEpoch);
} }
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) { Future<ToDeviceEvent> _decryptToDeviceEvent(ToDeviceEvent event) async {
if (event.type != EventTypes.Encrypted) { if (event.type != EventTypes.Encrypted) {
return event; return event;
} }
@ -341,12 +350,12 @@ class OlmManager {
throw DecryptException( throw DecryptException(
DecryptException.decryptionFailed, e.toString()); DecryptException.decryptionFailed, e.toString());
} }
updateSessionUsage(session); await updateSessionUsage(session);
break; break;
} else if (type == 1) { } else if (type == 1) {
try { try {
plaintext = session.session!.decrypt(type, body); plaintext = session.session!.decrypt(type, body);
updateSessionUsage(session); await updateSessionUsage(session);
break; break;
} catch (_) { } catch (_) {
plaintext = null; plaintext = null;
@ -363,16 +372,16 @@ class OlmManager {
try { try {
newSession.create_inbound_from(_olmAccount!, senderKey, body); newSession.create_inbound_from(_olmAccount!, senderKey, body);
_olmAccount!.remove_one_time_keys(newSession); _olmAccount!.remove_one_time_keys(newSession);
client.database?.updateClientKeys(pickledOlmAccount!); await client.database?.updateClientKeys(pickledOlmAccount!);
plaintext = newSession.decrypt(type, body); plaintext = newSession.decrypt(type, body);
runInRoot(() => storeOlmSession(OlmSession( await runInRoot(() => storeOlmSession(OlmSession(
key: client.userID!, key: client.userID!,
identityKey: senderKey, identityKey: senderKey,
sessionId: newSession.session_id(), sessionId: newSession.session_id(),
session: newSession, session: newSession,
lastReceived: DateTime.now(), lastReceived: DateTime.now(),
))); )));
updateSessionUsage(); await updateSessionUsage();
} catch (e) { } catch (e) {
newSession.free(); newSession.free();
throw DecryptException(DecryptException.decryptionFailed, e.toString()); throw DecryptException(DecryptException.decryptionFailed, e.toString());
@ -479,7 +488,7 @@ class OlmManager {
await loadFromDb(); await loadFromDb();
} }
try { try {
event = _decryptToDeviceEvent(event); event = await _decryptToDeviceEvent(event);
if (event.type != EventTypes.Encrypted || !(await loadFromDb())) { if (event.type != EventTypes.Encrypted || !(await loadFromDb())) {
return event; return event;
} }
@ -566,14 +575,14 @@ class OlmManager {
final encryptResult = sess.first.session!.encrypt(json.encode(fullPayload)); final encryptResult = sess.first.session!.encrypt(json.encode(fullPayload));
await storeOlmSession(sess.first); await storeOlmSession(sess.first);
if (client.database != null) { if (client.database != null) {
// ignore: unawaited_futures await runInRoot(
runInRoot(() => client.database?.setLastSentMessageUserDeviceKey( () async => client.database?.setLastSentMessageUserDeviceKey(
json.encode({ json.encode({
'type': type, 'type': type,
'content': payload, 'content': payload,
}), }),
device.userId, device.userId,
device.deviceId!)); device.deviceId!));
} }
final encryptedBody = <String, dynamic>{ final encryptedBody = <String, dynamic>{
'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, 'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2,
@ -651,7 +660,8 @@ class OlmManager {
} }
} }
void dispose() { Future<void> dispose() async {
await currentUpload?.cancel();
for (final sessions in olmSessions.values) { for (final sessions in olmSessions.values) {
for (final sess in sessions) { for (final sess in sessions) {
sess.dispose(); sess.dispose();

View File

@ -221,6 +221,16 @@ class KeyVerification {
await cancel('m.unknown_method'); await cancel('m.unknown_method');
return; 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); setState(KeyVerificationState.askAccept);
break; break;
case 'm.key.verification.ready': case 'm.key.verification.ready':
@ -248,6 +258,16 @@ class KeyVerification {
await cancel('m.unknown_method'); await cancel('m.unknown_method');
return; 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 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 // as such, we better set it *before* we send our start
lastStep = type; lastStep = type;
@ -291,6 +311,16 @@ class KeyVerification {
await cancel('m.unknown_method'); await cancel('m.unknown_method');
return; 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); method = _makeVerificationMethod(payload['method'], this);
if (lastStep == null) { if (lastStep == null) {
// validate the start time // validate the start time
@ -352,17 +382,17 @@ class KeyVerification {
String? recoveryKey, String? recoveryKey,
String? keyOrPassphrase, String? keyOrPassphrase,
bool skip = false}) async { bool skip = false}) async {
final next = () { final next = () async {
if (_nextAction == 'request') { if (_nextAction == 'request') {
sendStart(); await sendStart();
} else if (_nextAction == 'done') { } else if (_nextAction == 'done') {
// and now let's sign them all in the background // and now let's sign them all in the background
encryption.crossSigning.sign(_verifiedDevices); unawaited(encryption.crossSigning.sign(_verifiedDevices));
setState(KeyVerificationState.done); setState(KeyVerificationState.done);
} }
}; };
if (skip) { if (skip) {
next(); await next();
return; return;
} }
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning); final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
@ -371,7 +401,7 @@ class KeyVerification {
recoveryKey: recoveryKey, recoveryKey: recoveryKey,
keyOrPassphrase: keyOrPassphrase); keyOrPassphrase: keyOrPassphrase);
await handle.maybeCacheAll(); await handle.maybeCacheAll();
next(); await next();
} }
/// called when the user accepts an incoming verification /// called when the user accepts an incoming verification
@ -512,9 +542,9 @@ class KeyVerification {
encryption.crossSigning.signable(_verifiedDevices)) { encryption.crossSigning.signable(_verifiedDevices)) {
// these keys can be signed! Let's do so // these keys can be signed! Let's do so
if (await encryption.crossSigning.isCached()) { if (await encryption.crossSigning.isCached()) {
// and now let's sign them all in the background // we want to make sure the verification state is correct for the other party after this event is handled.
// ignore: unawaited_futures // Otherwise the verification dialog might be stuck in an unverified but done state for a bit.
encryption.crossSigning.sign(_verifiedDevices); await encryption.crossSigning.sign(_verifiedDevices);
} else if (!wasUnknownSession) { } else if (!wasUnknownSession) {
askingSSSS = true; askingSSSS = true;
} }

View File

@ -1307,7 +1307,7 @@ class Client extends MatrixApi {
if (isLogged()) return; if (isLogged()) return;
} }
// we aren't logged in // we aren't logged in
encryption?.dispose(); await encryption?.dispose();
encryption = null; encryption = null;
onLoginStateChanged.add(LoginState.loggedOut); onLoginStateChanged.add(LoginState.loggedOut);
Logs().i('User is not logged in.'); Logs().i('User is not logged in.');
@ -1315,14 +1315,14 @@ class Client extends MatrixApi {
return; return;
} }
encryption?.dispose(); await encryption?.dispose();
try { try {
// make sure to throw an exception if libolm doesn't exist // make sure to throw an exception if libolm doesn't exist
await olm.init(); await olm.init();
olm.get_library_version(); olm.get_library_version();
encryption = Encryption(client: this); encryption = Encryption(client: this);
} catch (_) { } catch (_) {
encryption?.dispose(); await encryption?.dispose();
encryption = null; encryption = null;
} }
await encryption?.init(olmAccount); await encryption?.init(olmAccount);
@ -1408,7 +1408,7 @@ class Client extends MatrixApi {
_id = accessToken = syncFilterId = _id = accessToken = syncFilterId =
homeserver = _userID = _deviceID = _deviceName = prevBatch = null; homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
_rooms = []; _rooms = [];
encryption?.dispose(); await encryption?.dispose();
encryption = null; encryption = null;
final databaseDestroyer = this.databaseDestroyer; final databaseDestroyer = this.databaseDestroyer;
if (databaseDestroyer != null) { if (databaseDestroyer != null) {
@ -1443,18 +1443,14 @@ class Client extends MatrixApi {
return _sync(); return _sync();
} }
Future<void> _sync() async { Future<void> _sync() {
if (_currentSync == null) { final _currentSync = this._currentSync ??= _innerSync().whenComplete(() {
final _currentSync = this._currentSync = _innerSync(); this._currentSync = null;
// ignore: unawaited_futures if (_backgroundSync && isLogged() && !_disposed) {
_currentSync.whenComplete(() { _sync();
this._currentSync = null; }
if (_backgroundSync && isLogged() && !_disposed) { });
_sync(); return _currentSync;
}
});
}
await _currentSync;
} }
/// Presence that is set on sync. /// Presence that is set on sync.
@ -2112,7 +2108,7 @@ class Client extends MatrixApi {
final Map<String, DateTime> _keyQueryFailures = {}; final Map<String, DateTime> _keyQueryFailures = {};
Future<void> updateUserDeviceKeys() async { Future<void> updateUserDeviceKeys({Set<String>? additionalUsers}) async {
try { try {
final database = this.database; final database = this.database;
if (!isLogged() || database == null) return; if (!isLogged() || database == null) return;
@ -2120,6 +2116,7 @@ class Client extends MatrixApi {
final trackedUserIds = await _getUserIdsInEncryptedRooms(); final trackedUserIds = await _getUserIdsInEncryptedRooms();
if (!isLogged()) return; if (!isLogged()) return;
trackedUserIds.add(userID!); trackedUserIds.add(userID!);
if (additionalUsers != null) trackedUserIds.addAll(additionalUsers);
// Remove all userIds we no longer need to track the devices of. // Remove all userIds we no longer need to track the devices of.
_userDeviceKeys _userDeviceKeys
@ -2511,8 +2508,7 @@ class Client extends MatrixApi {
? deviceKeys.length ? deviceKeys.length
: i + chunkSize); : i + chunkSize);
// and send // and send
// ignore: unawaited_futures await sendToDeviceEncrypted(chunk, eventType, message);
sendToDeviceEncrypted(chunk, eventType, message);
} }
}(); }();
} }
@ -2665,14 +2661,15 @@ class Client extends MatrixApi {
Future<void> dispose({bool closeDatabase = true}) async { Future<void> dispose({bool closeDatabase = true}) async {
_disposed = true; _disposed = true;
await abortSync(); await abortSync();
encryption?.dispose(); await encryption?.dispose();
encryption = null; encryption = null;
try { try {
if (closeDatabase) { if (closeDatabase) {
final database = _database;
_database = null;
await database await database
?.close() ?.close()
.catchError((e, s) => Logs().w('Failed to close database: ', e, s)); .catchError((e, s) => Logs().w('Failed to close database: ', e, s));
_database = null;
} }
} catch (error, stacktrace) { } catch (error, stacktrace) {
Logs().w('Failed to close database: ', error, stacktrace); Logs().w('Failed to close database: ', error, stacktrace);

View File

@ -8,6 +8,7 @@ environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
dependencies: dependencies:
async: ^2.8.0
blurhash_dart: ^1.1.0 blurhash_dart: ^1.1.0
http: ^0.13.0 http: ^0.13.0
mime: ^1.0.0 mime: ^1.0.0

File diff suppressed because one or more lines are too long

View File

@ -288,9 +288,8 @@ void main() {
.getInboundGroupSession(roomId, sessionId, senderKey) != .getInboundGroupSession(roomId, sessionId, senderKey) !=
null, null,
false); false);
client.encryption!.keyManager await client.encryption!.keyManager
.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent); .setInboundGroupSession(roomId, sessionId, senderKey, sessionContent);
await Future.delayed(Duration(milliseconds: 10));
expect( expect(
client.encryption!.keyManager client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) != .getInboundGroupSession(roomId, sessionId, senderKey) !=
@ -395,7 +394,7 @@ void main() {
'sender_key': senderKey, 'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
expect( expect(
@ -421,7 +420,7 @@ void main() {
'sender_key': senderKey, 'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
expect( expect(
@ -447,7 +446,7 @@ void main() {
'sender_key': senderKey, 'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
expect( expect(
@ -473,7 +472,7 @@ void main() {
'sender_key': senderKey, 'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
expect( expect(
@ -499,7 +498,7 @@ void main() {
'sender_key': senderKey, 'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
expect( expect(
@ -539,8 +538,9 @@ void main() {
.remove('JLAFKJWSCS'); .remove('JLAFKJWSCS');
// Alice adds her device with same device ID but different keys // Alice adds her device with same device ID but different keys
final oldResp = FakeMatrixApi.api['POST']?['/client/v3/keys/query'](null); final oldResp =
FakeMatrixApi.api['POST']?['/client/v3/keys/query'] = (_) { FakeMatrixApi.currentApi?.api['POST']?['/client/v3/keys/query'](null);
FakeMatrixApi.currentApi?.api['POST']?['/client/v3/keys/query'] = (_) {
oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = { oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = {
'user_id': '@alice:example.com', 'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS', 'device_id': 'JLAFKJWSCS',

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:matrix/matrix.dart'; 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 /// All Tests related to the ChatTime
group('Key Verification', () { group('Key Verification', () {
Logs().level = Level.error; Logs().level = Level.error;
var olmEnabled = true;
// key @othertest:fakeServer.notExisting // key @othertest:fakeServer.notExisting
const otherPickledOlmAccount = const otherPickledOlmAccount =
@ -72,21 +84,11 @@ void main() {
late Client client1; late Client client1;
late Client client2; late Client client2;
test('setupClient', () async { setUp(() 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;
client1 = await getClient(); client1 = await getClient();
client2 = Client( client2 = Client(
'othertestclient', 'othertestclient',
httpClient: FakeMatrixApi(), httpClient: FakeMatrixApi.currentApi!,
databaseBuilder: getDatabase, databaseBuilder: getDatabase,
); );
await client2.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), await client2.checkHomeserver(Uri.parse('https://fakeserver.notexisting'),
@ -109,9 +111,12 @@ void main() {
KeyVerificationMethod.numbers KeyVerificationMethod.numbers
}; };
}); });
tearDown(() async {
await client1.dispose(closeDatabase: true);
await client2.dispose(closeDatabase: true);
});
test('Run emoji / number verification', () async { test('Run emoji / number verification', () async {
if (!olmEnabled) return;
// for a full run we test in-room verification in a cleartext room // 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 // because then we can easily intercept the payloads and inject in the other client
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
@ -122,15 +127,17 @@ void main() {
await client1.userDeviceKeys[client2.userID]!.startVerification( await client1.userDeviceKeys[client2.userID]!.startVerification(
newDirectChatEnableEncryption: false, newDirectChatEnableEncryption: false,
); );
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
var evt = getLastSentEvent(req1); var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept); expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2; final comp = Completer<KeyVerification>();
final sub = client2.onKeyVerificationRequest.stream.listen((req) { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req; comp.complete(req);
}); });
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10)); final req2 = await comp.future;
await sub.cancel(); await sub.cancel();
expect( expect(
@ -141,27 +148,37 @@ void main() {
// send ready // send ready
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification(); await req2.acceptVerification();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept); expect(req2.state, KeyVerificationState.waitingAccept);
// send start // send start
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send accept // send accept
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// receive last key // receive last key
@ -196,13 +213,22 @@ void main() {
await req1.acceptSas(); await req1.acceptSas();
evt = getLastSentEvent(req1); evt = getLastSentEvent(req1);
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); expect(req1.state, KeyVerificationState.waitingSas);
// send mac // send mac
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.acceptSas(); 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); evt = getLastSentEvent(req2);
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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(req1.state, KeyVerificationState.done);
expect(req2.state, KeyVerificationState.done); expect(req2.state, KeyVerificationState.done);
@ -219,7 +245,6 @@ void main() {
}); });
test('ask SSSS start', () async { test('ask SSSS start', () async {
if (!olmEnabled) return;
client1.userDeviceKeys[client1.userID]!.masterKey! client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(true); .setDirectVerified(true);
await client1.encryption!.ssss.clearCache(); await client1.encryption!.ssss.clearCache();
@ -227,7 +252,8 @@ void main() {
.startVerification(newDirectChatEnableEncryption: false); .startVerification(newDirectChatEnableEncryption: false);
expect(req1.state, KeyVerificationState.askSSSS); expect(req1.state, KeyVerificationState.askSSSS);
await req1.openSSSS(recoveryKey: ssssKey); 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); expect(req1.state, KeyVerificationState.waitingAccept);
await req1.cancel(); await req1.cancel();
@ -235,7 +261,6 @@ void main() {
}); });
test('ask SSSS end', () async { test('ask SSSS end', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now // make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID]!.masterKey! client1.userDeviceKeys[client1.userID]!.masterKey!
@ -245,41 +270,53 @@ void main() {
.setDirectVerified(true); .setDirectVerified(true);
final req1 = await client1.userDeviceKeys[client2.userID]! final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false); .startVerification(newDirectChatEnableEncryption: false);
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
var evt = getLastSentEvent(req1); var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept); expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2; final comp = Completer<KeyVerification>();
final sub = client2.onKeyVerificationRequest.stream.listen((req) { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req; comp.complete(req);
}); });
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10)); final req2 = await comp.future;
await sub.cancel(); await sub.cancel();
// send ready // send ready
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification(); await req2.acceptVerification();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept); expect(req2.state, KeyVerificationState.waitingAccept);
// send start // send start
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send accept // send accept
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// receive last key // receive last key
@ -313,6 +350,8 @@ void main() {
await req1.acceptSas(); await req1.acceptSas();
evt = getLastSentEvent(req1); evt = getLastSentEvent(req1);
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); expect(req1.state, KeyVerificationState.waitingSas);
// send mac // send mac
@ -320,12 +359,16 @@ void main() {
await req2.acceptSas(); await req2.acceptSas();
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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(req1.state, KeyVerificationState.askSSSS);
expect(req2.state, KeyVerificationState.done); expect(req2.state, KeyVerificationState.done);
await req1.openSSSS(recoveryKey: ssssKey); await req1.openSSSS(recoveryKey: ssssKey);
await Future.delayed(Duration(milliseconds: 10));
expect(req1.state, KeyVerificationState.done); expect(req1.state, KeyVerificationState.done);
client1.encryption!.ssss = MockSSSS(client1.encryption!); client1.encryption!.ssss = MockSSSS(client1.encryption!);
@ -334,34 +377,35 @@ void main() {
await req1.maybeRequestSSSSSecrets(); await req1.maybeRequestSSSSSecrets();
await Future.delayed(Duration(milliseconds: 10)); await Future.delayed(Duration(milliseconds: 10));
expect((client1.encryption!.ssss as MockSSSS).requestedSecrets, true); 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 client1.encryption!.keyVerificationManager.cleanup();
await client2.encryption!.keyVerificationManager.cleanup(); await client2.encryption!.keyVerificationManager.cleanup();
}); });
test('reject verification', () async { test('reject verification', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now // make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID]!.masterKey! client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(false); .setDirectVerified(false);
final req1 = await client1.userDeviceKeys[client2.userID]! final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false); .startVerification(newDirectChatEnableEncryption: false);
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
var evt = getLastSentEvent(req1); var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept); expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2; final comp = Completer<KeyVerification>();
final sub = client2.onKeyVerificationRequest.stream.listen((req) { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req; comp.complete(req);
}); });
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10)); final req2 = await comp.future;
await sub.cancel(); await sub.cancel();
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.rejectVerification(); await req2.rejectVerification();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.cancel'));
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error); expect(req1.state, KeyVerificationState.error);
@ -372,48 +416,59 @@ void main() {
}); });
test('reject sas', () async { test('reject sas', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now // make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID]!.masterKey! client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(false); .setDirectVerified(false);
final req1 = await client1.userDeviceKeys[client2.userID]! final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false); .startVerification(newDirectChatEnableEncryption: false);
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
var evt = getLastSentEvent(req1); var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept); expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2; final comp = Completer<KeyVerification>();
final sub = client2.onKeyVerificationRequest.stream.listen((req) { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req; comp.complete(req);
}); });
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10)); final req2 = await comp.future;
await sub.cancel(); await sub.cancel();
// send ready // send ready
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification(); await req2.acceptVerification();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.ready'));
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept); expect(req2.state, KeyVerificationState.waitingAccept);
// send start // send start
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send accept // send accept
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req1);
// send key // send key
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); 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); evt = getLastSentEvent(req2);
// receive last key // receive last key
@ -421,8 +476,12 @@ void main() {
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
await req1.acceptSas(); await req1.acceptSas();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.mac'));
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
await req2.rejectSas(); await req2.rejectSas();
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.key.verification.cancel'));
evt = getLastSentEvent(req2); evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt); await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error); expect(req1.state, KeyVerificationState.error);
@ -433,22 +492,23 @@ void main() {
}); });
test('other device accepted', () async { test('other device accepted', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now // make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID]!.masterKey! client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(false); .setDirectVerified(false);
final req1 = await client1.userDeviceKeys[client2.userID]! final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false); .startVerification(newDirectChatEnableEncryption: false);
await FakeMatrixApi.firstWhere((e) => e.startsWith(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/m.room.message'));
final evt = getLastSentEvent(req1); final evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept); expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2; final comp = Completer<KeyVerification>();
final sub = client2.onKeyVerificationRequest.stream.listen((req) { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req; comp.complete(req);
}); });
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt); await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10)); final req2 = await comp.future;
await sub.cancel(); await sub.cancel();
await client2.encryption!.keyVerificationManager await client2.encryption!.keyVerificationManager
@ -473,14 +533,10 @@ void main() {
expect(req2.state, KeyVerificationState.error); expect(req2.state, KeyVerificationState.error);
await req2.cancel(); 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 client1.encryption!.keyVerificationManager.cleanup();
await client2.encryption!.keyVerificationManager.cleanup(); await client2.encryption!.keyVerificationManager.cleanup();
}); });
}, skip: skip);
test('dispose client', () async {
if (!olmEnabled) return;
await client1.dispose(closeDatabase: true);
await client2.dispose(closeDatabase: true);
});
});
} }

View File

@ -33,7 +33,7 @@ void main() {
late Client client; late Client client;
test('setupClient', () async { setUp(() async {
try { try {
await olm.init(); await olm.init();
olm.get_library_version(); olm.get_library_version();
@ -42,9 +42,10 @@ void main() {
Logs().w('[LibOlm] Failed to load LibOlm', e); Logs().w('[LibOlm] Failed to load LibOlm', e);
} }
Logs().i('[LibOlm] Enabled: $olmEnabled'); Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return; if (!olmEnabled) return Future.value();
client = await getClient(); client = await getClient();
return Future.value();
}); });
test('signatures', () async { test('signatures', () async {
@ -89,10 +90,11 @@ void main() {
test('handleDeviceOneTimeKeysCount', () async { test('handleDeviceOneTimeKeysCount', () async {
if (!olmEnabled) return; if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager client.encryption!.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 20}, null); .handleDeviceOneTimeKeysCount({'signed_curve25519': 20}, null);
await Future.delayed(Duration(milliseconds: 50)); await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload');
expect( expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'),
true); true);
@ -100,14 +102,15 @@ void main() {
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager client.encryption!.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 70}, null); .handleDeviceOneTimeKeysCount({'signed_curve25519': 70}, null);
await Future.delayed(Duration(milliseconds: 50)); await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload')
.timeout(Duration(milliseconds: 50), onTimeout: () => '');
expect( expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'),
false); false);
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager.handleDeviceOneTimeKeysCount(null, []); client.encryption!.olmManager.handleDeviceOneTimeKeysCount(null, []);
await Future.delayed(Duration(milliseconds: 50)); await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload');
expect( expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'),
true); true);
@ -116,7 +119,7 @@ void main() {
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager client.encryption!.olmManager
.handleDeviceOneTimeKeysCount(null, ['signed_curve25519']); .handleDeviceOneTimeKeysCount(null, ['signed_curve25519']);
await Future.delayed(Duration(milliseconds: 50)); await FakeMatrixApi.firstWhereValue('/client/v3/keys/upload');
expect( expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'), FakeMatrixApi.calledEndpoints.containsKey('/client/v3/keys/upload'),
true); true);

View File

@ -93,13 +93,14 @@ void main() {
'sender_claimed_ed25519_key': client.fingerprintKey, 'sender_claimed_ed25519_key': client.fingerprintKey,
}; };
FakeMatrixApi.calledEndpoints.clear(); FakeMatrixApi.calledEndpoints.clear();
client.encryption!.keyManager.setInboundGroupSession( await client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload, roomId, sessionId, senderKey, sessionPayload,
forwarded: true); forwarded: true);
await Future.delayed(Duration(milliseconds: 500));
var dbSessions = await client.database!.getInboundGroupSessionsToUpload(); var dbSessions = await client.database!.getInboundGroupSessionsToUpload();
expect(dbSessions.isNotEmpty, true); expect(dbSessions.isNotEmpty, true);
await client.encryption!.keyManager.backgroundTasks(); await client.encryption!.keyManager.backgroundTasks();
await FakeMatrixApi.firstWhereValue(
'/client/v3/room_keys/keys?version=5');
final payload = FakeMatrixApi final payload = FakeMatrixApi
.calledEndpoints['/client/v3/room_keys/keys?version=5']!.first; .calledEndpoints['/client/v3/room_keys/keys?version=5']!.first;
dbSessions = await client.database!.getInboundGroupSessionsToUpload(); dbSessions = await client.database!.getInboundGroupSessionsToUpload();

View File

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:core'; import 'dart:core';
import 'dart:math'; import 'dart:math';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -35,120 +35,209 @@ Map<String, dynamic> decodeJson(dynamic data) {
return data; return data;
} }
class FakeMatrixApi extends MockClient { class FakeMatrixApi extends BaseClient {
static final calledEndpoints = <String, List<dynamic>>{}; static Map<String, List<dynamic>> get calledEndpoints =>
static int eventCounter = 0; currentApi!._calledEndpoints;
static sdk.Client? client; static int get eventCounter => currentApi!._eventCounter;
static bool failToDevice = false; static set eventCounter(int c) {
currentApi!._eventCounter = c;
}
FakeMatrixApi() static set client(sdk.Client? c) {
: super((request) async { currentApi?._client = c;
// 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;
}
if (action.endsWith('?')) { static set failToDevice(bool fail) {
action = action.substring(0, action.length - 1); currentApi?._failToDevice = fail;
} }
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'); static set trace(bool t) {
currentApi?._trace = t;
}
// Sync requests with timeout final _calledEndpoints = <String, List<dynamic>>{};
if (data is Map<String, dynamic> && data['timeout'] is String) { int _eventCounter = 0;
await Future.delayed(Duration(seconds: 5)); sdk.Client? _client;
} bool _failToDevice = false;
bool _trace = false;
final _apiCallStream = StreamController<String>.broadcast();
if (request.url.origin != 'https://fakeserver.notexisting') { static FakeMatrixApi? currentApi;
return Response(
'<html><head></head><body>Not found...</body></html>', 404);
}
// Call API static Future<String> firstWhereValue(String value) {
if (!calledEndpoints.containsKey(action)) { return firstWhere((v) => v == value);
calledEndpoints[action] = <dynamic>[]; }
}
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;
}
return Response.bytes(utf8.encode(json.encode(res)), statusCode); static Future<String> 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<String>();
StreamSubscription<String>? sub;
sub = currentApi!._apiCallStream.stream.listen((action) {
if (test(action)) {
sub?.cancel();
completer.complete(action);
}
});
return completer.future;
}
FutureOr<Response> 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<String, dynamic> && data['timeout'] is String) {
await Future.delayed(Duration(seconds: 5));
}
if (request.url.origin != 'https://fakeserver.notexisting') {
return Response(
'<html><head></head><body>Not found...</body></html>', 404);
}
// Call API
(_calledEndpoints[action] ??= <dynamic>[]).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<String, dynamic> messagesResponsePast = { unawaited(Future.delayed(Duration(milliseconds: 1)).then((_) async {
_apiCallStream.add(action);
}));
return Response.bytes(utf8.encode(json.encode(res)), statusCode);
}
@override
Future<StreamedResponse> 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<String, dynamic> messagesResponsePast = {
'start': 't47429-4392820_219380_26003_2265', 'start': 't47429-4392820_219380_26003_2265',
'end': 't47409-4357353_219380_26003_2265', 'end': 't47409-4357353_219380_26003_2265',
'chunk': [ 'chunk': [
@ -206,7 +295,7 @@ class FakeMatrixApi extends MockClient {
], ],
'state': [], 'state': [],
}; };
static Map<String, dynamic> messagesResponseFuture = { static const Map<String, dynamic> messagesResponseFuture = {
'start': 't456', 'start': 't456',
'end': 't789', 'end': 't789',
'chunk': [ 'chunk': [
@ -264,7 +353,7 @@ class FakeMatrixApi extends MockClient {
], ],
'state': [], 'state': [],
}; };
static Map<String, dynamic> messagesResponseFutureEnd = { static const Map<String, dynamic> messagesResponseFutureEnd = {
'start': 't789', 'start': 't789',
'end': null, 'end': null,
'chunk': [], 'chunk': [],
@ -856,7 +945,7 @@ class FakeMatrixApi extends MockClient {
} }
}; };
static final Map<String, Map<String, dynamic>> api = { final Map<String, Map<String, dynamic>> api = {
'GET': { 'GET': {
'/path/to/auth/error': (var req) => { '/path/to/auth/error': (var req) => {
'errcode': 'M_FORBIDDEN', '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/ban': (var reqI) => {},
'/client/v3/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {}, '/client/v3/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {},
'/client/v3/rooms/!localpart%3Aserver.abc/invite': (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/keys/signatures/upload': (var reqI) => {'failures': {}},
'/client/v3/room_keys/version': (var reqI) => {'version': '5'}, '/client/v3/room_keys/version': (var reqI) => {'version': '5'},
}, },

View File

@ -16,28 +16,55 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/src/models/timeline_chunk.dart'; import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'fake_client.dart'; import 'fake_client.dart';
import 'fake_matrix_api.dart';
void main() { void main() {
group('Timeline context', () { group('Timeline context', () {
Logs().level = Level.error; Logs().level = Level.error;
final roomID = '!1234:example.com'; final roomID = '!1234:example.com';
final testTimeStamp = DateTime.now().millisecondsSinceEpoch; var testTimeStamp = 0;
var updateCount = 0; var updateCount = 0;
final insertList = <int>[]; final insertList = <int>[];
final changeList = <int>[]; final changeList = <int>[];
final removeList = <int>[]; final removeList = <int>[];
var olmEnabled = true; var olmEnabled = true;
final countStream = StreamController<int>.broadcast();
Future<int> waitForCount(int count) {
if (updateCount == count) {
return Future.value(updateCount);
}
final completer = Completer<int>();
StreamSubscription<int>? 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 Client client;
late Room room; late Room room;
late Timeline timeline; late Timeline timeline;
test('create stuff', () async { setUp(() async {
try { try {
await olm.init(); await olm.init();
olm.get_library_version(); olm.get_library_version();
@ -56,21 +83,33 @@ void main() {
chunk: TimelineChunk(events: [], nextBatch: 't456', prevBatch: 't123'), chunk: TimelineChunk(events: [], nextBatch: 't456', prevBatch: 't123'),
onUpdate: () { onUpdate: () {
updateCount++; updateCount++;
countStream.add(updateCount);
}, },
onInsert: insertList.add, onInsert: insertList.add,
onChange: changeList.add, onChange: changeList.add,
onRemove: removeList.add, onRemove: removeList.add,
); );
expect(timeline.isFragmentedTimeline, true); expect(timeline.isFragmentedTimeline, true);
expect(timeline.allowNewEvent, false); 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 { test('Request future', () async {
timeline.events.clear(); timeline.events.clear();
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=t456&dir=f'));
expect(updateCount, 3); expect(updateCount, 3);
expect(insertList, [0, 1, 2]); 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. /// 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 { test('Send message not displayed', () async {
updateCount = 0;
await room.sendTextEvent('test', txid: '1234'); 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(updateCount, 0);
expect(insertList, [0, 1, 2]); expect(insertList, []);
expect(insertList.length, expect(insertList.length,
timeline.events.length); // expect no new events to have been added 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. )); // 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(updateCount, 0);
expect(insertList, [0, 1, 2]); expect(insertList, []);
expect(timeline.events.length, 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 { test('Request future end of timeline', () async {
FakeMatrixApi.calledEndpoints.clear();
await timeline.requestFuture();
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, [0, 1, 2]);
expect(insertList.length, timeline.events.length); expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org'); expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
@ -137,10 +176,15 @@ void main() {
}); });
test('Send message', () async { test('Send message', () async {
FakeMatrixApi.calledEndpoints.clear();
await timeline.requestFuture();
await timeline.requestFuture();
await room.sendTextEvent('test', txid: '1234'); await room.sendTextEvent('test', txid: '1234');
await Future.delayed(Duration(milliseconds: 50)); await FakeMatrixApi.firstWhere((a) => a.startsWith(
expect(updateCount, 5); '/client/v3/rooms/!1234%3Aexample.com/send/m.room.message/1234'));
expect(updateCount, 8);
expect(insertList, [0, 1, 2, 0]); expect(insertList, [0, 1, 2, 0]);
expect(insertList.length, timeline.events.length); expect(insertList.length, timeline.events.length);
final eventId = timeline.events[0].eventId; 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, [0, 1, 2, 0]);
expect(insertList.length, timeline.events.length); expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, eventId); expect(timeline.events[0].eventId, eventId);
@ -171,7 +215,10 @@ void main() {
}); });
test('Send message with error', () async { test('Send message with error', () async {
updateCount = 0; await timeline.requestFuture();
await timeline.requestFuture();
await waitForCount(6);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -184,22 +231,31 @@ void main() {
'origin_server_ts': testTimeStamp 'origin_server_ts': testTimeStamp
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 1); await waitForCount(7);
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));
expect(updateCount, 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(insertList.length, timeline.events.length);
expect(changeList, [0, 0, 0, 1, 2]); expect(changeList, [0, 1, 2]);
expect(removeList, []); expect(removeList, []);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.error); expect(timeline.events[1].status, EventStatus.error);
@ -207,21 +263,51 @@ void main() {
}); });
test('Remove message', () async { 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 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]);
expect(changeList, []);
expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2]);
expect(changeList, [0, 0, 0, 1, 2]);
expect(removeList, [0]); expect(removeList, [0]);
expect(timeline.events.length, 7); expect(timeline.events.length, 3);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.synced);
}); });
test('getEventById', () async { 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'); var event = await timeline.getEventById('abc');
expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'}); expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'});
@ -240,7 +326,8 @@ void main() {
test('Resend message', () async { test('Resend message', () async {
timeline.events.clear(); timeline.events.clear();
updateCount = 0; await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -254,32 +341,46 @@ void main() {
'unsigned': {'transaction_id': 'newresend'}, 'unsigned': {'transaction_id': 'newresend'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(7);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
FakeMatrixApi.calledEndpoints.clear();
await timeline.events[0].sendAgain(); 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(insertList, [0, 1, 2, 0]);
expect(changeList, [0, 0, 0, 1, 2, 0, 0]); expect(changeList, [0, 0]);
expect(removeList, [0]); expect(removeList, []);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
expect(timeline.events[0].status, EventStatus.sent); expect(timeline.events[0].status, EventStatus.sent);
}); });
test('Clear cache on limited timeline', () async { test('Clear cache on limited timeline', () async {
client.onSync.add( FakeMatrixApi.calledEndpoints.clear();
await timeline.requestFuture();
await timeline.requestFuture();
await client.handleSync(
SyncUpdate( SyncUpdate(
nextBatch: '1234', nextBatch: '1234',
rooms: RoomsUpdate( rooms: RoomsUpdate(
join: { join: {
roomID: JoinedRoomUpdate( roomID: JoinedRoomUpdate(
timeline: TimelineUpdate( timeline:
limited: true, TimelineUpdate(limited: true, prevBatch: 'blah', events: [
prevBatch: 'blah', MatrixEvent(
), eventId: '\$somerandomfox',
type: 'm.room.message',
content: {'msgtype': 'm.text', 'body': 'Testcase'},
senderId: '@alice:example.com',
originServerTs:
DateTime.fromMillisecondsSinceEpoch(testTimeStamp),
),
]),
unreadNotifications: UnreadNotificationCounts( unreadNotifications: UnreadNotificationCounts(
highlightCount: 0, highlightCount: 0,
notificationCount: 0, notificationCount: 0,
@ -289,12 +390,14 @@ void main() {
), ),
), ),
); );
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(7);
expect(timeline.events.isEmpty, true); expect(timeline.events.length, 1);
}); });
test('sort errors on top', () async { test('sort errors on top', () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -319,13 +422,15 @@ void main() {
'origin_server_ts': testTimeStamp + 5 'origin_server_ts': testTimeStamp + 5
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(8);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.synced); expect(timeline.events[1].status, EventStatus.synced);
}); });
test('sending event to failed update', () async { test('sending event to failed update', () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -338,9 +443,9 @@ void main() {
'origin_server_ts': DateTime.now().millisecondsSinceEpoch, '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[0].status, EventStatus.sending);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -353,11 +458,13 @@ void main() {
'origin_server_ts': testTimeStamp 'origin_server_ts': testTimeStamp
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(8);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
}); });
test('setReadMarker', () async { test('setReadMarker', () async {
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -370,7 +477,8 @@ void main() {
'origin_server_ts': DateTime.now().millisecondsSinceEpoch, 'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(7);
room.notificationCount = 1; room.notificationCount = 1;
await timeline.setReadMarker(); await timeline.setReadMarker();
expect(room.notificationCount, 0); expect(room.notificationCount, 0);
@ -378,6 +486,9 @@ void main() {
test('sending an event and the http request finishes first, 0 -> 1 -> 2', test('sending an event and the http request finishes first, 0 -> 1 -> 2',
() async { () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -390,9 +501,9 @@ void main() {
'origin_server_ts': DateTime.now().millisecondsSinceEpoch, '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[0].status, EventStatus.sending);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -406,9 +517,9 @@ void main() {
'unsigned': {'transaction_id': 'transaction'} 'unsigned': {'transaction_id': 'transaction'}
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(8);
expect(timeline.events[0].status, EventStatus.sent); expect(timeline.events[0].status, EventStatus.sent);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -422,13 +533,16 @@ void main() {
'unsigned': {'transaction_id': 'transaction'} 'unsigned': {'transaction_id': 'transaction'}
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(9);
expect(timeline.events[0].status, EventStatus.synced); 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', test('sending an event where the sync reply arrives first, 0 -> 2 -> 1',
() async { () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, 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[0].status, EventStatus.sending);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, 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[0].status, EventStatus.synced);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, 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[0].status, EventStatus.synced);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
}); });
test('sending an event 0 -> -1 -> 2', () async { test('sending an event 0 -> -1 -> 2', () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -498,9 +615,9 @@ void main() {
'origin_server_ts': DateTime.now().millisecondsSinceEpoch, '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[0].status, EventStatus.sending);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -513,9 +630,9 @@ void main() {
'unsigned': {'transaction_id': 'transaction'}, 'unsigned': {'transaction_id': 'transaction'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(8);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -529,12 +646,15 @@ void main() {
'unsigned': {'transaction_id': 'transaction'}, 'unsigned': {'transaction_id': 'transaction'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(9);
expect(timeline.events[0].status, EventStatus.synced); 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 { test('sending an event 0 -> 2 -> -1', () async {
timeline.events.clear(); timeline.events.clear();
await timeline.requestFuture();
await timeline.requestFuture();
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -547,9 +667,9 @@ void main() {
'origin_server_ts': DateTime.now().millisecondsSinceEpoch, '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[0].status, EventStatus.sending);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -563,9 +683,9 @@ void main() {
'unsigned': {'transaction_id': 'transaction'}, 'unsigned': {'transaction_id': 'transaction'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(8);
expect(timeline.events[0].status, EventStatus.synced); expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
client.onEvent.add(EventUpdate( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -578,9 +698,9 @@ void main() {
'unsigned': {'transaction_id': 'transaction'}, 'unsigned': {'transaction_id': 'transaction'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(9);
expect(timeline.events[0].status, EventStatus.synced); expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1); expect(timeline.events.length, 4);
}); });
test('logout', () async { test('logout', () async {
await client.logout(); await client.logout();

View File

@ -16,28 +16,57 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'dart:math';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/src/models/timeline_chunk.dart'; import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'fake_client.dart'; import 'fake_client.dart';
import 'fake_matrix_api.dart';
void main() { void main() {
group('Timeline', () { group('Timeline', () {
Logs().level = Level.error; Logs().level = Level.error;
final roomID = '!1234:example.com'; final roomID = '!1234:example.com';
final testTimeStamp = DateTime.now().millisecondsSinceEpoch; var testTimeStamp = 0;
var updateCount = 0; var updateCount = 0;
final insertList = <int>[]; final insertList = <int>[];
final changeList = <int>[]; final changeList = <int>[];
final removeList = <int>[]; final removeList = <int>[];
var olmEnabled = true; var olmEnabled = true;
var currentPoison = 0;
final countStream = StreamController<int>.broadcast();
Future<int> waitForCount(int count) {
if (updateCount == count) {
return Future.value(updateCount);
}
final completer = Completer<int>();
StreamSubscription<int>? 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 Client client;
late Room room; late Room room;
late Timeline timeline; late Timeline timeline;
test('create stuff', () async { setUp(() async {
try { try {
await olm.init(); await olm.init();
olm.get_library_version(); olm.get_library_version();
@ -49,24 +78,40 @@ void main() {
client = await getClient(); client = await getClient();
client.sendMessageTimeoutSeconds = 5; client.sendMessageTimeoutSeconds = 5;
final poison = Random().nextInt(2 ^ 32);
currentPoison = poison;
room = Room( room = Room(
id: roomID, client: client, prev_batch: '1234', roomAccountData: {}); id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
timeline = Timeline( timeline = Timeline(
room: room, room: room,
chunk: TimelineChunk(events: []), chunk: TimelineChunk(events: []),
onUpdate: () { onUpdate: () {
if (poison != currentPoison) return;
updateCount++; updateCount++;
countStream.add(updateCount);
}, },
onInsert: insertList.add, onInsert: insertList.add,
onChange: changeList.add, onChange: changeList.add,
onRemove: removeList.add, onRemove: removeList.add,
); );
});
test('Create', () async {
await client.checkHomeserver(Uri.parse('https://fakeserver.notexisting'), await client.checkHomeserver(Uri.parse('https://fakeserver.notexisting'),
checkWellKnown: false); 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( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -94,7 +139,7 @@ void main() {
expect(timeline.sub != null, true); expect(timeline.sub != null, true);
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(2);
expect(updateCount, 2); expect(updateCount, 2);
expect(insertList, [0, 0]); expect(insertList, [0, 0]);
@ -143,7 +188,7 @@ void main() {
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(3);
expect(updateCount, 3); expect(updateCount, 3);
expect(insertList, [0, 0, 0]); expect(insertList, [0, 0, 0]);
@ -157,10 +202,9 @@ void main() {
test('Send message', () async { test('Send message', () async {
await room.sendTextEvent('test', txid: '1234'); await room.sendTextEvent('test', txid: '1234');
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(2);
expect(updateCount, 2);
expect(updateCount, 5); expect(insertList, [0]);
expect(insertList, [0, 0, 0, 0]);
expect(insertList.length, timeline.events.length); expect(insertList.length, timeline.events.length);
final eventId = timeline.events[0].eventId; final eventId = timeline.events[0].eventId;
expect(eventId.startsWith('\$event'), true); expect(eventId.startsWith('\$event'), true);
@ -180,16 +224,52 @@ void main() {
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(3);
expect(updateCount, 3);
expect(updateCount, 6); expect(insertList, [0]);
expect(insertList, [0, 0, 0, 0]);
expect(insertList.length, timeline.events.length); expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, eventId); expect(timeline.events[0].eventId, eventId);
expect(timeline.events[0].status, EventStatus.synced); expect(timeline.events[0].status, EventStatus.synced);
}); });
test('Send message with error', () async { 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( client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
roomID: roomID, roomID: roomID,
@ -202,43 +282,32 @@ void main() {
'origin_server_ts': testTimeStamp '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 timeline.events[0].remove();
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(2);
expect(updateCount, 14); expect(insertList, [0]);
expect(changeList, []);
expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2]);
expect(changeList, [2, 0, 0, 0, 1, 2]);
expect(removeList, [0]); expect(removeList, [0]);
expect(timeline.events.length, 7); expect(timeline.events.length, 0);
expect(timeline.events[0].status, EventStatus.error);
}); });
test('getEventById', () async { 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'); var event = await timeline.getEventById('abc');
expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'}); expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'});
@ -270,17 +339,17 @@ void main() {
'unsigned': {'transaction_id': 'newresend'}, 'unsigned': {'transaction_id': 'newresend'},
}, },
)); ));
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(1);
expect(timeline.events[0].status, EventStatus.error); expect(timeline.events[0].status, EventStatus.error);
await timeline.events[0].sendAgain(); 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(insertList, [0]);
expect(changeList, [2, 0, 0, 0, 1, 2, 0, 0]); expect(changeList, [0, 0]);
expect(removeList, [0]); expect(removeList, []);
expect(timeline.events.length, 1); expect(timeline.events.length, 1);
expect(timeline.events[0].status, EventStatus.sent); expect(timeline.events[0].status, EventStatus.sent);
}); });
@ -290,10 +359,10 @@ void main() {
expect(timeline.canRequestHistory, true); expect(timeline.canRequestHistory, true);
await room.requestHistory(); await room.requestHistory();
await Future.delayed(Duration(milliseconds: 50)); await waitForCount(3);
expect(updateCount, 20); expect(updateCount, 3);
expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 1, 2]); expect(insertList, [0, 1, 2]);
expect(timeline.events.length, 3); expect(timeline.events.length, 3);
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org'); expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org'); expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');

View File

@ -163,6 +163,7 @@ void main() {
expect(user2.mentionFragments, {'@Bob', '@Bob#1542'}); expect(user2.mentionFragments, {'@Bob', '@Bob#1542'});
}); });
test('dispose client', () async { test('dispose client', () async {
await Future.delayed(Duration(milliseconds: 50));
await client.dispose(closeDatabase: true); await client.dispose(closeDatabase: true);
}); });
}); });