Initial commit version 0.8.13

This commit is contained in:
PCoder 2022-04-18 14:27:08 +05:30
commit 9526dfa4f2
111 changed files with 35074 additions and 0 deletions

View file

@ -0,0 +1,54 @@
/*
* 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 'package:canonical_json/canonical_json.dart';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
void main() {
/// All Tests related to the ChatTime
group('Canonical Json', () {
Logs().level = Level.error;
final textMap = <String, Map<String, dynamic>>{
'{}': {},
'{"one":1,"two":"Two"}': {'one': 1, 'two': 'Two'},
'{"a":"1","b":"2"}': {'b': '2', 'a': '1'},
'{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}':
{
'auth': {
'success': true,
'mxid': '@john.doe:example.com',
'profile': {
'display_name': 'John Doe',
'three_pids': [
{'medium': 'email', 'address': 'john.doe@example.org'},
{'medium': 'msisdn', 'address': '123456789'}
]
}
}
},
'{"a":null}': {'a': null},
};
for (final entry in textMap.entries) {
test(entry.key, () async {
expect(
entry.key, String.fromCharCodes(canonicalJson.encode(entry.value)));
});
}
});
}

921
test/client_test.dart Normal file

File diff suppressed because one or more lines are too long

323
test/commands_test.dart Normal file
View file

@ -0,0 +1,323 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2021 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:convert';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'package:matrix/matrix.dart';
import 'fake_client.dart';
import 'fake_matrix_api.dart';
void main() {
group('Commands', () {
late Client client;
late Room room;
var olmEnabled = true;
final getLastMessagePayload =
([String type = 'm.room.message', String? stateKey]) {
final state = stateKey != null;
return json.decode(FakeMatrixApi.calledEndpoints.entries
.firstWhere((e) => e.key.startsWith(
'/client/r0/rooms/${Uri.encodeComponent(room.id)}/${state ? 'state' : 'send'}/${Uri.encodeComponent(type)}${state && stateKey?.isNotEmpty == true ? '/' + Uri.encodeComponent(stateKey!) : ''}'))
.value
.first);
};
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
}
client = await getClient();
room = Room(id: '!1234:fakeServer.notExisting', client: client);
room.setState(Event(
type: 'm.room.power_levels',
content: {},
room: room,
stateKey: '',
eventId: '\$fakeeventid',
originServerTs: DateTime.now(),
senderId: '\@fakeuser:fakeServer.notExisting',
));
room.setState(Event(
type: 'm.room.member',
content: {'membership': 'join'},
room: room,
stateKey: client.userID,
eventId: '\$fakeeventid',
originServerTs: DateTime.now(),
senderId: '\@fakeuser:fakeServer.notExisting',
));
});
test('send', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/send Hello World');
var sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': 'Hello World',
});
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('Beep Boop');
sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': 'Beep Boop',
});
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('Beep *Boop*');
sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': 'Beep *Boop*',
'format': 'org.matrix.custom.html',
'formatted_body': 'Beep <em>Boop</em>',
});
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('//send Hello World');
sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': '/send Hello World',
});
});
test('me', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/me heya');
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.emote',
'body': 'heya',
});
});
test('plain', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/plain *floof*');
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': '*floof*',
});
});
test('html', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/html <b>yay</b>');
final sent = getLastMessagePayload();
expect(sent, {
'msgtype': 'm.text',
'body': '<b>yay</b>',
'format': 'org.matrix.custom.html',
'formatted_body': '<b>yay</b>',
});
});
test('react', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/react 🦊',
inReplyTo: Event(
eventId: '\$event',
type: 'm.room.message',
content: {
'msgtype': 'm.text',
'body': '<b>yay</b>',
'format': 'org.matrix.custom.html',
'formatted_body': '<b>yay</b>',
},
originServerTs: DateTime.now(),
senderId: client.userID!,
room: room,
));
final sent = getLastMessagePayload('m.reaction');
expect(sent, {
'm.relates_to': {
'rel_type': 'm.annotation',
'event_id': '\$event',
'key': '🦊',
},
});
});
test('join', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/join !newroom:example.com');
expect(
FakeMatrixApi
.calledEndpoints['/client/r0/join/!newroom%3Aexample.com']
?.first !=
null,
true);
});
test('leave', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/leave');
expect(
FakeMatrixApi
.calledEndpoints[
'/client/r0/rooms/!1234%3AfakeServer.notExisting/leave']
?.first !=
null,
true);
});
test('op', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/op @user:example.org');
var sent = getLastMessagePayload('m.room.power_levels', '');
expect(sent, {
'users': {'@user:example.org': 50}
});
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/op @user:example.org 100');
sent = getLastMessagePayload('m.room.power_levels', '');
expect(sent, {
'users': {'@user:example.org': 100}
});
});
test('kick', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/kick @baduser:example.org');
expect(
json.decode(FakeMatrixApi
.calledEndpoints[
'/client/r0/rooms/!1234%3AfakeServer.notExisting/kick']
?.first),
{
'user_id': '@baduser:example.org',
});
});
test('ban', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/ban @baduser:example.org');
expect(
json.decode(FakeMatrixApi
.calledEndpoints[
'/client/r0/rooms/!1234%3AfakeServer.notExisting/ban']
?.first),
{
'user_id': '@baduser:example.org',
});
});
test('unban', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/unban @baduser:example.org');
expect(
json.decode(FakeMatrixApi
.calledEndpoints[
'/client/r0/rooms/!1234%3AfakeServer.notExisting/unban']
?.first),
{
'user_id': '@baduser:example.org',
});
});
test('invite', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/invite @baduser:example.org');
expect(
json.decode(FakeMatrixApi
.calledEndpoints[
'/client/r0/rooms/!1234%3AfakeServer.notExisting/invite']
?.first),
{
'user_id': '@baduser:example.org',
});
});
test('myroomnick', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/myroomnick Foxies~');
final sent = getLastMessagePayload('m.room.member', client.userID);
expect(sent, {
'displayname': 'Foxies~',
'membership': 'join',
});
});
test('myroomavatar', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/myroomavatar mxc://beep/boop');
final sent = getLastMessagePayload('m.room.member', client.userID);
expect(sent, {
'avatar_url': 'mxc://beep/boop',
'membership': 'join',
});
});
test('dm', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/dm @alice:example.com --no-encryption');
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/createRoom']?.first),
{
'invite': ['@alice:example.com'],
'is_direct': true,
'preset': 'trusted_private_chat'
});
});
test('create', () async {
FakeMatrixApi.calledEndpoints.clear();
await room.sendTextEvent('/create @alice:example.com --no-encryption');
expect(
json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/createRoom']?.first),
{'preset': 'private_chat'});
});
test('discardsession', () async {
if (olmEnabled) {
await client.encryption?.keyManager.createOutboundGroupSession(room.id);
expect(
client.encryption?.keyManager.getOutboundGroupSession(room.id) !=
null,
true);
await room.sendTextEvent('/discardsession');
expect(
client.encryption?.keyManager.getOutboundGroupSession(room.id) !=
null,
false);
}
});
test('create', () async {
await room.sendTextEvent('/clearcache');
expect(room.client.prevBatch, null);
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
}

485
test/database_api_test.dart Normal file
View file

@ -0,0 +1,485 @@
/*
* 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:convert';
import 'dart:typed_data';
import 'dart:async';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'fake_database.dart';
void main() {
group('FluffyBox Database Test', () {
testDatabase(
getFluffyBoxDatabase(null),
);
});
group('Hive Database Test', () {
testDatabase(
getHiveDatabase(null),
);
});
}
Future<bool> olmEnabled() async {
var olmEnabled = true;
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
}
return olmEnabled;
}
void testDatabase(
Future<DatabaseApi> futureDatabase,
) {
late DatabaseApi database;
late int toDeviceQueueIndex;
test('Open', () async {
database = await futureDatabase;
});
test('transaction', () async {
var counter = 0;
await database.transaction(() async {
expect(counter++, 0);
await database.transaction(() async {
expect(counter++, 1);
await Future.delayed(Duration(milliseconds: 50));
expect(counter++, 2);
});
expect(counter++, 3);
});
// we can't use Zone.root.run inside of tests so we abuse timers instead
Timer(Duration(milliseconds: 50), () async {
await database.transaction(() async {
expect(counter++, 6);
});
});
await database.transaction(() async {
expect(counter++, 4);
await Future.delayed(Duration(milliseconds: 100));
expect(counter++, 5);
});
});
test('insertIntoToDeviceQueue', () async {
toDeviceQueueIndex = await database.insertIntoToDeviceQueue(
'm.test',
'txnId',
'{"foo":"bar"}',
);
});
test('getToDeviceEventQueue', () async {
final toDeviceQueue = await database.getToDeviceEventQueue();
expect(toDeviceQueue.first.type, 'm.test');
});
test('deleteFromToDeviceQueue', () async {
await database.deleteFromToDeviceQueue(toDeviceQueueIndex);
final toDeviceQueue = await database.getToDeviceEventQueue();
expect(toDeviceQueue.isEmpty, true);
});
test('storeFile', () async {
await database.storeFile(
Uri.parse('mxc://test'), Uint8List.fromList([0]), 0);
final file = await database.getFile(Uri.parse('mxc://test'));
expect(file != null, database.supportsFileStoring);
});
test('getFile', () async {
await database.getFile(Uri.parse('mxc://test'));
});
test('deleteOldFiles', () async {
await database.deleteOldFiles(1);
final file = await database.getFile(Uri.parse('mxc://test'));
expect(file == null, true);
});
test('storeRoomUpdate', () async {
final roomUpdate = JoinedRoomUpdate.fromJson({
'highlight_count': 0,
'notification_count': 0,
'limited_timeline': false,
'membership': Membership.join,
});
final client = Client('testclient');
await database.storeRoomUpdate('!testroom', roomUpdate, client);
final rooms = await database.getRoomList(client);
expect(rooms.single.id, '!testroom');
});
test('getRoomList', () async {
final list = await database.getRoomList(Client('testclient'));
expect(list.single.id, '!testroom');
});
test('setRoomPrevBatch', () async {
final client = Client('testclient');
await database.setRoomPrevBatch('1234', '!testroom', client);
final rooms = await database.getRoomList(client);
expect(rooms.single.prev_batch, '1234');
});
test('forgetRoom', () async {
await database.forgetRoom('!testroom');
final rooms = await database.getRoomList(Client('testclient'));
expect(rooms.isEmpty, true);
});
test('getClient', () async {
await database.getClient('name');
});
test('insertClient', () async {
await database.insertClient(
'name',
'homeserverUrl',
'token',
'userId',
'deviceId',
'deviceName',
'prevBatch',
'olmAccount',
);
final client = await database.getClient('name');
expect(client?['token'], 'token');
});
test('updateClient', () async {
await database.updateClient(
'homeserverUrl',
'token_different',
'userId',
'deviceId',
'deviceName',
'prevBatch',
'olmAccount',
);
final client = await database.getClient('name');
expect(client?['token'], 'token_different');
});
test('updateClientKeys', () async {
await database.updateClientKeys(
'olmAccount2',
);
final client = await database.getClient('name');
expect(client?['olm_account'], 'olmAccount2');
});
test('storeSyncFilterId', () async {
await database.storeSyncFilterId(
'1234',
);
final client = await database.getClient('name');
expect(client?['sync_filter_id'], '1234');
});
test('getAccountData', () async {
await database.getAccountData();
});
test('storeAccountData', () async {
await database.storeAccountData('m.test', '{"foo":"bar"}');
final events = await database.getAccountData();
expect(events.values.single.type, 'm.test');
await database.storeAccountData('m.abc+de', '{"foo":"bar"}');
final events2 = await database.getAccountData();
expect(events2.values.any((element) => element.type == 'm.abc+de'), true);
});
test('storeEventUpdate', () async {
await database.storeEventUpdate(
EventUpdate(
roomID: '!testroom:example.com',
type: EventUpdateType.timeline,
content: {
'type': EventTypes.Message,
'content': {
'body': '* edit 3',
'msgtype': 'm.text',
'm.new_content': {
'body': 'edit 3',
'msgtype': 'm.text',
},
'm.relates_to': {
'event_id': '\$source',
'rel_type': RelationshipTypes.edit,
},
},
'event_id': '\$event:example.com',
'sender': '@bob:example.org',
},
),
Client('testclient'));
});
test('getEventById', () async {
final event = await database.getEventById('\$event:example.com',
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(event?.type, EventTypes.Message);
});
test('getEventList', () async {
final events = await database.getEventList(
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(events.single.type, EventTypes.Message);
});
test('getUser', () async {
final user = await database.getUser('@bob:example.org',
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(user, null);
});
test('getUsers', () async {
final users = await database.getUsers(
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(users.isEmpty, true);
});
test('removeEvent', () async {
await database.removeEvent('\$event:example.com', '!testroom:example.com');
final event = await database.getEventById('\$event:example.com',
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(event, null);
});
test('getAllInboundGroupSessions', () async {
final result = await database.getAllInboundGroupSessions();
expect(result.isEmpty, true);
});
test('getInboundGroupSession', () async {
await database.getInboundGroupSession('!testroom:example.com', 'sessionId');
});
test('getInboundGroupSessionsToUpload', () async {
await database.getInboundGroupSessionsToUpload();
});
test('storeInboundGroupSession', () async {
await database.storeInboundGroupSession(
'!testroom:example.com',
'sessionId',
'pickle',
'{"foo":"bar"}',
'{}',
'{}',
'senderKey',
'{}',
);
final session = await database.getInboundGroupSession(
'!testroom:example.com',
'sessionId',
);
expect(jsonDecode(session!.content)['foo'], 'bar');
});
test('markInboundGroupSessionAsUploaded', () async {
await database.markInboundGroupSessionAsUploaded(
'!testroom:example.com', 'sessionId');
});
test('markInboundGroupSessionsAsNeedingUpload', () async {
await database.markInboundGroupSessionsAsNeedingUpload();
});
test('updateInboundGroupSessionAllowedAtIndex', () async {
await database.updateInboundGroupSessionAllowedAtIndex(
'{}',
'!testroom:example.com',
'sessionId',
);
});
test('updateInboundGroupSessionIndexes', () async {
await database.updateInboundGroupSessionIndexes(
'{}',
'!testroom:example.com',
'sessionId',
);
});
test('getSSSSCache', () async {
final cache = await database.getSSSSCache('type');
expect(cache, null);
});
test('storeSSSSCache', () async {
await database.storeSSSSCache('type', 'keyId', 'ciphertext', '{}');
final cache = (await database.getSSSSCache('type'))!;
expect(cache.type, 'type');
expect(cache.keyId, 'keyId');
expect(cache.ciphertext, 'ciphertext');
expect(cache.content, '{}');
});
test('getOlmSessions', () async {
final olm = await database.getOlmSessions(
'identityKey',
'userId',
);
expect(olm.isEmpty, true);
});
test('getAllOlmSessions', () async {
var sessions = await database.getAllOlmSessions();
expect(sessions.isEmpty, true);
await database.storeOlmSession(
'identityKey',
'sessionId',
'pickle',
0,
);
await database.storeOlmSession(
'identityKey',
'sessionId2',
'pickle',
0,
);
sessions = await database.getAllOlmSessions();
expect(
sessions,
{
'identityKey': {
'sessionId': {
'identity_key': 'identityKey',
'pickle': 'pickle',
'session_id': 'sessionId',
'last_received': 0
},
'sessionId2': {
'identity_key': 'identityKey',
'pickle': 'pickle',
'session_id': 'sessionId2',
'last_received': 0
}
}
},
);
});
test('getOlmSessionsForDevices', () async {
final olm = await database.getOlmSessionsForDevices(
['identityKeys'],
'userId',
);
expect(olm.isEmpty, true);
});
test('storeOlmSession', () async {
if (!(await olmEnabled())) return;
await database.storeOlmSession(
'identityKey',
'sessionId',
'pickle',
0,
);
final olm = await database.getOlmSessions(
'identityKey',
'userId',
);
expect(olm.isNotEmpty, true);
});
test('getOutboundGroupSession', () async {
final session = await database.getOutboundGroupSession(
'!testroom:example.com',
'@alice:example.com',
);
expect(session, null);
});
test('storeOutboundGroupSession', () async {
if (!(await olmEnabled())) return;
await database.storeOutboundGroupSession(
'!testroom:example.com',
'pickle',
'{}',
0,
);
final session = await database.getOutboundGroupSession(
'!testroom:example.com',
'@alice:example.com',
);
expect(session?.devices.isEmpty, true);
});
test('getLastSentMessageUserDeviceKey', () async {
final list = await database.getLastSentMessageUserDeviceKey(
'userId',
'deviceId',
);
expect(list.isEmpty, true);
});
test('getUnimportantRoomEventStatesForRoom', () async {
final events = await database.getUnimportantRoomEventStatesForRoom(
['events'],
Room(id: '!mep', client: Client('testclient')),
);
expect(events.isEmpty, true);
});
test('getUserDeviceKeys', () async {
await database.getUserDeviceKeys(Client('testclient'));
});
test('storeUserCrossSigningKey', () async {
await database.storeUserCrossSigningKey(
'@alice:example.com',
'publicKey',
'{}',
false,
false,
);
});
test('setVerifiedUserCrossSigningKey', () async {
await database.setVerifiedUserCrossSigningKey(
true,
'@alice:example.com',
'publicKey',
);
});
test('setBlockedUserCrossSigningKey', () async {
await database.setBlockedUserCrossSigningKey(
true,
'@alice:example.com',
'publicKey',
);
});
test('removeUserCrossSigningKey', () async {
await database.removeUserCrossSigningKey(
'@alice:example.com',
'publicKey',
);
});
test('storeUserDeviceKeysInfo', () async {
await database.storeUserDeviceKeysInfo(
'@alice:example.com',
true,
);
});
test('storeUserDeviceKey', () async {
await database.storeUserDeviceKey(
'@alice:example.com',
'deviceId',
'{}',
false,
false,
0,
);
});
test('setVerifiedUserDeviceKey', () async {
await database.setVerifiedUserDeviceKey(
true,
'@alice:example.com',
'deviceId',
);
});
test('setBlockedUserDeviceKey', () async {
await database.setBlockedUserDeviceKey(
true,
'@alice:example.com',
'deviceId',
);
});
// Clearing up from here
test('clearSSSSCache', () async {
await database.clearSSSSCache();
});
test('clearCache', () async {
await database.clearCache();
});
test('clear', () async {
await database.clear();
});
test('Close', () async {
await database.close();
});
return;
}

View file

@ -0,0 +1,265 @@
/*
* 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:convert';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import './fake_client.dart';
import './fake_matrix_api.dart';
void main() {
/// All Tests related to device keys
group('Device keys', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('fromJson', () async {
if (!olmEnabled) return;
var rawJson = <String, dynamic>{
'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS',
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:JLAFKJWSCS':
'3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
'ed25519:JLAFKJWSCS': 'lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI'
},
'signatures': {
'@alice:example.com': {
'ed25519:JLAFKJWSCS':
'dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA'
}
},
'unsigned': {'device_display_name': "Alice's mobile phone"},
};
final key = DeviceKeys.fromJson(rawJson, client);
// NOTE(Nico): this actually doesn't do anything, because the device signature is invalid...
await key.setVerified(false, false);
await key.setBlocked(true);
expect(json.encode(key.toJson()), json.encode(rawJson));
expect(key.directVerified, false);
expect(key.blocked, true);
rawJson = <String, dynamic>{
'user_id': '@test:fakeServer.notExisting',
'usage': ['master'],
'keys': {
'ed25519:82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8':
'82mAXjsmbTbrE6zyShpR869jnrANO75H8nYY0nDLoJ8',
},
'signatures': {},
};
final crossKey = CrossSigningKey.fromJson(rawJson, client);
expect(json.encode(crossKey.toJson()), json.encode(rawJson));
expect(crossKey.usage.first, 'master');
});
test('reject devices without self-signature', () async {
if (!olmEnabled) return;
var key = DeviceKeys.fromJson({
'user_id': '@test:fakeServer.notExisting',
'device_id': 'BADDEVICE',
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:BADDEVICE': 'ds6+bItpDiWyRaT/b0ofoz1R+GCy7YTbORLJI4dmYho',
'ed25519:BADDEVICE': 'CdDKVf44LO2QlfWopP6VWmqedSrRaf9rhHKvdVyH38w'
},
}, client);
expect(key.isValid, false);
expect(key.selfSigned, false);
key = DeviceKeys.fromJson({
'user_id': '@test:fakeServer.notExisting',
'device_id': 'BADDEVICE',
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:BADDEVICE': 'ds6+bItpDiWyRaT/b0ofoz1R+GCy7YTbORLJI4dmYho',
'ed25519:BADDEVICE': 'CdDKVf44LO2QlfWopP6VWmqedSrRaf9rhHKvdVyH38w'
},
'signatures': {
'@test:fakeServer.notExisting': {
'ed25519:BADDEVICE': 'invalid',
},
},
}, client);
expect(key.isValid, false);
expect(key.selfSigned, false);
});
test('set blocked / verified', () async {
if (!olmEnabled) return;
final key =
client.userDeviceKeys[client.userID]!.deviceKeys['OTHERDEVICE']!;
client.userDeviceKeys[client.userID]?.deviceKeys['UNSIGNEDDEVICE'] =
DeviceKeys.fromJson({
'user_id': '@test:fakeServer.notExisting',
'device_id': 'UNSIGNEDDEVICE',
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:UNSIGNEDDEVICE':
'ds6+bItpDiWyRaT/b0ofoz1R+GCy7YTbORLJI4dmYho',
'ed25519:UNSIGNEDDEVICE':
'CdDKVf44LO2QlfWopP6VWmqedSrRaf9rhHKvdVyH38w'
},
'signatures': {
'@test:fakeServer.notExisting': {
'ed25519:UNSIGNEDDEVICE':
'f2p1kv6PIz+hnoFYnHEurhUKIyRsdxwR2RTKT1EnQ3aF2zlZOjmnndOCtIT24Q8vs2PovRw+/jkHKj4ge2yDDw',
},
},
}, client);
final masterKey = client.userDeviceKeys[client.userID]!.masterKey!;
masterKey.setDirectVerified(true);
// we need to populate the ssss cache to be able to test signing easily
final handle = client.encryption!.ssss.open();
await handle.unlock(recoveryKey: ssssKey);
await handle.maybeCacheAll();
expect(key.verified, true);
expect(key.encryptToDevice, true);
await key.setBlocked(true);
expect(key.verified, false);
expect(key.encryptToDevice, false);
await key.setBlocked(false);
expect(key.directVerified, false);
expect(key.verified, true); // still verified via cross-sgining
expect(key.encryptToDevice, true);
expect(
client.userDeviceKeys[client.userID]?.deviceKeys['UNSIGNEDDEVICE']
?.encryptToDevice,
false);
expect(masterKey.verified, true);
await masterKey.setBlocked(true);
expect(masterKey.verified, false);
expect(
client.userDeviceKeys[client.userID]?.deviceKeys['UNSIGNEDDEVICE']
?.encryptToDevice,
true);
await masterKey.setBlocked(false);
expect(masterKey.verified, true);
FakeMatrixApi.calledEndpoints.clear();
await key.setVerified(true);
await Future.delayed(Duration(milliseconds: 10));
expect(
FakeMatrixApi.calledEndpoints.keys
.any((k) => k == '/client/r0/keys/signatures/upload'),
true);
expect(key.directVerified, true);
FakeMatrixApi.calledEndpoints.clear();
await key.setVerified(false);
await Future.delayed(Duration(milliseconds: 10));
expect(
FakeMatrixApi.calledEndpoints.keys
.any((k) => k == '/client/r0/keys/signatures/upload'),
false);
expect(key.directVerified, false);
client.userDeviceKeys[client.userID]?.deviceKeys.remove('UNSIGNEDDEVICE');
});
test('verification based on signatures', () async {
if (!olmEnabled) return;
final user = client.userDeviceKeys[client.userID]!;
user.masterKey?.setDirectVerified(true);
expect(user.deviceKeys['GHTYAJCE']?.crossVerified, true);
expect(user.deviceKeys['GHTYAJCE']?.signed, true);
expect(user.getKey('GHTYAJCE')?.crossVerified, true);
expect(user.deviceKeys['OTHERDEVICE']?.crossVerified, true);
expect(user.selfSigningKey?.crossVerified, true);
expect(
user
.getKey('F9ypFzgbISXCzxQhhSnXMkc1vq12Luna3Nw5rqViOJY')
?.crossVerified,
true);
expect(user.userSigningKey?.crossVerified, true);
expect(user.verified, UserVerifiedStatus.verified);
user.masterKey?.setDirectVerified(false);
expect(user.deviceKeys['GHTYAJCE']?.crossVerified, false);
expect(user.deviceKeys['OTHERDEVICE']?.crossVerified, false);
expect(user.verified, UserVerifiedStatus.unknown);
user.deviceKeys['OTHERDEVICE']?.setDirectVerified(true);
expect(user.verified, UserVerifiedStatus.verified);
user.deviceKeys['OTHERDEVICE']?.setDirectVerified(false);
user.masterKey?.setDirectVerified(true);
user.deviceKeys['GHTYAJCE']?.signatures?[client.userID]
?.removeWhere((k, v) => k != 'ed25519:GHTYAJCE');
expect(user.deviceKeys['GHTYAJCE']?.verified,
true); // it's our own device, should be direct verified
expect(user.deviceKeys['GHTYAJCE']?.signed,
false); // not verified for others
user.deviceKeys['OTHERDEVICE']?.signatures?.clear();
expect(user.verified, UserVerifiedStatus.unknownDevice);
});
test('start verification', () async {
if (!olmEnabled) return;
var req = client
.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS']
?.startVerification();
expect(req != null, true);
expect(req?.room != null, false);
req = await client.userDeviceKeys['@alice:example.com']
?.startVerification(newDirectChatEnableEncryption: false);
expect(req != null, true);
expect(req?.room != null, true);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,275 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:matrix/encryption.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
void main() {
group('Bootstrap', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
late Map<String, dynamic> oldSecret;
late String origKeyId;
test('setupClient', () async {
client = await getClient();
await client.abortSync();
});
test('setup', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
Bootstrap? bootstrap;
bootstrap = client.encryption!.bootstrap(
onUpdate: () async {
while (bootstrap == null) {
await Future.delayed(Duration(milliseconds: 5));
}
if (bootstrap.state == BootstrapState.askWipeSsss) {
bootstrap.wipeSsss(true);
} else if (bootstrap.state == BootstrapState.askNewSsss) {
await bootstrap.newSsss('foxies');
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
bootstrap.wipeCrossSigning(true);
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
await bootstrap.askSetupCrossSigning(
setupMasterKey: true,
setupSelfSigningKey: true,
setupUserSigningKey: true,
);
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
bootstrap.wipeOnlineKeyBackup(true);
} else if (bootstrap.state ==
BootstrapState.askSetupOnlineKeyBackup) {
await bootstrap.askSetupOnlineKeyBackup(true);
}
},
);
while (bootstrap.state != BootstrapState.done) {
await Future.delayed(Duration(milliseconds: 50));
}
final defaultKey = client.encryption!.ssss.open();
await defaultKey.unlock(passphrase: 'foxies');
// test all the x-signing keys match up
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
final privateKey = base64
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
final keyObj = olm.PkSigning();
try {
final pubKey = keyObj.init_with_seed(privateKey);
expect(
pubKey,
client.userDeviceKeys[client.userID]
?.getCrossSigningKey(keyType)
?.publicKey);
} finally {
keyObj.free();
}
}
await defaultKey.store('foxes', 'floof');
await Future.delayed(Duration(milliseconds: 50));
oldSecret =
json.decode(json.encode(client.accountData['foxes']!.content));
origKeyId = defaultKey.keyId;
}, timeout: Timeout(Duration(minutes: 2)));
test('change recovery passphrase', () async {
if (!olmEnabled) return;
Bootstrap? bootstrap;
bootstrap = client.encryption!.bootstrap(
onUpdate: () async {
while (bootstrap == null) {
await Future.delayed(Duration(milliseconds: 5));
}
if (bootstrap.state == BootstrapState.askWipeSsss) {
bootstrap.wipeSsss(false);
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
bootstrap.useExistingSsss(false);
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
await bootstrap.oldSsssKeys![client.encryption!.ssss.defaultKeyId]!
.unlock(passphrase: 'foxies');
bootstrap.unlockedSsss();
} else if (bootstrap.state == BootstrapState.askNewSsss) {
await bootstrap.newSsss('newfoxies');
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
bootstrap.wipeCrossSigning(false);
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
bootstrap.wipeOnlineKeyBackup(false);
}
},
);
while (bootstrap.state != BootstrapState.done) {
await Future.delayed(Duration(milliseconds: 50));
}
final defaultKey = client.encryption!.ssss.open();
await defaultKey.unlock(passphrase: 'newfoxies');
// test all the x-signing keys match up
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
final privateKey = base64
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
final keyObj = olm.PkSigning();
try {
final pubKey = keyObj.init_with_seed(privateKey);
expect(
pubKey,
client.userDeviceKeys[client.userID]
?.getCrossSigningKey(keyType)
?.publicKey);
} finally {
keyObj.free();
}
}
expect(await defaultKey.getStored('foxes'), 'floof');
}, timeout: Timeout(Duration(minutes: 2)));
test('change passphrase with multiple keys', () async {
if (!olmEnabled) return;
await client.setAccountData(client.userID!, 'foxes', oldSecret);
await Future.delayed(Duration(milliseconds: 50));
Bootstrap? bootstrap;
bootstrap = client.encryption!.bootstrap(
onUpdate: () async {
while (bootstrap == null) {
await Future.delayed(Duration(milliseconds: 5));
}
if (bootstrap.state == BootstrapState.askWipeSsss) {
bootstrap.wipeSsss(false);
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
bootstrap.useExistingSsss(false);
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
await bootstrap.oldSsssKeys![client.encryption!.ssss.defaultKeyId]!
.unlock(passphrase: 'newfoxies');
await bootstrap.oldSsssKeys![origKeyId]!
.unlock(passphrase: 'foxies');
bootstrap.unlockedSsss();
} else if (bootstrap.state == BootstrapState.askNewSsss) {
await bootstrap.newSsss('supernewfoxies');
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
bootstrap.wipeCrossSigning(false);
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
bootstrap.wipeOnlineKeyBackup(false);
}
},
);
while (bootstrap.state != BootstrapState.done) {
await Future.delayed(Duration(milliseconds: 50));
}
final defaultKey = client.encryption!.ssss.open();
await defaultKey.unlock(passphrase: 'supernewfoxies');
// test all the x-signing keys match up
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
final privateKey = base64
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
final keyObj = olm.PkSigning();
try {
final pubKey = keyObj.init_with_seed(privateKey);
expect(
pubKey,
client.userDeviceKeys[client.userID]
?.getCrossSigningKey(keyType)
?.publicKey);
} finally {
keyObj.free();
}
}
expect(await defaultKey.getStored('foxes'), 'floof');
}, timeout: Timeout(Duration(minutes: 2)));
test('setup new ssss', () async {
if (!olmEnabled) return;
client.accountData.clear();
Bootstrap? bootstrap;
bootstrap = client.encryption!.bootstrap(
onUpdate: () async {
while (bootstrap == null) {
await Future.delayed(Duration(milliseconds: 5));
}
if (bootstrap.state == BootstrapState.askNewSsss) {
await bootstrap.newSsss('thenewestfoxies');
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
await bootstrap.askSetupCrossSigning();
} else if (bootstrap.state ==
BootstrapState.askSetupOnlineKeyBackup) {
await bootstrap.askSetupOnlineKeyBackup(false);
}
},
);
while (bootstrap.state != BootstrapState.done) {
await Future.delayed(Duration(milliseconds: 50));
}
final defaultKey = client.encryption!.ssss.open();
await defaultKey.unlock(passphrase: 'thenewestfoxies');
}, timeout: Timeout(Duration(minutes: 2)));
test('bad ssss', () async {
if (!olmEnabled) return;
client.accountData.clear();
await client.setAccountData(client.userID!, 'foxes', oldSecret);
await Future.delayed(Duration(milliseconds: 50));
var askedBadSsss = false;
Bootstrap? bootstrap;
bootstrap = client.encryption!.bootstrap(
onUpdate: () async {
while (bootstrap == null) {
await Future.delayed(Duration(milliseconds: 5));
}
if (bootstrap.state == BootstrapState.askWipeSsss) {
bootstrap.wipeSsss(false);
} else if (bootstrap.state == BootstrapState.askBadSsss) {
askedBadSsss = true;
bootstrap.ignoreBadSecrets(false);
}
},
);
while (bootstrap.state != BootstrapState.error) {
await Future.delayed(Duration(milliseconds: 50));
}
expect(askedBadSsss, true);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,122 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:convert';
import 'package:matrix/matrix.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('Cross Signing', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('basic things', () async {
if (!olmEnabled) return;
expect(client.encryption?.crossSigning.enabled, true);
});
test('selfSign', () async {
if (!olmEnabled) return;
final key = client.userDeviceKeys[client.userID]!.masterKey!;
key.setDirectVerified(false);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.crossSigning.selfSign(recoveryKey: ssssKey);
expect(key.directVerified, true);
expect(
FakeMatrixApi.calledEndpoints
.containsKey('/client/r0/keys/signatures/upload'),
true);
expect(await client.encryption!.crossSigning.isCached(), true);
});
test('signable', () async {
if (!olmEnabled) return;
expect(
client.encryption!.crossSigning
.signable([client.userDeviceKeys[client.userID!]!.masterKey!]),
true);
expect(
client.encryption!.crossSigning.signable([
client.userDeviceKeys[client.userID!]!.deviceKeys[client.deviceID!]!
]),
false);
expect(
client.encryption!.crossSigning.signable([
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!
]),
true);
expect(
client.encryption!.crossSigning.signable([
client
.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
]),
false);
});
test('sign', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.crossSigning.sign([
client.userDeviceKeys[client.userID!]!.masterKey!,
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!,
client.userDeviceKeys['@othertest:fakeServer.notExisting']!.masterKey!
]);
final body = json.decode(FakeMatrixApi
.calledEndpoints['/client/r0/keys/signatures/upload']!.first);
expect(body['@test:fakeServer.notExisting']?.containsKey('OTHERDEVICE'),
true);
expect(
body['@test:fakeServer.notExisting'].containsKey(
client.userDeviceKeys[client.userID]!.masterKey!.publicKey),
true);
expect(
body['@othertest:fakeServer.notExisting'].containsKey(client
.userDeviceKeys['@othertest:fakeServer.notExisting']
?.masterKey
?.publicKey),
true);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,107 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
void main() {
group('Encrypt/Decrypt room message', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
final roomId = '!726s6s6q:example.com';
late Room room;
late Map<String, dynamic> payload;
final now = DateTime.now();
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
room = client.getRoomById(roomId)!;
});
test('encrypt payload', () async {
if (!olmEnabled) return;
payload = await client.encryption!.encryptGroupMessagePayload(roomId, {
'msgtype': 'm.text',
'text': 'Hello foxies!',
});
expect(payload['algorithm'], AlgorithmTypes.megolmV1AesSha2);
expect(payload['ciphertext'] is String, true);
expect(payload['device_id'], client.deviceID);
expect(payload['sender_key'], client.identityKey);
expect(payload['session_id'] is String, true);
});
test('decrypt payload', () async {
if (!olmEnabled) return;
final encryptedEvent = Event(
type: EventTypes.Encrypted,
content: payload,
room: room,
originServerTs: now,
eventId: '\$event',
senderId: client.userID!,
);
final decryptedEvent =
await client.encryption!.decryptRoomEvent(roomId, encryptedEvent);
expect(decryptedEvent.type, 'm.room.message');
expect(decryptedEvent.content['msgtype'], 'm.text');
expect(decryptedEvent.content['text'], 'Hello foxies!');
});
test('decrypt payload nocache', () async {
if (!olmEnabled) return;
client.encryption!.keyManager.clearInboundGroupSessions();
final encryptedEvent = Event(
type: EventTypes.Encrypted,
content: payload,
room: room,
originServerTs: now,
eventId: '\$event',
senderId: '@alice:example.com',
);
final decryptedEvent =
await client.encryption!.decryptRoomEvent(roomId, encryptedEvent);
expect(decryptedEvent.type, 'm.room.message');
expect(decryptedEvent.content['msgtype'], 'm.text');
expect(decryptedEvent.content['text'], 'Hello foxies!');
await client.encryption!
.decryptRoomEvent(roomId, encryptedEvent, store: true);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,124 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_database.dart';
import '../fake_matrix_api.dart';
void main() {
// key @othertest:fakeServer.notExisting
const otherPickledOlmAccount =
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
group('Encrypt/Decrypt to-device messages', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
final otherClient = Client('othertestclient',
httpClient: FakeMatrixApi(), databaseBuilder: getDatabase);
late DeviceKeys device;
late Map<String, dynamic> payload;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
await client.abortSync();
await otherClient.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
await otherClient.init(
newToken: 'abc',
newUserID: '@othertest:fakeServer.notExisting',
newHomeserver: otherClient.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'FOXDEVICE',
newOlmAccount: otherPickledOlmAccount,
);
await otherClient.abortSync();
await Future.delayed(Duration(milliseconds: 10));
device = DeviceKeys.fromJson({
'user_id': client.userID,
'device_id': client.deviceID,
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:${client.deviceID}': client.identityKey,
'ed25519:${client.deviceID}': client.fingerprintKey,
},
}, client);
});
test('encryptToDeviceMessage', () async {
if (!olmEnabled) return;
payload = await otherClient.encryption!
.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'});
});
test('decryptToDeviceEvent', () async {
if (!olmEnabled) return;
final encryptedEvent = ToDeviceEvent(
sender: '@othertest:fakeServer.notExisting',
type: EventTypes.Encrypted,
content: payload[client.userID][client.deviceID],
);
final decryptedEvent =
await client.encryption!.decryptToDeviceEvent(encryptedEvent);
expect(decryptedEvent.type, 'm.to_device');
expect(decryptedEvent.content['hello'], 'foxies');
});
test('decryptToDeviceEvent nocache', () async {
if (!olmEnabled) return;
client.encryption!.olmManager.olmSessions.clear();
payload = await otherClient.encryption!.encryptToDeviceMessage(
[device], 'm.to_device', {'hello': 'superfoxies'});
final encryptedEvent = ToDeviceEvent(
sender: '@othertest:fakeServer.notExisting',
type: EventTypes.Encrypted,
content: payload[client.userID][client.deviceID],
);
final decryptedEvent =
await client.encryption!.decryptToDeviceEvent(encryptedEvent);
expect(decryptedEvent.type, 'm.to_device');
expect(decryptedEvent.content['hello'], 'superfoxies');
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
await otherClient.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,577 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:convert';
import 'package:matrix/matrix.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('Key Manager', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('handle new m.room_key', () async {
if (!olmEnabled) return;
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
final sessionKey =
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw';
client.encryption!.keyManager.clearInboundGroupSessions();
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
},
encryptedContent: {
'sender_key': validSenderKey,
});
await client.encryption!.keyManager.handleToDeviceEvent(event);
expect(
client.encryption!.keyManager.getInboundGroupSession(
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
null,
true);
// now test a few invalid scenarios
// not encrypted
client.encryption!.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
});
await client.encryption!.keyManager.handleToDeviceEvent(event);
expect(
client.encryption!.keyManager.getInboundGroupSession(
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
null,
false);
});
test('outbound group session', () async {
if (!olmEnabled) return;
final roomId = '!726s6s6q:example.com';
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
var sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
var inbound = client.encryption!.keyManager.getInboundGroupSession(
roomId, sess.outboundGroupSession!.session_id(), client.identityKey);
expect(inbound != null, true);
expect(
inbound!.allowedAtIndex['@alice:example.com']
?['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
?['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
0);
// rotate after too many messages
Iterable.generate(300).forEach((_) {
sess.outboundGroupSession!.encrypt('some string');
});
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
// rotate if device is blocked
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
.blocked = true;
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
.blocked = false;
// lazy-create if it would rotate
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
final oldSessKey = sess.outboundGroupSession!.session_key();
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
.blocked = true;
await client.encryption!.keyManager.prepareOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
expect(
client.encryption!.keyManager
.getOutboundGroupSession(roomId)!
.outboundGroupSession!
.session_key() !=
oldSessKey,
true);
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
.blocked = false;
// rotate if too far in the past
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
sess.creationTime = DateTime.now().subtract(Duration(days: 30));
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
// rotate if user leaves
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
final room = client.getRoomById(roomId)!;
final member = room.getState('m.room.member', '@alice:example.com');
member!.content['membership'] = 'leave';
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! - 1;
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
member.content['membership'] = 'join';
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! + 1;
// do not rotate if new device is added
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
sess.outboundGroupSession!.encrypt(
'foxies'); // so that the new device will have a different index
client.userDeviceKeys['@alice:example.com']?.deviceKeys['NEWDEVICE'] =
DeviceKeys.fromJson({
'user_id': '@alice:example.com',
'device_id': 'NEWDEVICE',
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
],
'keys': {
'curve25519:NEWDEVICE': 'bnKQp6pPW0l9cGoIgHpBoK5OUi4h0gylJ7upc4asFV8',
'ed25519:NEWDEVICE': 'ZZhPdvWYg3MRpGy2MwtI+4MHXe74wPkBli5hiEOUi8Y'
},
'signatures': {
'@alice:example.com': {
'ed25519:NEWDEVICE':
'94GSg8N9vNB8wyWHJtKaaX3MGNWPVOjBatJM+TijY6B1RlDFJT5Cl1h/tjr17AoQz0CDdOf6uFhrYsBkH1/ABg'
}
}
}, client);
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
inbound = client.encryption!.keyManager.getInboundGroupSession(
roomId, sess.outboundGroupSession!.session_id(), client.identityKey);
expect(
inbound!.allowedAtIndex['@alice:example.com']
?['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
?['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
0);
expect(
inbound.allowedAtIndex['@alice:example.com']
?['bnKQp6pPW0l9cGoIgHpBoK5OUi4h0gylJ7upc4asFV8'],
1);
// do not rotate if new user is added
member.content['membership'] = 'leave';
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! - 1;
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
member.content['membership'] = 'join';
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! + 1;
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
// force wipe
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
await client.encryption!.keyManager
.clearOrUseOutboundGroupSession(roomId, wipe: true);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
// load from database
sess = await client.encryption!.keyManager
.createOutboundGroupSession(roomId);
client.encryption!.keyManager.clearOutboundGroupSessions();
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
false);
await client.encryption!.keyManager.loadOutboundGroupSession(roomId);
expect(
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
true);
});
test('inbound group session', () async {
if (!olmEnabled) return;
final roomId = '!726s6s6q:example.com';
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
final sessionContent = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
'session_key':
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
};
client.encryption!.keyManager.clearInboundGroupSessions();
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
false);
client.encryption!.keyManager
.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent);
await Future.delayed(Duration(milliseconds: 10));
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
null,
false);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
expect(
client.encryption!.keyManager
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
null,
true);
expect(
client.encryption!.keyManager
.getInboundGroupSession('otherroom', sessionId, 'invalid') !=
null,
false);
expect(
client.encryption!.keyManager
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
null,
false);
client.encryption!.keyManager.clearInboundGroupSessions();
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
false);
await client.encryption!.keyManager
.loadInboundGroupSession(roomId, sessionId, senderKey);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
client.encryption!.keyManager.clearInboundGroupSessions();
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
false);
await client.encryption!.keyManager
.loadInboundGroupSession(roomId, sessionId, 'invalid');
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
null,
false);
});
test('setInboundGroupSession', () async {
if (!olmEnabled) return;
final session = olm.OutboundGroupSession();
session.create();
final inbound = olm.InboundGroupSession();
inbound.create(session.session_key());
final senderKey = client.identityKey;
final roomId = '!someroom:example.org';
final sessionId = inbound.session_id();
final room = Room(id: roomId, client: client);
client.rooms.add(room);
// we build up an encrypted message so that we can test if it successfully decrypted afterwards
room.setState(
Event(
senderId: '@test:example.com',
type: 'm.room.encrypted',
room: room,
eventId: '12345',
originServerTs: DateTime.now(),
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'ciphertext': session.encrypt(json.encode({
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'foxies'},
})),
'device_id': client.deviceID,
'sender_key': client.identityKey,
'session_id': sessionId,
},
stateKey: '',
),
);
expect(room.lastEvent?.type, 'm.room.encrypted');
// set a payload...
var sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey],
'session_id': sessionId,
'session_key': inbound.export_session(1),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.inboundGroupSession
?.first_known_index(),
1);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.forwardingCurve25519KeyChain
.length,
1);
// not set one with a higher first known index
sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey],
'session_id': sessionId,
'session_key': inbound.export_session(2),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.inboundGroupSession
?.first_known_index(),
1);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.forwardingCurve25519KeyChain
.length,
1);
// set one with a lower first known index
sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey],
'session_id': sessionId,
'session_key': inbound.export_session(0),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.inboundGroupSession
?.first_known_index(),
0);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.forwardingCurve25519KeyChain
.length,
1);
// not set one with a longer forwarding chain
sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey, 'beep'],
'session_id': sessionId,
'session_key': inbound.export_session(0),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.inboundGroupSession
?.first_known_index(),
0);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.forwardingCurve25519KeyChain
.length,
1);
// set one with a shorter forwarding chain
sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [],
'session_id': sessionId,
'session_key': inbound.export_session(0),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
client.encryption!.keyManager.setInboundGroupSession(
roomId, sessionId, senderKey, sessionPayload,
forwarded: true);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.inboundGroupSession
?.first_known_index(),
0);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey)
?.forwardingCurve25519KeyChain
.length,
0);
// test that it decrypted the last event
expect(room.lastEvent?.type, 'm.room.message');
expect(room.lastEvent?.content['body'], 'foxies');
inbound.free();
session.free();
});
test('Reused deviceID attack', () async {
if (!olmEnabled) return;
Logs().level = Level.warning;
// Ensure the device came from sync
expect(
client.userDeviceKeys['@alice:example.com']
?.deviceKeys['JLAFKJWSCS'] !=
null,
true);
// Alice removes her device
client.userDeviceKeys['@alice:example.com']?.deviceKeys
.remove('JLAFKJWSCS');
// Alice adds her device with same device ID but different keys
final oldResp = FakeMatrixApi.api['POST']?['/client/r0/keys/query'](null);
FakeMatrixApi.api['POST']?['/client/r0/keys/query'] = (_) {
oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = {
'user_id': '@alice:example.com',
'device_id': 'JLAFKJWSCS',
'algorithms': [
'm.olm.v1.curve25519-aes-sha2',
'm.megolm.v1.aes-sha2'
],
'keys': {
'curve25519:JLAFKJWSCS':
'WbwrNyD7nvtmcLQ0TTuVPFGJq6JznfjrVsjIpmBqvDw',
'ed25519:JLAFKJWSCS': 'vl0d54pTVRcvBgUzoQFa8e6TldHWG9O8bh0iuIvgd/I'
},
'signatures': {
'@alice:example.com': {
'ed25519:JLAFKJWSCS':
's/L86jLa8BTroL8GsBeqO0gRLC3ZrSA7Gch6UoLI2SefC1+1ycmnP9UGbLPh3qBJOmlhczMpBLZwelg87qNNDA'
}
}
};
return oldResp;
};
client.userDeviceKeys['@alice:example.com']!.outdated = true;
await client.updateUserDeviceKeys();
expect(
client.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS'],
null);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: false);
});
});
}

View file

@ -0,0 +1,404 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:convert';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_matrix_api.dart';
Map<String, dynamic> jsonDecode(dynamic payload) {
if (payload is String) {
try {
return json.decode(payload);
} catch (e) {
return {};
}
}
if (payload is Map<String, dynamic>) return payload;
return {};
}
void main() {
/// All Tests related to device keys
group('Key Request', () {
Logs().level = Level.error;
var olmEnabled = true;
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
test('Create Request', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
final matrix = await getClient();
final requestRoom = matrix.getRoomById('!726s6s6q:example.com')!;
await matrix.encryption!.keyManager.request(
requestRoom, 'sessionId', validSenderKey,
tryOnlineBackup: false);
var foundEvent = false;
for (final entry in FakeMatrixApi.calledEndpoints.entries) {
final payload = jsonDecode(entry.value.first);
if (entry.key
.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
(payload['messages'] is Map) &&
(payload['messages']['@alice:example.com'] is Map) &&
(payload['messages']['@alice:example.com']['*'] is Map)) {
final content = payload['messages']['@alice:example.com']['*'];
if (content['action'] == 'request' &&
content['body']['room_id'] == '!726s6s6q:example.com' &&
content['body']['sender_key'] == validSenderKey &&
content['body']['session_id'] == 'sessionId') {
foundEvent = true;
break;
}
}
}
expect(foundEvent, true);
await matrix.dispose(closeDatabase: true);
});
test('Reply To Request', () async {
if (!olmEnabled) return;
final matrix = await getClient();
matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
FakeMatrixApi.calledEndpoints.clear();
await matrix
.userDeviceKeys['@alice:example.com']!.deviceKeys['OTHERDEVICE']!
.setBlocked(false);
await matrix
.userDeviceKeys['@alice:example.com']!.deviceKeys['OTHERDEVICE']!
.setVerified(true);
final session = await matrix.encryption!.keyManager
.loadInboundGroupSession(
'!726s6s6q:example.com', validSessionId, validSenderKey);
// test a successful share
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_1',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// test a successful foreign share
FakeMatrixApi.calledEndpoints.clear();
session!.allowedAtIndex['@test:fakeServer.notExisting'] = <String, int>{
matrix.userDeviceKeys['@test:fakeServer.notExisting']!
.deviceKeys['OTHERDEVICE']!.curve25519Key!: 0,
};
event = ToDeviceEvent(
sender: '@test:fakeServer.notExisting',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_a1',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
session.allowedAtIndex.remove('@test:fakeServer.notExisting');
// test various fail scenarios
// unknown person
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@test:fakeServer.notExisting',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_a2',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// no body
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'request_id': 'request_2',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// request by ourself
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_3',
'requesting_device_id': 'JLAFKJWSCS',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not found
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_4',
'requesting_device_id': 'blubb',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknown room
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!invalid:example.com',
'sender_key': validSenderKey,
'session_id': validSessionId,
},
'request_id': 'request_5',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// unknwon session
FakeMatrixApi.calledEndpoints.clear();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.room_key_request',
content: {
'action': 'request',
'body': {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'sender_key': validSenderKey,
'session_id': 'invalid',
},
'request_id': 'request_6',
'requesting_device_id': 'OTHERDEVICE',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
FakeMatrixApi.calledEndpoints.clear();
await matrix.dispose(closeDatabase: true);
});
test('Receive shared keys', () async {
if (!olmEnabled) return;
final matrix = await getClient();
final requestRoom = matrix.getRoomById('!726s6s6q:example.com')!;
await matrix.encryption!.keyManager.request(
requestRoom, validSessionId, validSenderKey,
tryOnlineBackup: false);
final session = (await matrix.encryption!.keyManager
.loadInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey))!;
final sessionKey = session.inboundGroupSession!
.export_session(session.inboundGroupSession!.first_known_index());
matrix.encryption!.keyManager.clearInboundGroupSessions();
var event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
'sender_claimed_ed25519_key':
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
},
encryptedContent: {
'sender_key': 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption!.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
true);
// now test a few invalid scenarios
// request not found
matrix.encryption!.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
'sender_claimed_ed25519_key':
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
},
encryptedContent: {
'sender_key': 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption!.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
// unknown device
await matrix.encryption!.keyManager.request(
requestRoom, validSessionId, validSenderKey,
tryOnlineBackup: false);
matrix.encryption!.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
'sender_claimed_ed25519_key':
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
},
encryptedContent: {
'sender_key': 'invalid',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption!.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
// no encrypted content
await matrix.encryption!.keyManager.request(
requestRoom, validSessionId, validSenderKey,
tryOnlineBackup: false);
matrix.encryption!.keyManager.clearInboundGroupSessions();
event = ToDeviceEvent(
sender: '@alice:example.com',
type: 'm.forwarded_room_key',
content: {
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': '!726s6s6q:example.com',
'session_id': validSessionId,
'session_key': sessionKey,
'sender_key': validSenderKey,
'forwarding_curve25519_key_chain': [],
'sender_claimed_ed25519_key':
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
});
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
expect(
matrix.encryption!.keyManager.getInboundGroupSession(
requestRoom.id, validSessionId, validSenderKey) !=
null,
false);
// There is a non awaiting setInboundGroupSession call on the database
await Future.delayed(Duration(seconds: 1));
await matrix.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,486 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:convert';
import 'package:matrix/matrix.dart';
import 'package:matrix/encryption.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_database.dart';
import '../fake_matrix_api.dart';
class MockSSSS extends SSSS {
MockSSSS(Encryption encryption) : super(encryption);
bool requestedSecrets = false;
@override
Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
requestedSecrets = true;
final handle = open();
await handle.unlock(recoveryKey: ssssKey);
await handle.maybeCacheAll();
}
}
EventUpdate getLastSentEvent(KeyVerification req) {
final entry = FakeMatrixApi.calledEndpoints.entries
.firstWhere((p) => p.key.contains('/send/'));
final type = entry.key.split('/')[6];
final content = json.decode(entry.value.first);
return EventUpdate(
content: {
'event_id': req.transactionId,
'type': type,
'content': content,
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'sender': req.client.userID,
},
type: EventUpdateType.timeline,
roomID: req.room!.id,
);
}
void main() {
/// All Tests related to the ChatTime
group('Key Verification', () {
Logs().level = Level.error;
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();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client1 = await getClient();
client2 = Client(
'othertestclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
await client2.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
await client2.init(
newToken: 'abc',
newUserID: '@othertest:fakeServer.notExisting',
newHomeserver: client2.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'FOXDEVICE',
newOlmAccount: otherPickledOlmAccount,
);
await Future.delayed(Duration(milliseconds: 10));
client1.verificationMethods = {
KeyVerificationMethod.emoji,
KeyVerificationMethod.numbers
};
client2.verificationMethods = {
KeyVerificationMethod.emoji,
KeyVerificationMethod.numbers
};
});
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();
// 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,
);
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
expect(
client2.encryption!.keyVerificationManager
.getRequest(req2.transactionId!),
req2);
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
// compare emoji
expect(req1.state, KeyVerificationState.askSas);
expect(req2.state, KeyVerificationState.askSas);
expect(req1.sasTypes[0], 'emoji');
expect(req1.sasTypes[1], 'decimal');
expect(req2.sasTypes[0], 'emoji');
expect(req2.sasTypes[1], 'decimal');
// compare emoji
final emoji1 = req1.sasEmojis;
final emoji2 = req2.sasEmojis;
for (var i = 0; i < 7; i++) {
expect(emoji1[i].emoji, emoji2[i].emoji);
expect(emoji1[i].name, emoji2[i].name);
}
// compare numbers
final numbers1 = req1.sasNumbers;
final numbers2 = req2.sasNumbers;
for (var i = 0; i < 3; i++) {
expect(numbers1[i], numbers2[i]);
}
// alright, they match
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req1.acceptSas();
evt = getLastSentEvent(req1);
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.waitingSas);
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptSas();
evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.done);
expect(req2.state, KeyVerificationState.done);
expect(
client1.userDeviceKeys[client2.userID]?.deviceKeys[client2.deviceID]
?.directVerified,
true);
expect(
client2.userDeviceKeys[client1.userID]?.deviceKeys[client1.deviceID]
?.directVerified,
true);
await client1.encryption!.keyVerificationManager.cleanup();
await client2.encryption!.keyVerificationManager.cleanup();
});
test('ask SSSS start', () async {
if (!olmEnabled) return;
client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(true);
await client1.encryption!.ssss.clearCache();
final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false);
expect(req1.state, KeyVerificationState.askSSSS);
await req1.openSSSS(recoveryKey: ssssKey);
await Future.delayed(Duration(seconds: 1));
expect(req1.state, KeyVerificationState.waitingAccept);
await req1.cancel();
await client1.encryption!.keyVerificationManager.cleanup();
});
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!
.setDirectVerified(false);
// the other one has to have their master key verified to trigger asking for ssss
client2.userDeviceKeys[client2.userID]!.masterKey!
.setDirectVerified(true);
final req1 = await client1.userDeviceKeys[client2.userID]!
.startVerification(newDirectChatEnableEncryption: false);
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
// compare emoji
expect(req1.state, KeyVerificationState.askSas);
expect(req2.state, KeyVerificationState.askSas);
// compare emoji
final emoji1 = req1.sasEmojis;
final emoji2 = req2.sasEmojis;
for (var i = 0; i < 7; i++) {
expect(emoji1[i].emoji, emoji2[i].emoji);
expect(emoji1[i].name, emoji2[i].name);
}
// compare numbers
final numbers1 = req1.sasNumbers;
final numbers2 = req2.sasNumbers;
for (var i = 0; i < 3; i++) {
expect(numbers1[i], numbers2[i]);
}
// alright, they match
client1.userDeviceKeys[client1.userID]!.masterKey!
.setDirectVerified(true);
await client1.encryption!.ssss.clearCache();
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req1.acceptSas();
evt = getLastSentEvent(req1);
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.waitingSas);
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptSas();
evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
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!);
(client1.encryption!.ssss as MockSSSS).requestedSecrets = false;
await client1.encryption!.ssss.clearCache();
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);
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
FakeMatrixApi.calledEndpoints.clear();
await req2.rejectVerification();
evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error);
expect(req2.state, KeyVerificationState.error);
await client1.encryption!.keyVerificationManager.cleanup();
await client2.encryption!.keyVerificationManager.cleanup();
});
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);
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
await req1.acceptSas();
FakeMatrixApi.calledEndpoints.clear();
await req2.rejectSas();
evt = getLastSentEvent(req2);
await client1.encryption!.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error);
expect(req2.state, KeyVerificationState.error);
await client1.encryption!.keyVerificationManager.cleanup();
await client2.encryption!.keyVerificationManager.cleanup();
});
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);
final evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
late KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption!.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
await client2.encryption!.keyVerificationManager
.handleEventUpdate(EventUpdate(
content: {
'event_id': req2.transactionId,
'type': 'm.key.verification.ready',
'content': {
'methods': ['m.sas.v1'],
'from_device': 'SOMEOTHERDEVICE',
'm.relates_to': {
'rel_type': 'm.reference',
'event_id': req2.transactionId,
},
},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'sender': client2.userID,
},
type: EventUpdateType.timeline,
roomID: req2.room!.id,
));
expect(req2.state, KeyVerificationState.error);
await req2.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);
});
});
}

View file

@ -0,0 +1,264 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2020, 2021 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:convert';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'package:matrix/encryption/utils/json_signature_check_extension.dart';
import '../fake_client.dart';
import '../fake_matrix_api.dart';
void main() {
group('Olm Manager', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('signatures', () async {
if (!olmEnabled) return;
final payload = <String, dynamic>{
'fox': 'floof',
};
final signedPayload = client.encryption!.olmManager.signJson(payload);
expect(
signedPayload.checkJsonSignature(
client.fingerprintKey, client.userID!, client.deviceID!),
true);
});
test('uploadKeys', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear();
final res = await client.encryption!.olmManager
.uploadKeys(uploadDeviceKeys: true);
expect(res, true);
var sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
expect(sent['device_keys'] != null, true);
expect(sent['one_time_keys'] != null, true);
expect(sent['one_time_keys'].keys.length, 66);
expect(sent['fallback_keys'] != null, true);
expect(sent['fallback_keys'].keys.length, 1);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.olmManager.uploadKeys();
sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
expect(sent['device_keys'] != null, false);
expect(sent['fallback_keys'].keys.length, 1);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.olmManager
.uploadKeys(oldKeyCount: 20, unusedFallbackKey: true);
sent = json.decode(
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
expect(sent['one_time_keys'].keys.length, 46);
expect(sent['fallback_keys'].keys.length, 0);
});
test('handleDeviceOneTimeKeysCount', () async {
if (!olmEnabled) return;
FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 20}, null);
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
true);
FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager
.handleDeviceOneTimeKeysCount({'signed_curve25519': 70}, null);
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
false);
FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager.handleDeviceOneTimeKeysCount(null, []);
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
true);
// this will upload keys because we assume the key count is 0, if the server doesn't send one
FakeMatrixApi.calledEndpoints.clear();
client.encryption!.olmManager
.handleDeviceOneTimeKeysCount(null, ['signed_curve25519']);
await Future.delayed(Duration(milliseconds: 50));
expect(
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
true);
});
test('restoreOlmSession', () async {
if (!olmEnabled) return;
client.encryption!.olmManager.olmSessions.clear();
await client.encryption!.olmManager
.restoreOlmSession(client.userID!, client.identityKey);
expect(client.encryption!.olmManager.olmSessions.length, 1);
client.encryption!.olmManager.olmSessions.clear();
await client.encryption!.olmManager
.restoreOlmSession(client.userID!, 'invalid');
expect(client.encryption!.olmManager.olmSessions.length, 0);
client.encryption!.olmManager.olmSessions.clear();
await client.encryption!.olmManager
.restoreOlmSession('invalid', client.identityKey);
expect(client.encryption!.olmManager.olmSessions.length, 0);
});
test('startOutgoingOlmSessions', () async {
if (!olmEnabled) return;
// start an olm session.....with ourself!
client.encryption!.olmManager.olmSessions.clear();
await client.encryption!.olmManager.startOutgoingOlmSessions([
client.userDeviceKeys[client.userID!]!.deviceKeys[client.deviceID]!
]);
expect(
client.encryption!.olmManager.olmSessions
.containsKey(client.identityKey),
true);
});
test('replay to_device events', () async {
if (!olmEnabled) return;
final userId = '@alice:example.com';
final deviceId = 'JLAFKJWSCS';
final senderKey = 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8';
FakeMatrixApi.calledEndpoints.clear();
await client.database!.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
userId,
deviceId);
var event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: {
'sender_key': senderKey,
},
);
await client.encryption!.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// fail scenarios
// not encrypted
FakeMatrixApi.calledEndpoints.clear();
await client.database!.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
userId,
deviceId);
event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: null,
);
await client.encryption!.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not found
FakeMatrixApi.calledEndpoints.clear();
await client.database!.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
userId,
deviceId);
event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: {
'sender_key': 'invalid',
},
);
await client.encryption!.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// don't replay if the last event is m.dummy itself
FakeMatrixApi.calledEndpoints.clear();
await client.database!.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.dummy',
'content': {},
}),
userId,
deviceId);
event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: {
'sender_key': senderKey,
},
);
await client.encryption!.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,124 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:convert';
import 'package:matrix/matrix.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('Online Key Backup', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
final roomId = '!726s6s6q:example.com';
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('basic things', () async {
if (!olmEnabled) return;
expect(client.encryption!.keyManager.enabled, true);
expect(await client.encryption!.keyManager.isCached(), false);
final handle = client.encryption!.ssss.open();
await handle.unlock(recoveryKey: ssssKey);
await handle.maybeCacheAll();
expect(await client.encryption!.keyManager.isCached(), true);
});
test('load key', () async {
if (!olmEnabled) return;
client.encryption!.keyManager.clearInboundGroupSessions();
await client.encryption!.keyManager
.request(client.getRoomById(roomId)!, sessionId, senderKey);
expect(
client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey) !=
null,
true);
});
test('upload key', () async {
if (!olmEnabled) return;
final session = olm.OutboundGroupSession();
session.create();
final inbound = olm.InboundGroupSession();
inbound.create(session.session_key());
final senderKey = client.identityKey;
final roomId = '!someroom:example.org';
final sessionId = inbound.session_id();
// set a payload...
final sessionPayload = <String, dynamic>{
'algorithm': AlgorithmTypes.megolmV1AesSha2,
'room_id': roomId,
'forwarding_curve25519_key_chain': [client.identityKey],
'session_id': sessionId,
'session_key': inbound.export_session(1),
'sender_key': senderKey,
'sender_claimed_ed25519_key': client.fingerprintKey,
};
FakeMatrixApi.calledEndpoints.clear();
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();
final payload = FakeMatrixApi
.calledEndpoints['/client/unstable/room_keys/keys?version=5']!.first;
dbSessions = await client.database!.getInboundGroupSessionsToUpload();
expect(dbSessions.isEmpty, true);
final onlineKeys = RoomKeys.fromJson(json.decode(payload));
client.encryption!.keyManager.clearInboundGroupSessions();
var ret = client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey);
expect(ret, null);
await client.encryption!.keyManager.loadFromResponse(onlineKeys);
ret = client.encryption!.keyManager
.getInboundGroupSession(roomId, sessionId, senderKey);
expect(ret != null, true);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: false);
});
});
}

View file

@ -0,0 +1,501 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:typed_data';
import 'dart:convert';
import 'dart:math';
import 'package:matrix/matrix.dart';
import 'package:matrix/encryption.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import '../fake_client.dart';
import '../fake_matrix_api.dart';
Uint8List secureRandomBytes(int len) {
final rng = Random.secure();
final list = Uint8List(len);
list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256)));
return list;
}
class MockSSSS extends SSSS {
MockSSSS(Encryption encryption) : super(encryption);
bool requestedSecrets = false;
@override
Future<void> maybeRequestAll([List<DeviceKeys>? devices]) async {
requestedSecrets = true;
final handle = open();
await handle.unlock(recoveryKey: ssssKey);
await handle.maybeCacheAll();
}
}
void main() {
group('SSSS', () {
Logs().level = Level.error;
var olmEnabled = true;
late Client client;
test('setupClient', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
if (!olmEnabled) return;
client = await getClient();
});
test('basic things', () async {
if (!olmEnabled) return;
expect(client.encryption!.ssss.defaultKeyId,
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3');
});
test('encrypt / decrypt', () async {
if (!olmEnabled) return;
final key = Uint8List.fromList(secureRandomBytes(32));
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
final dec = await SSSS.decryptAes(enc, key, 'name');
expect(dec, 'secret foxies');
});
test('store', () async {
if (!olmEnabled) return;
final handle = client.encryption!.ssss.open();
var failed = false;
try {
await handle.unlock(passphrase: 'invalid');
} catch (_) {
failed = true;
}
expect(failed, true);
expect(handle.isUnlocked, false);
failed = false;
try {
await handle.unlock(recoveryKey: 'invalid');
} catch (_) {
failed = true;
}
expect(failed, true);
expect(handle.isUnlocked, false);
await handle.unlock(passphrase: ssssPassphrase);
await handle.unlock(recoveryKey: ssssKey);
expect(handle.isUnlocked, true);
FakeMatrixApi.calledEndpoints.clear();
await handle.store('best animal', 'foxies');
// alright, since we don't properly sync we will manually have to update
// account_data for this test
final content = FakeMatrixApi
.calledEndpoints[
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
.first;
client.accountData['best animal'] = BasicEvent.fromJson({
'type': 'best animal',
'content': json.decode(content),
});
expect(await handle.getStored('best animal'), 'foxies');
});
test('encode / decode recovery key', () async {
if (!olmEnabled) return;
final key = Uint8List.fromList(secureRandomBytes(32));
final encoded = SSSS.encodeRecoveryKey(key);
var decoded = SSSS.decodeRecoveryKey(encoded);
expect(key, decoded);
decoded = SSSS.decodeRecoveryKey(encoded + ' \n\t');
expect(key, decoded);
final handle = client.encryption!.ssss.open();
await handle.unlock(recoveryKey: ssssKey);
expect(handle.recoveryKey, ssssKey);
});
test('cache', () async {
if (!olmEnabled) return;
await client.encryption!.ssss.clearCache();
final handle =
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningSelfSigning)) !=
null,
false);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
false);
await handle.getStored(EventTypes.CrossSigningSelfSigning);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningSelfSigning)) !=
null,
true);
await handle.maybeCacheAll();
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
true);
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true);
});
test('postUnlock', () async {
if (!olmEnabled) return;
await client.encryption!.ssss.clearCache();
client.userDeviceKeys[client.userID!]!.masterKey!
.setDirectVerified(false);
final handle =
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
await handle.unlock(recoveryKey: ssssKey);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningSelfSigning)) !=
null,
true);
expect(
(await client.encryption!.ssss
.getCached(EventTypes.CrossSigningUserSigning)) !=
null,
true);
expect(
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
null,
true);
expect(client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
true);
});
test('make share requests', () async {
if (!olmEnabled) return;
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(true);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.request('some.type', [key]);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
});
test('answer to share requests', () async {
if (!olmEnabled) return;
var event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// now test some fail scenarios
// not by us
event = ToDeviceEvent(
sender: '@someotheruser:example.org',
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// secret not cached
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': 'm.unknown.secret',
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// is a cancelation
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request_cancellation',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not verified
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(false);
client.userDeviceKeys[client.userID!]!.masterKey!
.setDirectVerified(false);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.request',
content: {
'action': 'request',
'requesting_device_id': 'OTHERDEVICE',
'name': EventTypes.CrossSigningSelfSigning,
'request_id': '1',
},
);
FakeMatrixApi.calledEndpoints.clear();
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
key.setDirectVerified(true);
});
test('receive share requests', () async {
if (!olmEnabled) return;
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(true);
final handle =
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
await handle.unlock(recoveryKey: ssssKey);
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
var event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), 'foxies!');
// test the different validators
for (final type in [
EventTypes.CrossSigningSelfSigning,
EventTypes.CrossSigningUserSigning,
EventTypes.MegolmBackup
]) {
final secret = await handle.getStored(type);
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request(type, [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id':
client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': secret,
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached(type), secret);
}
// test different fail scenarios
// not encrypted
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// unknown request id
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': 'invalid',
'secret': 'foxies!',
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// not from a device we sent the request to
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
'sender_key': 'invalid',
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// secret not a string
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request('best animal', [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 42,
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached('best animal'), null);
// validator doesn't check out
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
event = ToDeviceEvent(
sender: client.userID!,
type: 'm.secret.send',
content: {
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
'secret': 'foxies!',
},
encryptedContent: {
'sender_key': key.curve25519Key,
},
);
await client.encryption!.ssss.handleToDeviceEvent(event);
expect(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup),
null);
});
test('request all', () async {
if (!olmEnabled) return;
final key =
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
key.setDirectVerified(true);
await client.encryption!.ssss.clearCache();
client.encryption!.ssss.pendingShareRequests.clear();
await client.encryption!.ssss.maybeRequestAll([key]);
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
});
test('periodicallyRequestMissingCache', () async {
if (!olmEnabled) return;
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true);
client.encryption!.ssss = MockSSSS(client.encryption!);
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
await client.encryption!.ssss.periodicallyRequestMissingCache();
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true);
// it should only retry once every 15 min
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
await client.encryption!.ssss.periodicallyRequestMissingCache();
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
});
test('createKey', () async {
if (!olmEnabled) return;
// with passphrase
var newKey = await client.encryption!.ssss.createKey('test');
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
var testKey = client.encryption!.ssss.open(newKey.keyId);
await testKey.unlock(passphrase: 'test');
await testKey.setPrivateKey(newKey.privateKey!);
// without passphrase
newKey = await client.encryption!.ssss.createKey();
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
testKey = client.encryption!.ssss.open(newKey.keyId);
await testKey.setPrivateKey(newKey.privateKey!);
});
test('dispose client', () async {
if (!olmEnabled) return;
await client.dispose(closeDatabase: true);
});
});
}

View file

@ -0,0 +1,76 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2022 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:convert';
import 'package:matrix/encryption/utils/base64_unpadded.dart';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
void main() {
group('Utils', () {
const base64input = 'foobar';
final utf8codec = Utf8Codec();
test('base64 padded', () {
final paddedBase64 = base64.encode(base64input.codeUnits);
final decodedPadded =
utf8codec.decode(base64decodeUnpadded(paddedBase64));
expect(decodedPadded, base64input, reason: 'Padded base64 decode');
});
test('base64 unpadded', () {
const unpaddedBase64 = 'Zm9vYmFy';
final decodedUnpadded =
utf8codec.decode(base64decodeUnpadded(unpaddedBase64));
expect(decodedUnpadded, base64input, reason: 'Unpadded base64 decode');
});
});
group('MatrixFile', () {
test('MatrixImageFile', () async {
const base64Image =
'iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC';
final data = base64Decode(base64Image);
final image = await MatrixImageFile.create(
bytes: data,
name: 'bomb.png',
mimeType: 'image/png',
);
expect(image.width, 220, reason: 'Unexpected image width');
expect(image.height, 220, reason: 'Unexpected image heigth');
expect(image.blurhash, 'L75NyU5krSbx=zAF#kSNZxOZ%4NE',
reason: 'Unexpected image blur');
final thumbnail = await image.generateThumbnail(dimension: 64);
expect(thumbnail!.height, 64, reason: 'Unexpected thumbnail height');
final shrinkedImage = await MatrixImageFile.shrink(
bytes: data,
name: 'bomb.png',
mimeType: 'image/png',
maxDimension: 150);
expect(shrinkedImage.width, 150, reason: 'Unexpected scaled image width');
expect(shrinkedImage.height, 150,
reason: 'Unexpected scaled image heigth');
expect(shrinkedImage.blurhash, 'L75NyU5kvvbx^7AF#kSgZxOZ%5NE',
reason: 'Unexpected scaled image blur');
});
});
}

1625
test/event_test.dart Normal file

File diff suppressed because it is too large Load diff

50
test/fake_client.dart Normal file
View file

@ -0,0 +1,50 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'fake_matrix_api.dart';
import 'fake_database.dart';
const ssssPassphrase = 'nae7ahDiequ7ohniufah3ieS2je1thohX4xeeka7aixohsho9O';
const ssssKey = 'EsT9 RzbW VhPW yqNp cC7j ViiW 5TZB LuY4 ryyv 9guN Ysmr WDPH';
// key @test:fakeServer.notExisting
const pickledOlmAccount =
'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
Future<Client> getClient() async {
final client = Client(
'testclient',
httpClient: FakeMatrixApi(),
databaseBuilder: getDatabase,
);
FakeMatrixApi.client = client;
await client.checkHomeserver('https://fakeServer.notExisting',
checkWellKnown: false);
await client.init(
newToken: 'abcd',
newUserID: '@test:fakeServer.notExisting',
newHomeserver: client.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'GHTYAJCE',
newOlmAccount: pickledOlmAccount,
);
await Future.delayed(Duration(milliseconds: 10));
return client;
}

55
test/fake_database.dart Normal file
View file

@ -0,0 +1,55 @@
/*
* 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:io';
import 'dart:math';
import 'package:matrix/matrix.dart';
import 'package:file/memory.dart';
import 'package:hive/hive.dart';
Future<DatabaseApi> getDatabase(Client? _) => getHiveDatabase(_);
bool hiveInitialized = false;
Future<FluffyBoxDatabase> getFluffyBoxDatabase(Client? c) async {
final fileSystem = MemoryFileSystem();
final testHivePath =
'${fileSystem.path}/build/.test_store/${Random().nextDouble()}';
Directory(testHivePath).createSync(recursive: true);
final db = FluffyBoxDatabase(
'unit_test.${c?.hashCode}',
testHivePath,
);
await db.open();
return db;
}
Future<FamedlySdkHiveDatabase> getHiveDatabase(Client? c) async {
if (!hiveInitialized) {
final fileSystem = MemoryFileSystem();
final testHivePath =
'${fileSystem.path}/build/.test_store/${Random().nextDouble()}';
Directory(testHivePath).createSync(recursive: true);
Hive.init(testHivePath);
hiveInitialized = true;
}
final db = FamedlySdkHiveDatabase('unit_test.${c?.hashCode}');
await db.open();
return db;
}

2277
test/fake_matrix_api.dart Normal file

File diff suppressed because it is too large Load diff

101
test/html_to_text_test.dart Normal file
View file

@ -0,0 +1,101 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2021 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 'package:matrix/src/utils/html_to_text.dart';
import 'package:test/test.dart';
void main() {
group('htmlToText', () {
final testMap = <String, String>{
'': '',
'hello world\nthis is a test': 'hello world\nthis is a test',
'<em>That\'s</em> not a test, <strong>this</strong> is a test':
'*That\'s* not a test, **this** is a test',
'Visit <del><a href="http://example.com">our website</a></del> (outdated)':
'Visit ~~🔗our website~~ (outdated)',
'(cw spiders) <span data-mx-spoiler>spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<span data-mx-spoiler="cw spiders">spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<img src="test.gif" alt="a test case" />': 'a test case',
'List of cute animals:\n<ul>\n<li>Kittens</li>\n<li>Puppies</li>\n<li>Snakes<br/>(I think they\'re cute!)</li>\n</ul>\n(This list is incomplete, you can help by adding to it!)':
'List of cute animals:\n● Kittens\n● Puppies\n● Snakes\n (I think they\'re cute!)\n(This list is incomplete, you can help by adding to it!)',
'<em>fox</em>': '*fox*',
'<i>fox</i>': '*fox*',
'<strong>fox</i>': '**fox**',
'<b>fox</b>': '**fox**',
'<u>fox</u>': '__fox__',
'<ins>fox</ins>': '__fox__',
'<del>fox</del>': '~~fox~~',
'<strike>fox</strike>': '~~fox~~',
'<s>fox</s>': '~~fox~~',
'<code>&gt;fox</code>': '`>fox`',
'<pre>meep</pre>': '```\nmeep\n```',
'<pre>meep\n</pre>': '```\nmeep\n```',
'<pre><code class="language-floof">meep</code></pre>':
'```floof\nmeep\n```',
'before<pre>code</pre>after': 'before\n```\ncode\n```\nafter',
'<p>before</p><pre>code</pre><p>after</p>':
'before\n```\ncode\n```\nafter',
'<p>fox</p>': 'fox',
'<p>fox</p><p>floof</p>': 'fox\n\nfloof',
'<a href="https://example.org">website</a>': '🔗website',
'<a href="https://matrix.to/#/@user:example.org">fox</a>': 'fox',
'<a href="matrix:u/user:example.org">fox</a>': 'fox',
'<img alt=":wave:" src="mxc://fox">': ':wave:',
'fox<br>floof': 'fox\nfloof',
'<blockquote>fox</blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote><p>floof</p>': '> fox\nfloof',
'a<blockquote>fox</blockquote>floof': 'a\n> fox\nfloof',
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
'> > fox\n> floof\nfluff',
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
'● hey\n ○ a\n ○ b\n● foxies',
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
'1. a\n 1. aa\n 2. bb\n2. b',
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
'1. a\n ○ aa\n ○ bb\n2. b',
'<ul><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ul>':
'● a\n 1. aa\n 2. bb\n● b',
'<mx-reply>bunny</mx-reply>fox': 'fox',
'fox<hr>floof': 'fox\n----------\nfloof',
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
'<h1>fox</h1>floof': '# fox\nfloof',
'<h1>fox</h1><p>floof</p>': '# fox\nfloof',
'floof<h1>fox</h1>': 'floof\n# fox',
'<p>floof</p><h1>fox</h1>': 'floof\n# fox',
'<h2>fox</h2>': '## fox',
'<h3>fox</h3>': '### fox',
'<h4>fox</h4>': '#### fox',
'<h5>fox</h5>': '##### fox',
'<h6>fox</h6>': '###### fox',
'<span>fox</span>': 'fox',
'<p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
'<mx-reply>beep</mx-reply><p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
'<pre><code></code></pre>': '``````',
};
for (final entry in testMap.entries) {
test(entry.key, () async {
expect(HtmlToText.convert(entry.key), entry.value);
});
}
});
}

266
test/image_pack_test.dart Normal file
View file

@ -0,0 +1,266 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2021 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 'package:test/test.dart';
import 'package:matrix/matrix.dart';
import 'fake_client.dart';
void main() {
group('Image Pack', () {
late Client client;
late Room room;
late Room room2;
test('setupClient', () async {
client = await getClient();
room = Room(id: '!1234:fakeServer.notExisting', client: client);
room2 = Room(id: '!abcd:fakeServer.notExisting', client: client);
room.setState(Event(
type: 'm.room.power_levels',
content: {},
room: room,
stateKey: '',
senderId: client.userID!,
eventId: '\$fakeid1:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
room.setState(Event(
type: 'm.room.member',
content: {'membership': 'join'},
room: room,
stateKey: client.userID,
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid2:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
room2.setState(Event(
type: 'm.room.power_levels',
content: {},
room: room,
stateKey: '',
senderId: client.userID!,
eventId: '\$fakeid3:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
room2.setState(Event(
type: 'm.room.member',
content: {'membership': 'join'},
room: room,
stateKey: client.userID,
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid4:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
client.rooms.add(room);
client.rooms.add(room2);
});
test('Single room', () async {
room.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'room_plain': {'url': 'mxc://room_plain'}
}
},
room: room,
stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid5:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
final packs = room.getImagePacks();
expect(packs.length, 1);
expect(packs['room']?.images.length, 1);
expect(packs['room']?.images['room_plain']?.url.toString(),
'mxc://room_plain');
var packsFlat = room.getImagePacksFlat();
expect(packsFlat, {
'room': {'room_plain': 'mxc://room_plain'}
});
room.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'emote': {
'url': 'mxc://emote',
'usage': ['emoticon']
},
'sticker': {
'url': 'mxc://sticker',
'usage': ['sticker']
},
}
},
room: room,
stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid6:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote'}
});
packsFlat = room.getImagePacksFlat(ImagePackUsage.sticker);
expect(packsFlat, {
'room': {'sticker': 'mxc://sticker'}
});
room.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'emote': {'url': 'mxc://emote'},
'sticker': {'url': 'mxc://sticker'},
},
'pack': {
'usage': ['emoticon'],
}
},
room: room,
stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid7:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'}
});
packsFlat = room.getImagePacksFlat(ImagePackUsage.sticker);
expect(packsFlat, {});
room.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'fox': {'url': 'mxc://fox'},
},
'pack': {
'usage': ['emoticon'],
}
},
room: room,
stateKey: 'fox',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid8:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
'fox': {'fox': 'mxc://fox'},
});
});
test('user pack', () async {
client.accountData['im.ponies.user_emotes'] = BasicEvent.fromJson({
'type': 'im.ponies.user_emotes',
'content': {
'images': {
'user': {
'url': 'mxc://user',
}
},
},
});
final packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
'fox': {'fox': 'mxc://fox'},
'user': {'user': 'mxc://user'},
});
});
test('other rooms', () async {
room2.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'other_room_emote': {'url': 'mxc://other_room_emote'},
},
'pack': {
'usage': ['emoticon'],
}
},
room: room2,
stateKey: '',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid9:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
'type': 'im.ponies.emote_rooms',
'content': {
'rooms': {
'!abcd:fakeServer.notExisting': {'': {}},
},
},
});
var packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
'fox': {'fox': 'mxc://fox'},
'user': {'user': 'mxc://user'},
'empty-chat-abcdfakeservernotexisting': {
'other_room_emote': 'mxc://other_room_emote'
},
});
room2.setState(Event(
type: 'im.ponies.room_emotes',
content: {
'images': {
'other_fox': {'url': 'mxc://other_fox'},
},
'pack': {
'usage': ['emoticon'],
}
},
room: room2,
stateKey: 'fox',
senderId: '\@fakeuser:fakeServer.notExisting',
eventId: '\$fakeid10:fakeServer.notExisting',
originServerTs: DateTime.now(),
));
client.accountData['im.ponies.emote_rooms'] = BasicEvent.fromJson({
'type': 'im.ponies.emote_rooms',
'content': {
'rooms': {
'!abcd:fakeServer.notExisting': {'': {}, 'fox': {}},
},
},
});
packsFlat = room.getImagePacksFlat(ImagePackUsage.emoticon);
expect(packsFlat, {
'room': {'emote': 'mxc://emote', 'sticker': 'mxc://sticker'},
'fox': {'fox': 'mxc://fox'},
'user': {'user': 'mxc://user'},
'empty-chat-abcdfakeservernotexisting': {
'other_room_emote': 'mxc://other_room_emote'
},
'empty-chat-fox-abcdfakeservernotexisting': {
'other_fox': 'mxc://other_fox'
},
});
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
}

130
test/markdown_test.dart Normal file
View file

@ -0,0 +1,130 @@
/*
* 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 'package:matrix/src/utils/markdown.dart';
import 'package:test/test.dart';
void main() {
group('markdown', () {
final emotePacks = {
'room': {
'fox': 'mxc://roomfox',
'bunny': 'mxc://roombunny',
},
'user': {
'fox': 'mxc://userfox',
'bunny': 'mxc://userbunny',
'raccoon': 'mxc://raccoon',
},
};
final mentionMap = {
'@Bob': '@bob:example.org',
'@[Bob Ross]': '@bobross:example.org',
'@Fox#123': '@fox:example.org',
'@[Fast Fox]#123': '@fastfox:example.org',
'@[">]': '@blah:example.org',
};
final getMention = (mention) => mentionMap[mention];
test('simple markdown', () {
expect(markdown('hey *there* how are **you** doing?'),
'hey <em>there</em> how are <strong>you</strong> doing?');
expect(markdown('wha ~~strike~~ works!'), 'wha <del>strike</del> works!');
});
test('spoilers', () {
expect(markdown('Snape killed ||Dumbledoor||'),
'Snape killed <span data-mx-spoiler="">Dumbledoor</span>');
expect(markdown('Snape killed ||Story|Dumbledoor||'),
'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>');
expect(markdown('Snape killed ||Some dumb loser|Dumbledoor||'),
'Snape killed <span data-mx-spoiler="Some dumb loser">Dumbledoor</span>');
expect(markdown('Snape killed ||Some dumb loser|Dumbledoor **bold**||'),
'Snape killed <span data-mx-spoiler="Some dumb loser">Dumbledoor <strong>bold</strong></span>');
expect(markdown('Snape killed ||Dumbledoor **bold**||'),
'Snape killed <span data-mx-spoiler="">Dumbledoor <strong>bold</strong></span>');
});
test('multiple paragraphs', () {
expect(markdown('Heya!\n\nBeep'), '<p>Heya!</p>\n<p>Beep</p>');
});
test('Other block elements', () {
expect(markdown('# blah\n\nblubb'), '<h1>blah</h1>\n<p>blubb</p>');
});
test('linebreaks', () {
expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
});
test('emotes', () {
expect(markdown(':fox:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':user~fox:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', getEmotePacks: () => emotePacks),
'<img data-mx-emoticon="" src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(
markdown(':invalid:', getEmotePacks: () => emotePacks), ':invalid:');
expect(markdown(':invalid:?!', getEmotePacks: () => emotePacks),
':invalid:?!');
expect(markdown(':room~invalid:', getEmotePacks: () => emotePacks),
':room~invalid:');
});
test('pills', () {
expect(markdown('Hey @sorunome:sorunome.de!'),
'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
expect(markdown('#fox:sorunome.de: you all are awesome'),
'<a href="https://matrix.to/#/#fox:sorunome.de">#fox:sorunome.de</a>: you all are awesome');
expect(markdown('!blah:example.org'),
'<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</a>');
expect(markdown('https://matrix.to/#/#fox:sorunome.de'),
'https://matrix.to/#/#fox:sorunome.de');
expect(markdown('Hey @sorunome:sorunome.de:1234!'),
'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de:1234">@sorunome:sorunome.de:1234</a>!');
expect(markdown('Hey @sorunome:127.0.0.1!'),
'Hey <a href="https://matrix.to/#/@sorunome:127.0.0.1">@sorunome:127.0.0.1</a>!');
expect(markdown('Hey @sorunome:[::1]!'),
'Hey <a href="https://matrix.to/#/@sorunome:[::1]">@sorunome:[::1]</a>!');
});
test('mentions', () {
expect(markdown('Hey @Bob!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@bob:example.org">@Bob</a>!');
expect(markdown('How is @[Bob Ross] doing?', getMention: getMention),
'How is <a href="https://matrix.to/#/@bobross:example.org">@[Bob Ross]</a> doing?');
expect(
markdown('Hey @invalid!', getMention: getMention), 'Hey @invalid!');
expect(markdown('Hey @Fox#123!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@fox:example.org">@Fox#123</a>!');
expect(markdown('Hey @[Fast Fox]#123!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@fastfox:example.org">@[Fast Fox]#123</a>!');
expect(markdown('Hey @[">]!', getMention: getMention),
'Hey <a href="https://matrix.to/#/@blah:example.org">@[&quot;&gt;]</a>!');
});
test('latex', () {
expect(markdown('meep \$\\frac{2}{3}\$'),
'meep <span data-mx-maths="\\frac{2}{3}"><code>\\frac{2}{3}</code></span>');
expect(markdown('meep \$hmm *yay*\$'),
'meep <span data-mx-maths="hmm *yay*"><code>hmm *yay*</code></span>');
expect(markdown('you have \$somevar and \$someothervar'),
'you have \$somevar and \$someothervar');
expect(markdown('meep ||\$\\frac{2}{3}\$||'),
'meep <span data-mx-spoiler=""><span data-mx-maths="\\frac{2}{3}"><code>\\frac{2}{3}</code></span></span>');
expect(markdown('meep `\$\\frac{2}{3}\$`'),
'meep <code>\$\\frac{2}{3}\$</code>');
expect(markdown('hey\n\$\$beep\$\$\nmeow'),
'<p>hey</p>\n<div data-mx-maths="beep">\n<pre><code>beep</code></pre>\n</div>\n<p>meow</p>');
expect(markdown('hey\n\$\$\nbeep\nboop\n\$\$\nmeow'),
'<p>hey</p>\n<div data-mx-maths="beep\nboop">\n<pre><code>beep\nboop</code></pre>\n</div>\n<p>meow</p>');
});
});
}

View file

@ -0,0 +1,39 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/src/utils/map_copy_extension.dart';
import 'package:test/test.dart';
void main() {
group('Map-copy-extension', () {
test('it should work', () {
final original = <String, dynamic>{
'attr': 'fox',
'child': <String, dynamic>{
'attr': 'bunny',
'list': [1, 2],
},
};
final copy = original.copy();
original['child']['attr'] = 'raccoon';
expect(copy['child']['attr'], 'bunny');
original['child']['list'].add(3);
expect(copy['child']['list'], [1, 2]);
});
});
}

View file

@ -0,0 +1,45 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix_api_lite/matrix_api_lite.dart';
import 'package:test/test.dart';
void main() {
group('Try-get-map-extension', () {
test('it should work', () {
final data = <String, dynamic>{
'str': 'foxies',
'int': 42,
'list': [2, 3, 4],
'map': <String, dynamic>{
'beep': 'boop',
},
};
expect(data.tryGet<String>('str'), 'foxies');
expect(data.tryGet<int>('str'), null);
expect(data.tryGet<int>('int'), 42);
expect(data.tryGet<List>('list'), [2, 3, 4]);
expect(data.tryGet<Map<String, dynamic>>('map')?.tryGet<String>('beep'),
'boop');
expect(data.tryGet<Map<String, dynamic>>('map')?.tryGet<String>('meep'),
null);
expect(data.tryGet<Map<String, dynamic>>('pam')?.tryGet<String>('beep'),
null);
});
});
}

View file

@ -0,0 +1,175 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'fake_database.dart';
void main() {
group('Databse', () {
Logs().level = Level.error;
final room = Room(id: '!room:blubb', client: Client('testclient'));
test('setupDatabase', () async {
final database = await getDatabase(null);
await database.insertClient(
'testclient',
'https://example.org',
'blubb',
'@test:example.org',
null,
null,
null,
null,
);
});
test('storeEventUpdate', () async {
final client = Client('testclient');
final database = await getDatabase(client);
// store a simple update
var update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': <String, dynamic>{'blah': 'blubb'},
'event_id': '\$event-1',
'sender': '@blah:blubb',
},
);
await database.storeEventUpdate(update, client);
var event = await database.getEventById('\$event-1', room);
expect(event?.eventId, '\$event-1');
// insert a transaction id
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': <String, dynamic>{'blah': 'blubb'},
'event_id': 'transaction-1',
'sender': '@blah:blubb',
'status': EventStatus.sending.intValue,
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('transaction-1', room);
expect(event?.eventId, 'transaction-1');
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': <String, dynamic>{'blah': 'blubb'},
'event_id': '\$event-2',
'sender': '@blah:blubb',
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-1',
},
'status': EventStatus.sent.intValue,
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('transaction-1', room);
expect(event, null);
event = await database.getEventById('\$event-2', room);
// insert a transaction id if the event id for it already exists
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': EventStatus.sending.intValue,
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('\$event-3', room);
expect(event?.eventId, '\$event-3');
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': EventStatus.sent.intValue,
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-2',
},
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('\$event-3', room);
expect(event?.eventId, '\$event-3');
expect(event?.status, EventStatus.sent);
event = await database.getEventById('transaction-2', room);
expect(event, null);
// insert transaction id and not update status
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': EventStatus.synced.intValue,
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('\$event-4', room);
expect(event?.eventId, '\$event-4');
update = EventUpdate(
type: EventUpdateType.timeline,
roomID: room.id,
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': EventStatus.sent.intValue,
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-3',
},
},
);
await database.storeEventUpdate(update, client);
event = await database.getEventById('\$event-4', room);
expect(event?.eventId, '\$event-4');
expect(event?.status, EventStatus.synced);
event = await database.getEventById('transaction-3', room);
expect(event, null);
});
});
}

View file

@ -0,0 +1,233 @@
/*
* 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 'package:matrix/matrix.dart';
class MatrixDefaultLocalizations extends MatrixLocalizations {
const MatrixDefaultLocalizations();
@override
String acceptedTheInvitation(String targetName) =>
'$targetName accepted the invitation';
@override
String activatedEndToEndEncryption(String senderName) =>
'$senderName activated end to end encryption';
@override
String get anyoneCanJoin => 'Anyone can join';
@override
String bannedUser(String senderName, String targetName) =>
'$senderName banned $targetName';
@override
String changedTheChatAvatar(String senderName) =>
'$senderName changed the chat avatar';
@override
String changedTheChatDescriptionTo(String senderName, String content) =>
'$senderName changed the chat description to $content';
@override
String changedTheChatNameTo(String senderName, String content) =>
'$senderName changed the chat name to $content';
@override
String changedTheChatPermissions(String senderName) =>
'$senderName changed the chat permissions';
@override
String changedTheDisplaynameTo(String targetName, String newDisplayname) =>
'$targetName changed the displayname to $newDisplayname';
@override
String changedTheGuestAccessRules(String senderName) =>
'$senderName changed the guest access rules';
@override
String changedTheGuestAccessRulesTo(
String senderName, String localizedString) =>
'$senderName changed the guest access rules to $localizedString';
@override
String changedTheHistoryVisibility(String senderName) =>
'$senderName changed the history visibility';
@override
String changedTheHistoryVisibilityTo(
String senderName, String localizedString) =>
'$senderName changed the history visibility to $localizedString';
@override
String changedTheJoinRules(String senderName) =>
'$senderName changed the join rules';
@override
String changedTheJoinRulesTo(String senderName, String localizedString) =>
'$senderName changed the join rules to $localizedString';
@override
String changedTheProfileAvatar(String targetName) =>
'$targetName changed the profile avatar';
@override
String changedTheRoomAliases(String senderName) =>
'$senderName changed the room aliases';
@override
String changedTheRoomInvitationLink(String senderName) =>
'$senderName changed the room invitation link';
@override
String get channelCorruptedDecryptError =>
'The secure channel has been corrupted';
@override
String couldNotDecryptMessage(String errorText) =>
'Could not decrypt message: $errorText';
@override
String createdTheChat(String senderName) => '$senderName created the chat';
@override
String get emptyChat => 'Empty chat';
@override
String get encryptionNotEnabled => 'Encryption not enabled';
@override
String get fromJoining => 'From joining';
@override
String get fromTheInvitation => 'From the invitation';
@override
String groupWith(String displayname) => 'Group with $displayname';
@override
String get guestsAreForbidden => 'Guests are forbidden';
@override
String get guestsCanJoin => 'Guests can join';
@override
String hasWithdrawnTheInvitationFor(String senderName, String targetName) =>
'$senderName has withdrawn the invitation for $targetName';
@override
String invitedUser(String senderName, String targetName) =>
'$senderName has invited $targetName';
@override
String get invitedUsersOnly => 'Invited users only';
@override
String joinedTheChat(String targetName) => '$targetName joined the chat';
@override
String kicked(String senderName, String targetName) =>
'$senderName kicked $targetName';
@override
String kickedAndBanned(String senderName, String targetName) =>
'$senderName banned $targetName';
@override
String get needPantalaimonWarning => 'Need pantalaimon';
@override
String get noPermission => 'No permission';
@override
String redactedAnEvent(String senderName) => '$senderName redacted an event';
@override
String rejectedTheInvitation(String targetName) =>
'$targetName rejected the invitation';
@override
String removedBy(String calcDisplayname) => 'Removed by $calcDisplayname';
@override
String get roomHasBeenUpgraded => 'Room has been upgraded';
@override
String sentAFile(String senderName) => '$senderName sent a file';
@override
String sentAPicture(String senderName) => '$senderName sent a picture';
@override
String sentASticker(String senderName) => '$senderName sent a sticker';
@override
String sentAVideo(String senderName) => '$senderName sent a video';
@override
String sentAnAudio(String senderName) => '$senderName sent an audio';
@override
String sharedTheLocation(String senderName) =>
'$senderName shared the location';
@override
String unbannedUser(String senderName, String targetName) =>
'$senderName unbanned $targetName';
@override
String get unknownEncryptionAlgorithm => 'Unknown encryption algorithm';
@override
String unknownEvent(String typeKey) => 'Unknown event $typeKey';
@override
String userLeftTheChat(String targetName) => '$targetName left the chat';
@override
String get visibleForAllParticipants => 'Visible for all participants';
@override
String get visibleForEveryone => 'Visible for everyone';
@override
String get you => 'You';
@override
String answeredTheCall(String senderName) {
return 'answeredTheCall';
}
@override
String endedTheCall(String senderName) {
return 'endedTheCall';
}
@override
String sentCallInformations(String senderName) {
return 'sentCallInformations';
}
@override
String startedACall(String senderName) {
return 'startedACall';
}
@override
String sentReaction(String senderName, String reactionKey) {
return '$senderName reacted with $reactionKey';
}
}

View file

@ -0,0 +1,68 @@
/*
* 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 'package:matrix/matrix.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';
void main() {
/// All Tests related to device keys
group('Matrix Exception', () {
Logs().level = Level.error;
test('Matrix Exception', () async {
final matrixException = MatrixException(
Response(
'{"flows":[{"stages":["example.type.foo"]}],"params":{"example.type.baz":{"example_key":"foobar"}},"session":"xxxxxxyz","completed":["example.type.foo"]}',
401,
),
);
expect(matrixException.errcode, 'M_FORBIDDEN');
final flows = matrixException.authenticationFlows;
expect(flows?.length, 1);
expect(flows?.first.stages.length, 1);
expect(flows?.first.stages.first, 'example.type.foo');
expect(
matrixException.authenticationParams?['example.type.baz'],
{'example_key': 'foobar'},
);
expect(matrixException.completedAuthenticationFlows.length, 1);
expect(matrixException.completedAuthenticationFlows.first,
'example.type.foo');
expect(matrixException.session, 'xxxxxxyz');
});
test('Unknown Exception', () async {
final matrixException = MatrixException(
Response(
'{"errcode":"M_HAHA","error":"HAHA","retry_after_ms":500}',
401,
),
);
expect(matrixException.error, MatrixError.M_UNKNOWN);
expect(matrixException.retryAfterMs, 500);
});
test('Missing Exception', () async {
final matrixException = MatrixException(
Response(
'{"error":"HAHA"}',
401,
),
);
expect(matrixException.error, MatrixError.M_UNKNOWN);
});
});
}

View file

@ -0,0 +1,49 @@
/*
* 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:typed_data';
import 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
void main() {
/// All Tests related to device keys
group('Matrix File', () {
Logs().level = Level.error;
test('Decrypt', () async {
final text = 'hello world';
final file = MatrixFile(
name: 'file.txt',
bytes: Uint8List.fromList(text.codeUnits),
);
var olmEnabled = true;
try {
await olm.init();
olm.Account();
} catch (_) {
olmEnabled = false;
}
if (olmEnabled) {
final encryptedFile = await file.encrypt();
expect(encryptedFile.data.isNotEmpty, true);
}
});
});
}

View file

@ -0,0 +1,119 @@
/*
* 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 'package:test/test.dart';
import 'package:matrix/src/utils/matrix_id_string_extension.dart';
void main() {
/// All Tests related to the ChatTime
group('Matrix ID String Extension', () {
test('Matrix ID String Extension', () async {
final mxId = '@test:example.com';
expect(mxId.isValidMatrixId, true);
expect('#test:example.com'.isValidMatrixId, true);
expect('!test:example.com'.isValidMatrixId, true);
expect('+test:example.com'.isValidMatrixId, true);
expect('\$test:example.com'.isValidMatrixId, true);
expect('\$testevent'.isValidMatrixId, true);
expect('test:example.com'.isValidMatrixId, false);
expect('@testexample.com'.isValidMatrixId, false);
expect('@:example.com'.isValidMatrixId, true);
expect('@test:'.isValidMatrixId, false);
expect(mxId.sigil, '@');
expect('#test:example.com'.sigil, '#');
expect('!test:example.com'.sigil, '!');
expect('+test:example.com'.sigil, '+');
expect('\$test:example.com'.sigil, '\$');
expect(mxId.localpart, 'test');
expect(mxId.domain, 'example.com');
expect(mxId.equals('@Test:example.com'), true);
expect(mxId.equals('@test:example.org'), false);
expect('@user:domain:8448'.localpart, 'user');
expect('@user:domain:8448'.domain, 'domain:8448');
});
test('parseIdentifierIntoParts', () {
var res = '#alias:beep'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, null);
expect('blha'.parseIdentifierIntoParts(), null);
res = '#alias:beep/\$event'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, '\$event');
expect(res.queryString, null);
res = '#alias:beep?blubb'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, 'blubb');
res = '#alias:beep/\$event?blubb'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, '\$event');
expect(res.queryString, 'blubb');
res = '#/\$?:beep/\$event?blubb?b'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#/\$?:beep');
expect(res.secondaryIdentifier, '\$event');
expect(res.queryString, 'blubb?b');
res = 'https://matrix.to/#/#alias:beep'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, null);
res = 'https://matrix.to/#/#🦊:beep'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#🦊:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, null);
res = 'https://matrix.to/#/%23alias%3abeep'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, null);
res = 'https://matrix.to/#/%23alias%3abeep?boop%F0%9F%A7%A1%F0%9F%A6%8A'
.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#alias:beep');
expect(res.secondaryIdentifier, null);
expect(res.queryString, 'boop%F0%9F%A7%A1%F0%9F%A6%8A');
res = 'https://matrix.to/#/#alias:beep?via=fox.com&via=fox.org'
.parseIdentifierIntoParts()!;
expect(res.via, <String>{'fox.com', 'fox.org'});
res = 'matrix:u/her:example.org'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '@her:example.org');
expect(res.secondaryIdentifier, null);
expect('matrix:u/bad'.parseIdentifierIntoParts(), null);
res = 'matrix:roomid/rid:example.org'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '!rid:example.org');
expect(res.secondaryIdentifier, null);
expect(res.action, null);
res = 'matrix:r/us:example.org?action=chat'.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#us:example.org');
expect(res.secondaryIdentifier, null);
expect(res.action, 'chat');
res = 'matrix:r/us:example.org/e/lol823y4bcp3qo4'
.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '#us:example.org');
expect(res.secondaryIdentifier, '\$lol823y4bcp3qo4');
res = 'matrix:roomid/rid:example.org?via=fox.com&via=fox.org'
.parseIdentifierIntoParts()!;
expect(res.primaryIdentifier, '!rid:example.org');
expect(res.secondaryIdentifier, null);
expect(res.via, <String>{'fox.com', 'fox.org'});
expect('matrix:beep/boop:example.org'.parseIdentifierIntoParts(), null);
expect('matrix:boop'.parseIdentifierIntoParts(), null);
});
});
}

View file

@ -0,0 +1,60 @@
/*
* 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'matrix_default_localizations.dart';
void main() {
/// All Tests related to device keys
group('Matrix Localizations', () {
test('Matrix Localizations', () {
expect(
HistoryVisibility.invited
.getLocalizedString(MatrixDefaultLocalizations()),
'From the invitation');
expect(
HistoryVisibility.joined
.getLocalizedString(MatrixDefaultLocalizations()),
'From joining');
expect(
HistoryVisibility.shared
.getLocalizedString(MatrixDefaultLocalizations()),
'Visible for all participants');
expect(
HistoryVisibility.worldReadable
.getLocalizedString(MatrixDefaultLocalizations()),
'Visible for everyone');
expect(
GuestAccess.canJoin.getLocalizedString(MatrixDefaultLocalizations()),
'Guests can join');
expect(
GuestAccess.forbidden
.getLocalizedString(MatrixDefaultLocalizations()),
'Guests are forbidden');
expect(JoinRules.invite.getLocalizedString(MatrixDefaultLocalizations()),
'Invited users only');
expect(JoinRules.public.getLocalizedString(MatrixDefaultLocalizations()),
'Anyone can join');
expect(JoinRules.private.getLocalizedString(MatrixDefaultLocalizations()),
'private');
expect(JoinRules.knock.getLocalizedString(MatrixDefaultLocalizations()),
'knock');
});
});
}

62
test/multilock_test.dart Normal file
View file

@ -0,0 +1,62 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2021 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 'package:matrix/src/utils/multilock.dart';
import 'package:test/test.dart';
void main() {
group('lock', () {
final lock = MultiLock<String>();
test('lock and unlock', () async {
// lock and unlock
await lock.lock(['fox']);
lock.unlock(['fox']);
expect(true, true); // we were able to reach this line of code!
});
test('lock the same lock', () async {
var counter = 0;
await lock.lock(['fox']);
final future = lock.lock(['fox']).then((_) {
counter++;
lock.unlock(['fox']);
});
await Future.delayed(Duration(milliseconds: 50));
expect(counter, 0);
lock.unlock(['fox']);
await future;
expect(counter, 1);
});
test('multilock', () async {
var counter = 0;
await lock.lock(['fox']);
final future1 = lock.lock(['fox', 'raccoon']).then((_) {
counter++;
lock.unlock(['fox', 'raccoon']);
});
await Future.delayed(Duration(milliseconds: 50));
expect(counter, 0);
await lock.lock(['raccoon']);
lock.unlock(['fox']);
await Future.delayed(Duration(milliseconds: 50));
expect(counter, 0);
lock.unlock(['raccoon']);
await future1;
expect(counter, 1);
});
});
}

View file

@ -0,0 +1,88 @@
/*
* 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
void main() {
/// All Tests related to the MxContent
group('MxContent', () {
Logs().level = Level.error;
test('Formatting', () async {
final client = Client('testclient', httpClient: FakeMatrixApi());
await client.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
final mxc = 'mxc://exampleserver.abc/abcdefghijklmn';
final content = Uri.parse(mxc);
expect(content.isScheme('mxc'), true);
expect(content.getDownloadLink(client).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
expect(
content
.getThumbnail(client,
width: 50,
height: 50,
method: ThumbnailMethod.scale,
animated: true)
.toString(),
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
});
test('other port', () async {
final client = Client('testclient', httpClient: FakeMatrixApi());
await client.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
client.homeserver = Uri.parse('https://fakeserver.notexisting:1337');
final mxc = 'mxc://exampleserver.abc/abcdefghijklmn';
final content = Uri.parse(mxc);
expect(content.isScheme('mxc'), true);
expect(content.getDownloadLink(client).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc/abcdefghijklmn');
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
expect(
content
.getThumbnail(client,
width: 50,
height: 50,
method: ThumbnailMethod.scale,
animated: true)
.toString(),
'https://fakeserver.notexisting:1337/_matrix/media/r0/thumbnail/exampleserver.abc/abcdefghijklmn?width=50&height=50&method=scale&animated=true');
});
test('other remote port', () async {
final client = Client('testclient', httpClient: FakeMatrixApi());
await client.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
final mxc = 'mxc://exampleserver.abc:1234/abcdefghijklmn';
final content = Uri.parse(mxc);
expect(content.isScheme('mxc'), true);
expect(content.getDownloadLink(client).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/download/exampleserver.abc:1234/abcdefghijklmn');
expect(content.getThumbnail(client, width: 50, height: 50).toString(),
'${client.homeserver.toString()}/_matrix/media/r0/thumbnail/exampleserver.abc:1234/abcdefghijklmn?width=50&height=50&method=crop&animated=false');
});
});
}

1138
test/room_test.dart Normal file

File diff suppressed because it is too large Load diff

169
test/sync_filter_test.dart Normal file
View file

@ -0,0 +1,169 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
const updates = {
'empty': {
'next_batch': 'blah',
'account_data': {
'events': [],
},
'presences': {
'events': [],
},
'rooms': {
'join': {},
'leave': {},
'invite': {},
},
'to_device': {
'events': [],
},
},
'presence': {
'next_batch': 'blah',
'presence': {
'events': [
{
'content': {
'avatar_url': 'mxc://localhost:wefuiwegh8742w',
'last_active_ago': 2478593,
'presence': 'online',
'currently_active': false,
'status_msg': 'Making cupcakes'
},
'type': 'm.presence',
'sender': '@example:localhost',
},
],
},
},
'account_data': {
'next_batch': 'blah',
'account_data': {
'events': [
{
'type': 'blah',
'content': {
'beep': 'boop',
},
},
],
},
},
'invite': {
'next_batch': 'blah',
'rooms': {
'invite': {
'!room': {
'invite_state': {
'events': [],
},
},
},
},
},
'leave': {
'next_batch': 'blah',
'rooms': {
'leave': {
'!room': <String, dynamic>{},
},
},
},
'join': {
'next_batch': 'blah',
'rooms': {
'join': {
'!room': {
'timeline': {
'events': [],
},
'state': {
'events': [],
},
'account_data': {
'events': [],
},
'ephemeral': {
'events': [],
},
'unread_notifications': <String, dynamic>{},
'summary': <String, dynamic>{},
},
},
},
},
'to_device': {
'next_batch': 'blah',
'to_device': {
'events': [
{
'type': 'beep',
'sender': '@example:localhost',
'content': {
'blah': 'blubb',
},
},
],
},
},
};
void testUpdates(bool Function(SyncUpdate s) test, Map<String, bool> expected) {
for (final update in updates.entries) {
final sync = SyncUpdate.fromJson(update.value);
expect(test(sync), expected[update.key]);
}
}
void main() {
group('Sync Filters', () {
Logs().level = Level.error;
test('room update', () {
final testFn = (SyncUpdate s) => s.hasRoomUpdate;
final expected = {
'empty': false,
'presence': false,
'account_data': true,
'invite': true,
'leave': true,
'join': true,
'to_device': true,
};
testUpdates(testFn, expected);
});
test('presence update', () {
final testFn = (SyncUpdate s) => s.hasPresenceUpdate;
final expected = {
'empty': false,
'presence': true,
'account_data': false,
'invite': false,
'leave': false,
'join': false,
'to_device': false,
};
testUpdates(testFn, expected);
});
});
}

620
test/timeline_test.dart Normal file
View file

@ -0,0 +1,620 @@
/*
* 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'fake_client.dart';
void main() {
group('Timeline', () {
Logs().level = Level.error;
final roomID = '!1234:example.com';
final testTimeStamp = DateTime.now().millisecondsSinceEpoch;
var updateCount = 0;
final insertList = <int>[];
final changeList = <int>[];
final removeList = <int>[];
var olmEnabled = true;
late Client client;
late Room room;
late Timeline timeline;
test('create stuff', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
client = await getClient();
client.sendMessageTimeoutSeconds = 5;
room = Room(
id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
timeline = Timeline(
room: room,
events: [],
onUpdate: () {
updateCount++;
},
onInsert: insertList.add,
onChange: changeList.add,
onRemove: removeList.add,
);
});
test('Create', () async {
await client.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
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.synced.intValue,
'event_id': '2',
'origin_server_ts': testTimeStamp - 1000
},
));
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.synced.intValue,
'event_id': '1',
'origin_server_ts': testTimeStamp
},
));
expect(timeline.sub != null, true);
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 2);
expect(insertList, [0, 0]);
expect(insertList.length, timeline.events.length);
expect(changeList, []);
expect(removeList, []);
expect(timeline.events.length, 2);
expect(timeline.events[0].eventId, '1');
expect(timeline.events[0].sender.id, '@alice:example.com');
expect(timeline.events[0].originServerTs.millisecondsSinceEpoch,
testTimeStamp);
expect(timeline.events[0].body, 'Testcase');
expect(
timeline.events[0].originServerTs.millisecondsSinceEpoch >
timeline.events[1].originServerTs.millisecondsSinceEpoch,
true);
expect(timeline.events[0].receipts, []);
room.roomAccountData['m.receipt'] = BasicRoomEvent.fromJson({
'type': 'm.receipt',
'content': {
'@alice:example.com': {
'event_id': '1',
'ts': 1436451550453,
}
},
'room_id': roomID,
});
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].receipts.length, 1);
expect(timeline.events[0].receipts[0].user.id, '@alice:example.com');
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.redaction',
'content': {'reason': 'spamming'},
'sender': '@alice:example.com',
'redacts': '2',
'event_id': '3',
'origin_server_ts': testTimeStamp + 1000
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 3);
expect(insertList, [0, 0, 0]);
expect(insertList.length, timeline.events.length);
expect(changeList, [2]);
expect(removeList, []);
expect(timeline.events.length, 3);
expect(timeline.events[2].redacted, true);
});
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]);
expect(insertList.length, timeline.events.length);
final eventId = timeline.events[0].eventId;
expect(eventId.startsWith('\$event'), true);
expect(timeline.events[0].status, EventStatus.sent);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'test'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': eventId,
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 6);
expect(insertList, [0, 0, 0, 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'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
));
await Future.delayed(Duration(milliseconds: 50));
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));
expect(updateCount, 14);
expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2]);
expect(changeList, [2, 0, 0, 0, 1, 2]);
expect(removeList, [0]);
expect(timeline.events.length, 7);
expect(timeline.events[0].status, EventStatus.error);
});
test('getEventById', () async {
var event = await timeline.getEventById('abc');
expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'});
event = await timeline.getEventById('not_found');
expect(event, null);
event = await timeline.getEventById('unencrypted_event');
expect(event?.body, 'This is an example text message');
if (olmEnabled) {
event = await timeline.getEventById('encrypted_event');
// the event is invalid but should have traces of attempting to decrypt
expect(event?.messageType, MessageTypes.BadEncrypted);
}
});
test('Resend message', () async {
timeline.events.clear();
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.error.intValue,
'event_id': 'new-test-event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'newresend'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
await timeline.events[0].sendAgain();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 17);
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(timeline.events.length, 1);
expect(timeline.events[0].status, EventStatus.sent);
});
test('Request history', () async {
timeline.events.clear();
expect(timeline.canRequestHistory, true);
await room.requestHistory();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 20);
expect(insertList, [0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 1, 2]);
expect(timeline.events.length, 3);
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[2].eventId, '1143273582443PhrSn:example.org');
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
await timeline.events[2].redactEvent(reason: 'test', txid: '1234');
});
test('Clear cache on limited timeline', () async {
client.onSync.add(
SyncUpdate(
nextBatch: '1234',
rooms: RoomsUpdate(
join: {
roomID: JoinedRoomUpdate(
timeline: TimelineUpdate(
limited: true,
prevBatch: 'blah',
),
unreadNotifications: UnreadNotificationCounts(
highlightCount: 0,
notificationCount: 0,
),
),
},
),
),
);
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events.isEmpty, true);
});
test('sort errors on top', () async {
timeline.events.clear();
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.error.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
));
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.synced.intValue,
'event_id': 'def',
'origin_server_ts': testTimeStamp + 5
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.synced);
});
test('sending event to failed update', () async {
timeline.events.clear();
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': 'will-fail',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
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.error.intValue,
'event_id': 'will-fail',
'origin_server_ts': testTimeStamp
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
});
test('setReadMarker', () 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.synced.intValue,
'event_id': 'will-work',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
room.notificationCount = 1;
await timeline.setReadMarker();
expect(room.notificationCount, 0);
});
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
() async {
timeline.events.clear();
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': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
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.sent.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sent);
expect(timeline.events.length, 1);
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.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event where the sync reply arrives first, 0 -> 2 -> 1',
() async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'unsigned': {
messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': 'transaction',
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: EventStatus.synced.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: EventStatus.sent.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> -1 -> 2', () async {
timeline.events.clear();
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': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
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.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
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.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> 2 -> -1', () async {
timeline.events.clear();
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': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
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.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
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.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('logout', () async {
await client.logout();
});
});
}

107
test/uia_test.dart Normal file
View file

@ -0,0 +1,107 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
void main() {
group('UIA', () {
Logs().level = Level.error;
test('it should work', () async {
final completed = <String>[];
var updated = 0;
var finished = false;
final request = UiaRequest(
request: (auth) async {
if (auth != null &&
auth.session != null &&
auth.session != 'foxies') {
throw MatrixException.fromJson(<String, dynamic>{});
}
if (auth != null && auth.type == 'stage1') {
if (completed.isEmpty) {
completed.add('stage1');
}
} else if (auth != null && auth.type == 'stage2') {
if (completed.length == 1 && completed[0] == 'stage1') {
// okay, we are done!
return 'FOXIES ARE FLOOOOOFY!!!!!';
}
}
final res = <String, dynamic>{
'session': 'foxies',
'completed': completed,
'flows': [
<String, dynamic>{
'stages': ['stage1', 'stage2'],
}
],
'params': <String, dynamic>{},
};
throw MatrixException.fromJson(res);
},
onUpdate: (state) {
if (state == UiaRequestState.done) {
finished = true;
}
updated++;
},
);
await Future.delayed(Duration(milliseconds: 50));
expect(request.nextStages.contains('stage1'), true);
expect(request.nextStages.length, 1);
expect(updated, 1);
expect(finished, false);
await request.completeStage(AuthenticationData(type: 'stage1'));
expect(request.nextStages.contains('stage2'), true);
expect(request.nextStages.length, 1);
expect(updated, 3);
expect(finished, false);
final res =
await request.completeStage(AuthenticationData(type: 'stage2'));
expect(res, 'FOXIES ARE FLOOOOOFY!!!!!');
expect(request.result, 'FOXIES ARE FLOOOOOFY!!!!!');
expect(request.state, UiaRequestState.done);
expect(updated, 5);
expect(finished, true);
});
test('it should throw errors', () async {
var updated = false;
var finished = false;
final request = UiaRequest(
request: (auth) async {
throw Exception('nope');
},
onUpdate: (state) {
if (state == UiaRequestState.fail) {
finished = true;
}
updated = true;
},
);
await Future.delayed(Duration(milliseconds: 50));
expect(request.state, UiaRequestState.fail);
expect(updated, true);
expect(finished, true);
expect(request.error.toString(), Exception('nope').toString());
});
});
}

168
test/user_test.dart Normal file
View file

@ -0,0 +1,168 @@
/*
* 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 'package:matrix/matrix.dart';
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
void main() {
/// All Tests related to the Event
group('User', () {
Logs().level = Level.error;
final client = Client('testclient', httpClient: FakeMatrixApi());
final room = Room(id: '!localpart:server.abc', client: client);
final user1 = User(
'@alice:example.com',
membership: 'join',
displayName: 'Alice M',
avatarUrl: 'mxc://bla',
room: room,
);
final user2 = User(
'@bob:example.com',
membership: 'join',
displayName: 'Bob',
avatarUrl: 'mxc://bla',
room: room,
);
room.setState(user1);
room.setState(user2);
setUp(() async {
await client.checkHomeserver('https://fakeserver.notexisting',
checkWellKnown: false);
await client.login(LoginType.mLoginPassword,
identifier: AuthenticationUserIdentifier(user: 'test'),
password: '1234');
});
tearDown(() async {
await client.logout();
});
test('create', () async {
expect(user1.powerLevel, 0);
expect(user1.stateKey, '@alice:example.com');
expect(user1.id, '@alice:example.com');
expect(user1.membership, Membership.join);
expect(user1.avatarUrl.toString(), 'mxc://bla');
expect(user1.displayName, 'Alice M');
});
test('Create from json', () async {
final id = '@alice:server.abc';
final membership = Membership.join;
final displayName = 'Alice';
final avatarUrl = '';
final jsonObj = {
'content': {
'membership': 'join',
'avatar_url': avatarUrl,
'displayname': displayName
},
'type': 'm.room.member',
'event_id': '143273582443PhrSn:example.org',
'room_id': '!636q39766251:example.com',
'sender': id,
'origin_server_ts': 1432735824653,
'unsigned': {'age': 1234},
'state_key': id
};
final user = Event.fromJson(jsonObj, room).asUser;
expect(user.id, id);
expect(user.membership, membership);
expect(user.displayName, displayName);
expect(user.avatarUrl.toString(), avatarUrl);
expect(user.calcDisplayname(), displayName);
});
test('calcDisplayname', () async {
final user1 = User('@alice:example.com', room: room);
final user2 = User('@SuperAlice:example.com', room: room);
final user3 = User('@alice_mep:example.com', room: room);
expect(user1.calcDisplayname(), 'Alice');
expect(user2.calcDisplayname(), 'SuperAlice');
expect(user3.calcDisplayname(), 'Alice Mep');
expect(user3.calcDisplayname(formatLocalpart: false), 'alice_mep');
expect(
user3.calcDisplayname(mxidLocalPartFallback: false), 'Unknown user');
});
test('kick', () async {
await user1.kick();
});
test('ban', () async {
await user1.ban();
});
test('unban', () async {
await user1.unban();
});
test('setPower', () async {
await user1.setPower(50);
});
test('startDirectChat', () async {
await user1.startDirectChat(waitForSync: false);
});
test('getPresence', () async {
await client.handleSync(SyncUpdate.fromJson({
'next_batch': 'fake',
'presence': {
'events': [
{
'sender': '@alice:example.com',
'type': 'm.presence',
'content': {'presence': 'online'}
}
]
}
}));
expect(user1.presence?.presence.presence, PresenceType.online);
});
test('canBan', () async {
expect(user1.canBan, false);
});
test('canKick', () async {
expect(user1.canKick, false);
});
test('canChangePowerLevel', () async {
expect(user1.canChangePowerLevel, false);
});
test('mention', () async {
expect(user1.mention, '@[Alice M]');
expect(user2.mention, '@Bob');
user1.content['displayname'] = '[Alice M]';
expect(user1.mention, '@alice:example.com');
user1.content['displayname'] = 'Alice:M';
expect(user1.mention, '@alice:example.com');
user1.content['displayname'] = 'Alice M';
user2.content['displayname'] = 'Alice M';
expect(user1.mention, '@[Alice M]#1745');
user1.content['displayname'] = 'Bob';
user2.content['displayname'] = 'Bob';
expect(user1.mention, '@Bob#1745');
user1.content['displayname'] = 'Alice M';
});
test('mentionFragments', () async {
expect(user1.mentionFragments, {'@[Alice M]', '@[Alice M]#1745'});
expect(user2.mentionFragments, {'@Bob', '@Bob#1542'});
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});
});
}