matrix-dart-sdk/test/client_test.dart

1642 lines
69 KiB
Dart

/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:canonical_json/canonical_json.dart';
import 'package:collection/collection.dart';
import 'package:olm/olm.dart' as olm;
import 'package:path/path.dart' show join;
import 'package:test/test.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/client_init_exception.dart';
import 'fake_client.dart';
import 'fake_database.dart';
void main() {
// key @test:fakeServer.notExisting
const pickledOlmAccount =
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk';
const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
group('client path', () {
late Client clientOnPath;
final dbPath = join(Directory.current.path, 'test.sqlite');
setUp(() async {
expect(
await File(dbPath).exists(),
false,
reason: '$dbPath should not exist',
);
clientOnPath = await getClient(
databasePath: dbPath,
);
await clientOnPath.abortSync();
expect(await File(dbPath).exists(), true, reason: '$dbPath should exist');
});
test('logout', () async {
expect(await File(dbPath).exists(), true);
await clientOnPath.logout();
expect(await File(dbPath).exists(), false);
});
});
/// All Tests related to the Login
group('client mem', tags: 'olm', () {
late Client matrix;
Logs().level = Level.error;
/// Check if all Elements get created
setUp(() async {
matrix = await getClient();
});
test('barebones client login', () async {
final client = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
expect(client.isLogged(), false);
final Set<InitState> initStates = {};
await client.init(onInitStateChanged: initStates.add);
expect(client.isLogged(), false);
expect(initStates, {InitState.initializing, InitState.finished});
initStates.clear();
await client.login(
LoginType.mLoginPassword,
token: 'abcd',
identifier:
AuthenticationUserIdentifier(user: '@test:fakeServer.notExisting'),
deviceId: 'GHTYAJCE',
onInitStateChanged: initStates.add,
);
expect(initStates, {
InitState.initializing,
InitState.settingUpEncryption,
InitState.loadingData,
InitState.waitingForFirstSync,
InitState.finished,
});
expect(client.isLogged(), true);
await client.logout();
});
test('Login', () async {
matrix = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
final eventUpdateListFuture = matrix.onTimelineEvent.stream.toList();
final toDeviceUpdateListFuture = matrix.onToDeviceEvent.stream.toList();
var presenceCounter = 0;
var accountDataCounter = 0;
matrix.onPresenceChanged.stream.listen((CachedPresence data) {
presenceCounter++;
});
// ignore: deprecated_member_use_from_same_package
matrix.onAccountData.stream.listen((BasicEvent data) {
accountDataCounter++;
});
expect(matrix.homeserver, null);
try {
await matrix
.checkHomeserver(Uri.parse('https://fakeserver.wrongaddress'));
} catch (exception) {
expect(exception.toString().isNotEmpty, true);
}
await matrix.checkHomeserver(
Uri.parse('https://fakeserver.notexisting'),
checkWellKnown: false,
);
expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
final available = await matrix.checkUsernameAvailability('testuser');
expect(available, true);
final loginStateFuture = matrix.onLoginStateChanged.stream.first;
final syncFuture = matrix.onSync.stream.first;
await matrix.init(
newToken: 'abcd',
newUserID: '@test:fakeServer.notExisting',
newHomeserver: matrix.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newOlmAccount: pickledOlmAccount,
);
final loginState = await loginStateFuture;
final sync = await syncFuture;
// to ensure our state doesn't get overwritten once we manually inject SyncUpdates
await matrix.abortSync();
expect(loginState, LoginState.loggedIn);
expect(matrix.onSync.value != null, true);
expect(matrix.encryptionEnabled, true);
expect(matrix.identityKey, identityKey);
expect(matrix.fingerprintKey, fingerprintKey);
expect(sync.nextBatch == matrix.prevBatch, true);
expect(matrix.accountData.length, 10);
expect(
matrix.getDirectChatFromUserId('@bob:example.com'),
'!726s6s6q:example.com',
);
expect(matrix.rooms[2].directChatMatrixID, '@bob:example.com');
expect(matrix.directChats, matrix.accountData['m.direct']?.content);
// ignore: deprecated_member_use_from_same_package
expect(matrix.presences.length, 1);
expect(matrix.rooms[2].ephemerals.length, 2);
expect(matrix.rooms[2].typingUsers.length, 1);
expect(matrix.rooms[2].typingUsers[0].id, '@alice:example.com');
expect(matrix.rooms[2].roomAccountData.length, 3);
expect(matrix.rooms[2].encrypted, true);
expect(
matrix.rooms[2].encryptionAlgorithm,
Client.supportedGroupEncryptionAlgorithms.first,
);
expect(
matrix
.rooms[2].receiptState.global.otherUsers['@alice:example.com']?.ts,
1436451550453,
);
expect(
matrix.rooms[2].receiptState.global.otherUsers['@alice:example.com']
?.eventId,
'\$7365636s6r6432:example.com',
);
final inviteRoom = matrix.rooms
.singleWhere((room) => room.membership == Membership.invite);
expect(inviteRoom.name, 'My Room Name');
expect(inviteRoom.states[EventTypes.RoomMember]?.length, 1);
expect(matrix.rooms.length, 3);
expect(
matrix.rooms[2].canonicalAlias,
"#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}",
);
expect(
// ignore: deprecated_member_use_from_same_package
matrix.presences['@alice:example.com']?.presence,
PresenceType.online,
);
expect(presenceCounter, 1);
expect(accountDataCounter, 10);
expect(matrix.userDeviceKeys.keys.toSet(), {
'@alice:example.com',
'@othertest:fakeServer.notExisting',
'@test:fakeServer.notExisting',
});
expect(matrix.userDeviceKeys['@alice:example.com']?.outdated, false);
expect(matrix.userDeviceKeys['@alice:example.com']?.deviceKeys.length, 2);
expect(
matrix.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS']
?.verified,
false,
);
expect(matrix.wellKnown, isNull);
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'device_lists': {
'changed': [
'@alice:example.com',
],
'left': [
'@bob:example.com',
],
},
}),
);
await Future.delayed(Duration(milliseconds: 50));
expect(matrix.userDeviceKeys.length, 3);
expect(matrix.userDeviceKeys['@alice:example.com']?.outdated, true);
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
'!726s6s6q:example.com': {
'state': {
'events': [
{
'sender': '@alice:example.com',
'type': 'm.room.canonical_alias',
'content': {'alias': ''},
'state_key': '',
'origin_server_ts': 1417731086799,
'event_id': '66697273743033:example.com',
}
],
},
},
},
},
}),
);
await Future.delayed(Duration(milliseconds: 50));
expect(
matrix.getRoomByAlias(
"#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}",
),
null,
);
await matrix.onTimelineEvent.close();
final eventUpdateList = await eventUpdateListFuture;
expect(eventUpdateList.length, 3);
expect(eventUpdateList[0].type, 'm.room.member');
expect(eventUpdateList[0].roomId, '!726s6s6q:example.com');
expect(eventUpdateList[1].type, 'm.room.message');
expect(eventUpdateList[1].roomId, '!726s6s6q:example.com');
expect(eventUpdateList[2].type, 'm.room.message');
expect(eventUpdateList[2].roomId, '!726s6s6f:example.com');
expect(
matrix
.getRoomById('!726s6s6q:example.com')
?.roomAccountData['org.example.custom.room.config']
?.type,
'org.example.custom.room.config',
);
expect(
matrix
.getRoomById('!726s6s6q:example.com')
?.roomAccountData[LatestReceiptState.eventType]
?.type,
LatestReceiptState.eventType,
);
expect(
matrix
.getRoomById('!726s6s6q:example.com')
?.roomAccountData['m.tag']
?.type,
'm.tag',
);
expect(
matrix
.getRoomById('!726s6s6q:example.com')
?.ephemerals['m.typing']
?.content,
{
'user_ids': ['@alice:example.com'],
},
);
expect(
matrix
.getRoomById('!726s6s6q:example.com')
?.ephemerals['m.receipt']
?.content,
{
'\$7365636s6r6432:example.com': {
'm.read': {
'@alice:example.com': {'ts': 1436451550453},
},
},
},
);
await matrix.onToDeviceEvent.close();
final deviceeventUpdateList = await toDeviceUpdateListFuture;
expect(deviceeventUpdateList.length, 2);
expect(deviceeventUpdateList[0].type, 'm.new_device');
expect(deviceeventUpdateList[1].type, 'm.room_key');
});
test('recentEmoji', () async {
final emojis = matrix.recentEmojis;
expect(emojis.length, 2);
expect(emojis['👍️'], 1);
expect(emojis['🖇️'], 0);
await matrix.addRecentEmoji('🦙');
// To check if the emoji is properly added, we need to wait for a sync roundtrip
});
test('accountData', () async {
final content = {
'bla': 'blub',
};
final key = 'abc def!/_-';
await matrix.setAccountData(matrix.userID!, key, content);
final dbContent = await matrix.database?.getAccountData();
expect(matrix.accountData[key]?.content, content);
expect(dbContent?[key]?.content, content);
});
test('roomAccountData', () async {
final content = {
'bla': 'blub',
};
final key = 'abc def!/_-';
final roomId = '!726s6s6q:example.com';
await matrix.setAccountDataPerRoom(matrix.userID!, roomId, key, content);
final roomFromList = (await matrix.database?.getRoomList(matrix))
?.firstWhere((room) => room.id == roomId);
final roomFromDb = await matrix.database?.getSingleRoom(matrix, roomId);
expect(
matrix.getRoomById(roomId)?.roomAccountData[key]?.content,
content,
);
expect(roomFromList?.roomAccountData[key]?.content, content);
expect(
roomFromDb?.roomAccountData[key]?.content,
content,
skip: 'The single room function does not load account data',
);
});
test('Logout', () async {
final loginStateFuture = matrix.onLoginStateChanged.stream.first;
await matrix.logout();
expect(matrix.accessToken == null, true);
expect(matrix.homeserver == null, true);
expect(matrix.userID == null, true);
expect(matrix.deviceID == null, true);
expect(matrix.deviceName == null, true);
expect(matrix.prevBatch == null, true);
final loginState = await loginStateFuture;
expect(loginState, LoginState.loggedOut);
});
test('Login again but break server when trying to logout', () async {
matrix = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
expect(matrix.homeserver, null);
try {
await matrix
.checkHomeserver(Uri.parse('https://fakeserver.wrongaddress'));
} catch (exception) {
expect(exception.toString().isNotEmpty, true);
}
await matrix.checkHomeserver(
Uri.parse('https://fakeserver.notexisting'),
checkWellKnown: false,
);
expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
final available = await matrix.checkUsernameAvailability('testuser');
expect(available, true);
final loginStateFuture = matrix.onLoginStateChanged.stream.first;
final syncFuture = matrix.onSync.stream.first;
await matrix.init(
newToken: 'abcd',
newUserID: '@test:fakeServer.notExisting',
newHomeserver: matrix.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newOlmAccount: pickledOlmAccount,
);
await Future.delayed(Duration(milliseconds: 50));
final loginState = await loginStateFuture;
final sync = await syncFuture;
expect(loginState, LoginState.loggedIn);
expect(matrix.onSync.value != null, true);
expect(matrix.encryptionEnabled, true);
expect(matrix.identityKey, identityKey);
expect(matrix.fingerprintKey, fingerprintKey);
expect(sync.nextBatch == matrix.prevBatch, true);
});
test('Logout but this time server is dead', () async {
final oldapi = FakeMatrixApi.currentApi?.api;
expect(
(FakeMatrixApi.currentApi?.api['PUT']?.keys.where(
(element) => element.startsWith('/client/v3/room_keys/keys?version'),
))?.length,
1,
);
// not a huge fan, open to ideas
FakeMatrixApi.currentApi?.api = {};
// random sanity test to test if the test to test the breaking server hack works.
expect(
(FakeMatrixApi.currentApi?.api['PUT']?.keys.where(
(element) => element.startsWith('/client/v3/room_keys/keys?version'),
))?.length,
null,
);
try {
await matrix.logout();
} catch (e) {
Logs().w(
'Ignore red warnings for this test, test is to check if database is cleared even if server breaks ',
);
}
expect(matrix.accessToken == null, true);
expect(matrix.homeserver == null, true);
expect(matrix.userID == null, true);
expect(matrix.deviceID == null, true);
expect(matrix.deviceName == null, true);
expect(matrix.prevBatch == null, true);
FakeMatrixApi.currentApi?.api = oldapi!;
});
test('Login', () async {
matrix = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
await matrix.checkHomeserver(
Uri.parse('https://fakeserver.notexisting'),
checkWellKnown: false,
);
final loginResp = await matrix.login(
LoginType.mLoginPassword,
identifier: AuthenticationUserIdentifier(user: 'test'),
password: '1234',
);
expect(loginResp.userId.isNotEmpty, true);
});
test('setAvatar', () async {
final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
await matrix.setAvatar(testFile);
});
test('setMuteAllPushNotifications', () async {
await matrix.setMuteAllPushNotifications(false);
});
test('createSpace', () async {
await matrix.createSpace(
name: 'space',
topic: 'My test space',
spaceAliasName: '#myspace:example.invalid',
invite: ['@alice:example.invalid'],
roomVersion: '3',
);
});
test('sync state event in-memory handling', () async {
final roomId = '!726s6s6q:example.com';
final room = matrix.getRoomById(roomId)!;
// put an important state event in-memory
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'state': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'm.room.name',
'content': <String, dynamic>{'name': 'foxies'},
'state_key': '',
'origin_server_ts': 1417731086799,
'event_id': '66697273743033:example.com',
}
],
},
},
},
},
}),
);
expect(room.getState('m.room.name')?.content['name'], 'foxies');
// drop an unimportant state event from in-memory handling
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'state': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'com.famedly.custom',
'content': <String, dynamic>{'name': 'foxies'},
'state_key': '',
'origin_server_ts': 1417731086799,
'event_id': '66697273743033:example.com',
}
],
},
},
},
},
}),
);
expect(room.getState('com.famedly.custom'), null);
// persist normal room messages
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'timeline': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'm.room.message',
'content': <String, dynamic>{
'msgtype': 'm.text',
'body': 'meow',
},
'origin_server_ts': 1417731086799,
'event_id': '\$last:example.com',
}
],
},
},
},
},
}),
);
expect(room.lastEvent!.content['body'], 'meow');
// ignore edits
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'timeline': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'm.room.message',
'content': <String, dynamic>{
'msgtype': 'm.text',
'body': '* floooof',
'm.new_content': <String, dynamic>{
'msgtype': 'm.text',
'body': 'floooof',
},
'm.relates_to': <String, dynamic>{
'rel_type': 'm.replace',
'event_id': '\$other:example.com',
},
},
'origin_server_ts': 1417731086799,
'event_id': '\$edit:example.com',
}
],
},
},
},
},
}),
);
expect(room.lastEvent!.content['body'], 'meow');
// accept edits to the last event
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'timeline': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'm.room.message',
'content': <String, dynamic>{
'msgtype': 'm.text',
'body': '* floooof',
'm.new_content': <String, dynamic>{
'msgtype': 'm.text',
'body': 'floooof',
},
'm.relates_to': <String, dynamic>{
'rel_type': 'm.replace',
'event_id': '\$last:example.com',
},
},
'origin_server_ts': 1417731086799,
'event_id': '\$edit:example.com',
}
],
},
},
},
},
}),
);
expect(room.lastEvent!.content['body'], '* floooof');
// Older state event should not overwrite current state events
room.partial = false;
await matrix.handleSync(
SyncUpdate(
nextBatch: '',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
state: [
MatrixEvent(
type: EventTypes.RoomMember,
content: {'displayname': 'Alice Catgirl'},
senderId: '@alice:example.com',
eventId: 'oldEventId',
stateKey: '@alice:example.com',
originServerTs:
DateTime.now().subtract(const Duration(days: 365 * 30)),
),
],
),
},
),
),
direction: Direction.b,
);
room.partial = true;
expect(room.getParticipants().first.id, '@alice:example.com');
expect(room.getParticipants().first.displayName, 'Alice Margatroid');
// accepts a consecutive edit
await matrix.handleSync(
SyncUpdate.fromJson({
'next_batch': 'fakesync',
'rooms': {
'join': {
roomId: {
'timeline': {
'events': [
<String, dynamic>{
'sender': '@alice:example.com',
'type': 'm.room.message',
'content': <String, dynamic>{
'msgtype': 'm.text',
'body': '* foxies',
'm.new_content': <String, dynamic>{
'msgtype': 'm.text',
'body': 'foxies',
},
'm.relates_to': <String, dynamic>{
'rel_type': 'm.replace',
'event_id': '\$last:example.com',
},
},
'origin_server_ts': 1417731086799,
'event_id': '\$edit2:example.com',
}
],
},
},
},
},
}),
);
expect(room.lastEvent!.content['body'], '* foxies');
});
test('getProfileFromUserId', () async {
final cachedProfile = await matrix.getUserProfile('@getme:example.com');
expect(cachedProfile.outdated, false);
expect(cachedProfile.avatarUrl.toString(), 'mxc://test');
expect(cachedProfile.displayname, 'You got me');
final profile = await matrix.getProfileFromUserId('@getme:example.com');
expect(profile.avatarUrl.toString(), 'mxc://test');
expect(profile.displayName, 'You got me');
final aliceProfile = await matrix.getUserProfile('@alice:example.com');
expect(aliceProfile.outdated, false);
expect(aliceProfile.avatarUrl.toString(), 'mxc://test');
expect(aliceProfile.displayname, 'Alice M');
await matrix.handleSync(
SyncUpdate(
nextBatch: '',
rooms: RoomsUpdate(
join: {
matrix.rooms.first.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
eventId: 'abcd',
type: EventTypes.RoomMember,
content: {'membership': 'join'},
senderId: '@alice:example.com',
stateKey: '@alice:example.com',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
expect(matrix.onUserProfileUpdate.value, '@alice:example.com');
final cachedProfileFromDb =
await matrix.database?.getUserProfile('@alice:example.com');
expect(cachedProfileFromDb?.outdated, true);
});
test('joinAfterInviteMembership', () async {
final client = await getClient();
await client.abortSync();
client.rooms.clear();
await client.database?.clearCache();
await client.handleSync(
SyncUpdate.fromJson(
jsonDecode(
'{"next_batch":"s198510_227245_8_1404_23586_11_51065_267416_0_2639","rooms":{"invite":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"invite_state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":5948,"currently_active":true},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]},"device_one_time_keys_count":{"signed_curve25519":66},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
),
),
);
await client.handleSync(
SyncUpdate.fromJson(
jsonDecode(
'{"next_batch":"s198511_227245_8_1404_23588_11_51066_267416_0_2639","account_data":{"events":[{"type":"m.direct","content":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":["!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de"]}}]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
),
),
);
await client.handleSync(
SyncUpdate.fromJson(
jsonDecode(
'{"next_batch":"s198512_227245_8_1404_23588_11_51066_267416_0_2639","rooms":{"join":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"summary":{"m.heroes":["@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"m.joined_member_count":2,"m.invited_member_count":0},"state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$qSgXGXjly6p5Kwbdb_PMBC_EF7nzHDbM23mvJFVeoiE","origin_server_ts":1709565579735,"unsigned":{"age":2255}}]},"timeline":{"events":[{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$rQzxxTrSd9Y0koxIGlkalPAV_lwu94jLOA-8PSunY24","origin_server_ts":1709565579871,"unsigned":{"age":2119,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.power_levels","content":{"users":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":100,"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":0,"historical":100},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$d6sgGs8PmkAbC3Iw3CkPT1QSub2zFTTvytegOxkPYPs","origin_server_ts":1709565579966,"unsigned":{"age":2024,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$EnA2Podch5181X4G1ZX34zaFGS_V4ZCZzLkBEfS_qyg","origin_server_ts":1709565579979,"unsigned":{"age":2011,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.history_visibility","content":{"history_visibility":"shared"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$6tNNo6ZkpZZrHrn8ZjXhMqI0CNv-VNNBw4R0h3_O-Tc","origin_server_ts":1709565579979,"unsigned":{"age":2011,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.guest_access","content":{"guest_access":"can_join"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$ViuL_LpN1sY9oYcGwycNjtp6FcGj__smUg8mzj3oa2o","origin_server_ts":1709565579980,"unsigned":{"age":2010,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$_e0az7OP7D78QU7DItiRAtlHlZmA07B5wenR93x5V1E","origin_server_ts":1709565579981,"unsigned":{"age":2009,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","origin_server_ts":1709565580185,"unsigned":{"age":1805,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.notsoencrypted","content":{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"AwgAEpACEZw8Ymg99Yfl7VsXRIdczlQ3+YSJ6te3o6ka/XXP0h4ZsgR2bu1Q8puQ77fOpwX5dPnrrCi5SQg9Zv5/u+0QbFV4FKE/k03Vxao/tiswb6wST14x9kYkwViOrZe7fzg7VF9tCi8U88TqxGsPDDOVjO+WNxG8I9ldP1zvPsxYzVSyGPhaB5E+q6llwlXcQ56wvpf7Ke7gX4Ly2Dlxa8Bmy7aUSCBoWAt/xFRdzCOsE9qI8oxzuvk4RF0H/7bY+4DkGTsP1rIYgA7Q0JueIFb47Yu6pK26BCKo1yPAR8qvpe8vGBICm4slMbKaJN4RqBHtR0zc12E5DXud91o3mArqTksv1NEbI1F4XgDREl76WBw8a7MafDSuun09JuWpGxzPHvLVOUVny6tTJPRutsZLkmnTeMTiXnsPexUiY7UTYlzOMeeoUSTDuJXJz6CM+gSc52CiKoHK/gE","device_id":"TNLOYXJFXM","sender_key":"e9W0gpUcSEKOQ8P/xIdroHUpP7yG4EjQfueiAngESRk","session_id":"hhZ8TBs9Xp0dmuvC6XpDBYsAKnTqb8WiBhZMzHcbBXI"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$KKIZX8cuB3S3uzS7CDtRlTkcaJRW73e2HW2NuW6OTEg","origin_server_ts":1709565580991,"unsigned":{"age":999,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177,"com.famedly.famedlysdk.message_sending_status":2}}],"limited":true,"prev_batch":"s198503_227245_8_1404_23588_11_51066_267416_0_2639"},"ephemeral":{"events":[]},"account_data":{"events":[]},"unread_notifications":{"highlight_count":0,"notification_count":0}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":843,"currently_active":true},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"}]},"device_lists":{"changed":["@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"left":[]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
),
),
);
//await client.handleSync(SyncUpdate.fromJson(jsonDecode('')));
final room = client
.getRoomById('!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de')!;
await room.postLoad();
final participants = await room.requestParticipants();
expect(
participants.where((u) => u.membership == Membership.join).length,
2,
);
await client.abortSync();
client.rooms.clear();
await client.database?.clearCache();
await client.dispose(closeDatabase: true);
});
test('leaveThenInvite should be invited', () async {
// Synapse includes a room in both invite and leave if you leave and get
// reinvited while you are offline. The other direction only contains the
// room in leave. Verify that we actually store the invite in the first
// case. See also
// https://github.com/famedly/product-management/issues/2283
final client = await getClient();
await client.abortSync();
client.rooms.clear();
await client.database?.clearCache();
final roomId = '!inviteLeaveRoom:example.com';
await client.handleSync(
SyncUpdate(
nextBatch: 'ABCDEF',
rooms: RoomsUpdate(
invite: {
roomId: InvitedRoomUpdate(
inviteState: [
StrippedStateEvent(
type: EventTypes.RoomMember,
senderId: '@bob:example.com',
stateKey: client.userID,
content: {
'membership': 'invite',
},
),
],
),
},
leave: {
roomId: LeftRoomUpdate(
state: [
MatrixEvent(
type: EventTypes.RoomMember,
senderId: client.userID!,
stateKey: client.userID,
originServerTs: DateTime.now(),
eventId:
'\$abcdefwsjaskdfabsjfhabfsjgbahsjfkgbasjffsajfgsfd',
content: {
'membership': 'leave',
},
),
],
),
},
),
),
);
final room = client.getRoomById(roomId);
expect(room?.membership, Membership.invite);
await client.abortSync();
client.rooms.clear();
await client.database?.clearCache();
await client.dispose(closeDatabase: true);
});
test('ownProfile', () async {
final client = await getClient();
await client.abortSync();
client.rooms.clear();
await client.database?.clearCache();
await client.handleSync(
SyncUpdate.fromJson(
jsonDecode(
'{"next_batch":"s82_571_2_6_39_1_2_34_1","account_data":{"events":[{"type":"m.push_rules","content":{"global":{"underride":[{"conditions":[{"kind":"event_match","key":"type","pattern":"m.call.invite"}],"actions":["notify",{"set_tweak":"sound","value":"ring"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.call","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted_room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.message","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"im.vector.modular.widgets"},{"kind":"event_match","key":"content.type","pattern":"jitsi"},{"kind":"event_match","key":"state_key","pattern":"*"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".im.vector.jitsi","default":true,"enabled":true}],"sender":[],"room":[],"content":[{"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"pattern":"056d6976-fb61-47cf-86f0-147387461565","rule_id":".m.rule.contains_user_name","default":true,"enabled":true}],"override":[{"conditions":[],"actions":["dont_notify"],"rule_id":".m.rule.master","default":true,"enabled":false},{"conditions":[{"kind":"event_match","key":"content.msgtype","pattern":"m.notice"}],"actions":["dont_notify"],"rule_id":".m.rule.suppress_notices","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"},{"kind":"event_match","key":"content.membership","pattern":"invite"},{"kind":"event_match","key":"state_key","pattern":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.invite_for_me","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"}],"actions":["dont_notify"],"rule_id":".m.rule.member_event","default":true,"enabled":true},{"conditions":[{"kind":"contains_display_name"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"rule_id":".m.rule.contains_display_name","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"content.body","pattern":"@room"},{"kind":"sender_notification_permission","key":"room"}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.roomnotif","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.tombstone"},{"kind":"event_match","key":"state_key","pattern":""}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.tombstone","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.reaction"}],"actions":["dont_notify"],"rule_id":".m.rule.reaction","default":true,"enabled":true}]},"device":{}}}]},"presence":{"events":[{"type":"m.presence","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"presence":"online","last_active_ago":43,"currently_active":true}}]},"device_one_time_keys_count":{"signed_curve25519":66},"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"],"device_unused_fallback_key_types":["signed_curve25519"],"rooms":{"join":{"!MEgZosbiZqjSjbHFqI:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":{"timeline":{"events":[{"type":"m.room.member","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Lars Kaiser"},"state_key":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647296944593,"unsigned":{"age":545455},"event_id":"\$mk9kFUEAKBZJgarWApLyYqOZQQocLIVV8tWp_gJEZFU"},{"type":"m.room.power_levels","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"users":{"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":50,"historical":100},"state_key":"","origin_server_ts":1647296944690,"unsigned":{"age":545358},"event_id":"\$3wL2YgVNQzgfl8y_ksi3BPMqRs94jb_m0WRonL1HNpY"},{"type":"m.room.canonical_alias","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"alias":"#user-discovery:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944806,"unsigned":{"age":545242},"event_id":"\$yXaVETL9F4jSN9rpRNyT_kUoctzD07n5Z4AIHziP7DQ"},{"type":"m.room.join_rules","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"join_rule":"public"},"state_key":"","origin_server_ts":1647296944894,"unsigned":{"age":545154},"event_id":"\$jBDHhgpNqr125eWUsGVw4r7ZG2hgr0BTzzR77S-ubvY"},{"type":"m.room.history_visibility","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"history_visibility":"shared"},"state_key":"","origin_server_ts":1647296944965,"unsigned":{"age":545083},"event_id":"\$kMessP7gAphUKW7mzOLlJT6NT8IsVGPmGir3_1uBNCE"},{"type":"m.room.name","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"name":"User Discovery"},"state_key":"","origin_server_ts":1647296945062,"unsigned":{"age":544986},"event_id":"\$Bo9Ut_0vcr3FuxCRye4IHEMxUxIIcSwc-ePnMzx-hYU"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296989893,"unsigned":{"age":500155},"event_id":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"Some First Name Some Last Name"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296990076,"unsigned":{"replaces_state":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE","prev_content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"prev_sender":"@test:fakeServer.notExisting","age":499972},"event_id":"\$3Ut97nFBgOtsrnRPW-pqr28z7ETNMttj7GcjkIv4zWw"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489154,"unsigned":{"age":894},"event_id":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Another User"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489290,"unsigned":{"replaces_state":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0","prev_content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"prev_sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","age":758},"event_id":"\$dtQblqCbjr3TGc3WmrQ4YTkHaXJ2PcO0TAYDr9K7iQc"}],"prev_batch":"t2-62_571_2_6_39_1_2_34_1","limited":true},"state":{"events":[{"type":"m.room.create","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"m.federate":false,"room_version":"9","creator":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944511,"unsigned":{"age":545537},"event_id":"\$PAWKKULBVOLnqfrAAtXZz8tHEPXXjgRVbJJLifwQWbE"}]},"account_data":{"events":[]},"ephemeral":{"events":[]},"unread_notifications":{"notification_count":0,"highlight_count":0},"summary":{"m.joined_member_count":3,"m.invited_member_count":0},"org.matrix.msc2654.unread_count":0}}}}',
),
),
);
final profile = await client.getUserProfile(client.userID!);
expect(profile.displayname, 'Some First Name Some Last Name');
expect(profile.outdated, false);
await client.dispose(closeDatabase: true);
});
test('sendToDeviceEncrypted', () async {
FakeMatrixApi.calledEndpoints.clear();
await matrix.sendToDeviceEncrypted(
matrix.userDeviceKeys['@alice:example.com']!.deviceKeys.values
.toList(),
'm.message',
{
'msgtype': 'm.text',
'body': 'Hello world',
});
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
),
true,
);
});
test('sendToDeviceEncryptedChunked', () async {
FakeMatrixApi.calledEndpoints.clear();
await matrix.sendToDeviceEncryptedChunked(
matrix.userDeviceKeys['@alice:example.com']!.deviceKeys.values
.toList(),
'm.message',
{
'msgtype': 'm.text',
'body': 'Hello world',
});
await Future.delayed(Duration(milliseconds: 100));
expect(
FakeMatrixApi.calledEndpoints.keys
.where(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
)
.length,
1,
);
final deviceKeys = <DeviceKeys>[];
for (var i = 0; i < 30; i++) {
final account = olm.Account();
account.create();
final keys = json.decode(account.identity_keys());
final userId = '@testuser:example.org';
final deviceId = 'DEVICE$i';
final keyObj = {
'user_id': userId,
'device_id': deviceId,
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2',
],
'keys': {
'curve25519:$deviceId': keys['curve25519'],
'ed25519:$deviceId': keys['ed25519'],
},
};
final signature =
account.sign(String.fromCharCodes(canonicalJson.encode(keyObj)));
keyObj['signatures'] = {
userId: {
'ed25519:$deviceId': signature,
},
};
account.free();
deviceKeys.add(DeviceKeys.fromJson(keyObj, matrix));
}
FakeMatrixApi.calledEndpoints.clear();
await matrix.sendToDeviceEncryptedChunked(deviceKeys, 'm.message', {
'msgtype': 'm.text',
'body': 'Hello world',
});
// it should send the first chunk right away
expect(
FakeMatrixApi.calledEndpoints.keys
.where(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
)
.length,
1,
);
await Future.delayed(Duration(milliseconds: 100));
expect(
FakeMatrixApi.calledEndpoints.keys
.where(
(k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
)
.length,
2,
);
});
test('send to_device queue', () async {
// we test:
// send fox --> fail
// send raccoon --> fox & raccoon sent
// send bunny --> only bunny sent
final client = await getClient();
FakeMatrixApi.failToDevice = true;
final foxContent = {
'@fox:example.org': {
'*': {
'fox': 'hole',
},
},
};
final raccoonContent = {
'@fox:example.org': {
'*': {
'raccoon': 'mask',
},
},
};
final bunnyContent = {
'@fox:example.org': {
'*': {
'bunny': 'burrow',
},
},
};
await client
.sendToDevice('foxies', 'floof_txnid', foxContent)
.catchError((e) => null); // ignore the error
FakeMatrixApi.failToDevice = false;
FakeMatrixApi.calledEndpoints.clear();
await client.sendToDevice('raccoon', 'raccoon_txnid', raccoonContent);
expect(
json.decode(
FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid']
?[0],
)['messages'],
foxContent,
);
expect(
json.decode(
FakeMatrixApi.calledEndpoints[
'/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0],
)['messages'],
raccoonContent,
);
FakeMatrixApi.calledEndpoints.clear();
await client.sendToDevice('bunny', 'bunny_txnid', bunnyContent);
expect(
FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid'],
null,
);
expect(
FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/raccoon/raccoon_txnid'],
null,
);
expect(
json.decode(
FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid']?[0],
)['messages'],
bunnyContent,
);
await client.dispose(closeDatabase: true);
});
test('send to_device queue multiple', () async {
// we test:
// send fox --> fail
// send raccoon --> fail
// send bunny --> all sent
final client = await getClient();
await client.abortSync();
FakeMatrixApi.failToDevice = true;
final foxContent = {
'@fox:example.org': {
'*': {
'fox': 'hole',
},
},
};
final raccoonContent = {
'@fox:example.org': {
'*': {
'raccoon': 'mask',
},
},
};
final bunnyContent = {
'@fox:example.org': {
'*': {
'bunny': 'burrow',
},
},
};
await client
.sendToDevice('foxies', 'floof_txnid', foxContent)
.catchError((e) => null); // ignore the error
await FakeMatrixApi.firstWhereValue(
'/client/v3/sendToDevice/foxies/floof_txnid',
);
FakeMatrixApi.calledEndpoints.clear();
await client
.sendToDevice('raccoon', 'raccoon_txnid', raccoonContent)
.catchError((e) => null);
await FakeMatrixApi.firstWhereValue(
'/client/v3/sendToDevice/foxies/floof_txnid',
);
FakeMatrixApi.calledEndpoints.clear();
FakeMatrixApi.failToDevice = false;
await client.sendToDevice('bunny', 'bunny_txnid', bunnyContent);
await FakeMatrixApi.firstWhereValue(
'/client/v3/sendToDevice/foxies/floof_txnid',
);
await FakeMatrixApi.firstWhereValue(
'/client/v3/sendToDevice/bunny/bunny_txnid',
);
final foxcall = FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid']?[0];
expect(foxcall != null, true);
expect(json.decode(foxcall)['messages'], foxContent);
final racooncall = FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0];
expect(racooncall != null, true);
expect(json.decode(racooncall)['messages'], raccoonContent);
final bunnycall = FakeMatrixApi
.calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid']?[0];
expect(bunnycall != null, true);
expect(json.decode(bunnycall)['messages'], bunnyContent);
await client.dispose(closeDatabase: true);
});
test('startDirectChat', () async {
await matrix.startDirectChat('@alice:example.com', waitForSync: false);
});
test('createGroupChat', () async {
await matrix.createGroupChat(groupName: 'Testgroup', waitForSync: false);
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
),
{
'initial_state': [
{
'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
'type': 'm.room.encryption',
}
],
'name': 'Testgroup',
'preset': 'private_chat',
},
);
await matrix.createGroupChat(
groupName: 'Testgroup',
waitForSync: false,
groupCall: true,
powerLevelContentOverride: {'events_default': 12},
);
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
),
{
'initial_state': [
{
'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
'type': 'm.room.encryption',
}
],
'name': 'Testgroup',
'power_level_content_override': {
'events_default': 12,
'events': {'com.famedly.call.member': 12},
},
'preset': 'private_chat',
},
);
await matrix.createGroupChat(
groupName: 'Testgroup',
waitForSync: false,
groupCall: true,
powerLevelContentOverride: {
'events_default': 12,
'events': {'com.famedly.call.member': 14},
},
);
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
),
{
'initial_state': [
{
'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
'type': 'm.room.encryption',
}
],
'name': 'Testgroup',
'power_level_content_override': {
'events_default': 12,
'events': {'com.famedly.call.member': 14},
},
'preset': 'private_chat',
},
);
await matrix.createGroupChat(
groupName: 'Testgroup',
waitForSync: false,
groupCall: true,
);
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
),
{
'initial_state': [
{
'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
'type': 'm.room.encryption',
}
],
'name': 'Testgroup',
'power_level_content_override': {
'events': {'com.famedly.call.member': 0},
},
'preset': 'private_chat',
},
);
});
test('Test the fake store api', () async {
final database = await getDatabase(null);
final client1 = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: (_) => database,
);
await client1.init(
newToken: 'abc123',
newUserID: '@test:fakeServer.notExisting',
newHomeserver: Uri.parse('https://fakeServer.notExisting'),
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newOlmAccount: pickledOlmAccount,
);
await client1.abortSync();
expect(client1.isLogged(), true);
expect(client1.rooms.length, 3);
final client2 = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: (_) => database,
);
await client2.init();
await client2.abortSync();
expect(client2.isLogged(), true);
expect(client2.accessToken, client1.accessToken);
expect(client2.userID, client1.userID);
expect(client2.homeserver, client1.homeserver);
expect(client2.deviceID, client1.deviceID);
expect(client2.deviceName, client1.deviceName);
expect(client2.rooms.length, 3);
if (client2.encryptionEnabled) {
expect(
client2.encryption?.fingerprintKey,
client1.encryption?.fingerprintKey,
);
expect(
client2.encryption?.identityKey,
client1.encryption?.identityKey,
);
expect(client2.rooms[1].id, client1.rooms[1].id);
}
await client1.logout();
await client2.logout();
});
test('changePassword', () async {
await matrix.changePassword('1234', oldPassword: '123456');
});
test('ignoredUsers', () async {
expect(matrix.ignoredUsers, []);
matrix.accountData['m.ignored_user_list'] = BasicEvent(
type: 'm.ignored_user_list',
content: {
'ignored_users': {
'@charley:stupid.abc': {},
},
},
);
expect(matrix.ignoredUsers, ['@charley:stupid.abc']);
await matrix.ignoreUser('@charley2:stupid.abc');
await matrix.unignoreUser('@charley:stupid.abc');
});
test('upload', () async {
final client = await getClient();
final response =
await client.uploadContent(Uint8List(0), filename: 'file.jpeg');
expect(response.toString(), 'mxc://example.com/AQwafuaFswefuhsfAFAgsw');
expect(
await client.database?.getFile(response) != null,
client.database?.supportsFileStoring,
);
await client.dispose(closeDatabase: true);
});
test('wellKnown cache', () async {
final client = await getClient();
expect(client.wellKnown, null);
await client.getWellknown();
expect(
client.wellKnown?.mHomeserver.baseUrl.host,
'fakeserver.notexisting',
);
await client.dispose();
});
test('refreshAccessToken', () async {
final client = await getClient();
expect(client.accessToken, 'abcd');
await client.refreshAccessToken();
expect(client.accessToken, 'a_new_token');
await client.dispose();
});
test('handleSoftLogout', () async {
final client = await getClient();
expect(client.accessToken, 'abcd');
var softLoggedOut = 0;
client.onSoftLogout = (client) {
softLoggedOut++;
return client.refreshAccessToken();
};
FakeMatrixApi.expectedAccessToken = 'a_new_token';
await client.oneShotSync();
await client.oneShotSync();
FakeMatrixApi.expectedAccessToken = null;
expect(client.accessToken, 'a_new_token');
expect(softLoggedOut, 1);
final storedClient = await client.database?.getClient(client.clientName);
expect(storedClient?.tryGet<String>('token'), 'a_new_token');
expect(
storedClient?.tryGet<String>('refresh_token'),
'another_new_token',
);
await client.dispose();
});
test('object equality', () async {
final time1 = DateTime.fromMillisecondsSinceEpoch(1);
final time2 = DateTime.fromMillisecondsSinceEpoch(0);
final user1 =
User('@user1:example.org', room: Room(id: '!room1', client: matrix));
final user2 =
User('@user2:example.org', room: Room(id: '!room1', client: matrix));
// receipts
expect(Receipt(user1, time1) == Receipt(user1, time1), true);
expect(Receipt(user1, time1) == Receipt(user1, time2), false);
expect(Receipt(user1, time1) == Receipt(user2, time1), false);
// ignore: unrelated_type_equality_checks
expect(Receipt(user1, time1) == 'beep', false);
// users
expect(user1 == user1, true);
expect(user1 == user2, false);
expect(
user1 ==
User(
'@user1:example.org',
room: Room(id: '!room2', client: matrix),
),
false,
);
expect(
user1 ==
User(
'@user1:example.org',
room: Room(id: '!room1', client: matrix),
membership: 'leave',
),
false,
);
// ignore: unrelated_type_equality_checks
expect(user1 == 'beep', false);
// rooms
expect(
Room(id: '!room1', client: matrix) ==
Room(id: '!room1', client: matrix),
true,
);
expect(
Room(id: '!room1', client: matrix) ==
Room(id: '!room2', client: matrix),
false,
);
// ignore: unrelated_type_equality_checks
expect(Room(id: '!room1', client: matrix) == 'beep', false);
});
test('clearCache', () async {
final client = await getClient();
client.backgroundSync = true;
await client.clearCache();
await client.dispose();
});
test('dispose', () async {
await matrix.dispose(closeDatabase: true);
});
test('Database Migration', () async {
final firstDatabase = await getDatabase(null);
final firstClient = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: (_) => firstDatabase,
);
FakeMatrixApi.client = firstClient;
await firstClient.checkHomeserver(
Uri.parse('https://fakeServer.notExisting'),
checkWellKnown: false,
);
await firstClient.init(
newToken: 'abcd',
newUserID: '@test:fakeServer.notExisting',
newHomeserver: firstClient.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newOlmAccount: pickledOlmAccount,
);
await Future.delayed(Duration(milliseconds: 200));
await firstClient.dispose(closeDatabase: false);
final newClient = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
legacyDatabaseBuilder: (_) => firstDatabase,
);
final Set<InitState> initStates = {};
await newClient.init(onInitStateChanged: initStates.add);
expect(initStates, {
InitState.initializing,
InitState.migratingDatabase,
InitState.settingUpEncryption,
InitState.finished,
});
await Future.delayed(Duration(milliseconds: 200));
expect(newClient.isLogged(), true);
await newClient.dispose(closeDatabase: false);
await firstDatabase.close();
final sameOldFirstDatabase = await getDatabase(null);
expect(await sameOldFirstDatabase.getClient('testclient'), null);
});
test('getEventByPushNotification', () async {
final client = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
)
..accessToken = '1234'
..baseUri = Uri.parse('https://fakeserver.notexisting');
Event? event;
event = await client
.getEventByPushNotification(PushNotification(devices: []));
expect(event, null);
event = await client.getEventByPushNotification(
PushNotification(
devices: [],
eventId: '123',
roomId: '!localpart2:server.abc',
content: {
'msgtype': 'm.text',
'body': 'Hello world',
},
roomAlias: '#testalias:blaaa',
roomName: 'TestRoomName',
sender: '@alicyy:example.com',
senderDisplayName: 'AlicE',
type: 'm.room.message',
),
);
expect(event?.eventId, '123');
expect(event?.body, 'Hello world');
expect(event?.senderId, '@alicyy:example.com');
expect(event?.senderFromMemoryOrFallback.calcDisplayname(), 'AlicE');
expect(event?.type, 'm.room.message');
expect(event?.messageType, 'm.text');
expect(event?.room.id, '!localpart2:server.abc');
expect(event?.room.name, 'TestRoomName');
expect(event?.room.canonicalAlias, '#testalias:blaaa');
final storedEvent =
await client.database?.getEventById('123', event!.room);
expect(storedEvent?.eventId, event?.eventId);
event = await client.getEventByPushNotification(
PushNotification(
devices: [],
eventId: '1234',
roomId: '!localpart:server.abc',
),
);
expect(event?.eventId, '143273582443PhrSn:example.org');
expect(event?.room.id, '!localpart:server.abc');
expect(event?.body, 'This is an example text message');
expect(event?.messageType, 'm.text');
expect(event?.type, 'm.room.message');
final storedEvent2 = await client.database
?.getEventById('143273582443PhrSn:example.org', event!.room);
expect(storedEvent2?.eventId, event?.eventId);
});
test('Rooms and archived rooms getter', () async {
final client = await getClient();
await Future.delayed(Duration(milliseconds: 50));
expect(
client.rooms.length,
3,
reason:
'Count of invited+joined before loadArchive() rooms does not match',
);
expect(
client.archivedRooms.length,
0,
reason: 'Count of archived rooms before loadArchive() does not match',
);
await client.loadArchive();
expect(
client.rooms.length,
3,
reason: 'Count of invited+joined rooms does not match',
);
expect(
client.archivedRooms.length,
2,
reason: 'Count of archived rooms does not match',
);
expect(
client.archivedRooms.firstWhereOrNull(
(r) => r.room.id == '!5345234234:example.com',
) !=
null,
true,
reason: '!5345234234:example.com not found as archived room',
);
expect(
client.archivedRooms.firstWhereOrNull(
(r) => r.room.id == '!5345234235:example.com',
) !=
null,
true,
reason: '!5345234235:example.com not found as archived room',
);
await client.dispose();
});
test(
'Client Init Exception',
() async {
final customClient = Client(
'failclient',
databaseBuilder: getMatrixSdkDatabase,
);
try {
await customClient.init(
newToken: 'testtoken',
newDeviceID: 'testdeviceid',
newDeviceName: 'testdevicename',
newHomeserver: Uri.parse('https://test.server'),
newOlmAccount: 'abcd',
newUserID: '@user:server',
);
throw Exception('No exception?');
} on ClientInitException catch (error) {
expect(error.accessToken, 'testtoken');
expect(error.deviceId, 'testdeviceid');
expect(error.deviceName, 'testdevicename');
expect(error.homeserver, Uri.parse('https://test.server'));
expect(error.olmAccount, 'abcd');
expect(error.userId, '@user:server');
expect(error.toString(), 'Exception: BAD_ACCOUNT_KEY');
}
await customClient.dispose(closeDatabase: true);
},
);
tearDown(() async {
await matrix.dispose(closeDatabase: true);
});
});
}