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;
keyManager.dispose();
olmManager.dispose();
await olmManager.dispose();
keyVerificationManager.dispose();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:math';
import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
@ -35,22 +35,68 @@ Map<String, dynamic> decodeJson(dynamic data) {
return data;
}
class FakeMatrixApi extends MockClient {
static final calledEndpoints = <String, List<dynamic>>{};
static int eventCounter = 0;
static sdk.Client? client;
static bool failToDevice = false;
class FakeMatrixApi extends BaseClient {
static Map<String, List<dynamic>> get calledEndpoints =>
currentApi!._calledEndpoints;
static int get eventCounter => currentApi!._eventCounter;
static set eventCounter(int c) {
currentApi!._eventCounter = c;
}
FakeMatrixApi()
: super((request) async {
static set client(sdk.Client? c) {
currentApi?._client = c;
}
static set failToDevice(bool fail) {
currentApi?._failToDevice = fail;
}
static set trace(bool t) {
currentApi?._trace = t;
}
final _calledEndpoints = <String, List<dynamic>>{};
int _eventCounter = 0;
sdk.Client? _client;
bool _failToDevice = false;
bool _trace = false;
final _apiCallStream = StreamController<String>.broadcast();
static FakeMatrixApi? currentApi;
static Future<String> firstWhereValue(String value) {
return firstWhere((v) => v == value);
}
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;
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);
}
@ -68,7 +114,7 @@ class FakeMatrixApi extends MockClient {
dynamic res = {};
var statusCode = 200;
//print('$method request to $action with Data: $data');
//print('\$method request to $action with Data: $data');
// Sync requests with timeout
if (data is Map<String, dynamic> && data['timeout'] is String) {
@ -81,10 +127,7 @@ class FakeMatrixApi extends MockClient {
}
// Call API
if (!calledEndpoints.containsKey(action)) {
calledEndpoints[action] = <dynamic>[];
}
calledEndpoints[action]?.add(data);
(_calledEndpoints[action] ??= <dynamic>[]).add(data);
final act = api[method]?[action];
if (act != null) {
res = act(data);
@ -95,10 +138,9 @@ class FakeMatrixApi extends MockClient {
statusCode = 405;
}
}
} else if (method == 'PUT' &&
action.contains('/client/v3/sendToDevice/')) {
} else if (method == 'PUT' && action.contains('/client/v3/sendToDevice/')) {
res = {};
if (failToDevice) {
if (_failToDevice) {
statusCode = 500;
}
} else if (method == 'GET' &&
@ -109,46 +151,93 @@ class FakeMatrixApi extends MockClient {
} else if (method == 'PUT' &&
action.contains(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/send/')) {
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
res = {'event_id': '\$event${_eventCounter++}'};
} else if (method == 'PUT' &&
action.contains(
'/client/v3/rooms/!1234%3AfakeServer.notExisting/state/')) {
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
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 &&
_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)
],
accountData: [sdk.BasicEvent(content: decodeJson(data), type: type)],
);
if (client?.database != null) {
await client?.database?.transaction(() async {
await client?.handleSync(syncUpdate);
if (_client?.database != null) {
await _client?.database?.transaction(() async {
await _client?.handleSync(syncUpdate);
});
} else {
await client?.handleSync(syncUpdate);
await _client?.handleSync(syncUpdate);
}
res = {};
} else {
res = {
'errcode': 'M_UNRECOGNIZED',
'error': 'Unrecognized request'
};
res = {'errcode': 'M_UNRECOGNIZED', 'error': 'Unrecognized request'};
statusCode = 405;
}
unawaited(Future.delayed(Duration(milliseconds: 1)).then((_) async {
_apiCallStream.add(action);
}));
return Response.bytes(utf8.encode(json.encode(res)), statusCode);
});
}
static Map<String, dynamic> messagesResponsePast = {
@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',
'end': 't47409-4357353_219380_26003_2265',
'chunk': [
@ -206,7 +295,7 @@ class FakeMatrixApi extends MockClient {
],
'state': [],
};
static Map<String, dynamic> messagesResponseFuture = {
static const Map<String, dynamic> messagesResponseFuture = {
'start': 't456',
'end': 't789',
'chunk': [
@ -264,7 +353,7 @@ class FakeMatrixApi extends MockClient {
],
'state': [],
};
static Map<String, dynamic> messagesResponseFutureEnd = {
static const Map<String, dynamic> messagesResponseFutureEnd = {
'start': 't789',
'end': null,
'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': {
'/path/to/auth/error': (var req) => {
'errcode': 'M_FORBIDDEN',
@ -2177,28 +2266,6 @@ class FakeMatrixApi extends MockClient {
'/client/v3/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {},
'/client/v3/rooms/!localpart%3Aserver.abc/unban': (var reqI) => {},
'/client/v3/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {},
'/client/v3/keys/device_signing/upload': (var reqI) {
if (client != null) {
final jsonBody = decodeJson(reqI);
for (final keyType in {
'master_key',
'self_signing_key',
'user_signing_key'
}) {
if (jsonBody[keyType] != null) {
final key =
sdk.CrossSigningKey.fromJson(jsonBody[keyType], client!);
client!.userDeviceKeys[client!.userID!]?.crossSigningKeys
.removeWhere((k, v) => v.usage.contains(key.usage.first));
client!.userDeviceKeys[client!.userID!]
?.crossSigningKeys[key.publicKey!] = key;
}
}
// and generate a fake sync
client!.handleSync(sdk.SyncUpdate(nextBatch: ''));
}
return {};
},
'/client/v3/keys/signatures/upload': (var reqI) => {'failures': {}},
'/client/v3/room_keys/version': (var reqI) => {'version': '5'},
},

View File

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

View File

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

View File

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