Initial commit version 0.8.13
This commit is contained in:
commit
9526dfa4f2
111 changed files with 35074 additions and 0 deletions
54
test/canonical_json_test.dart
Normal file
54
test/canonical_json_test.dart
Normal 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
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
323
test/commands_test.dart
Normal 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
485
test/database_api_test.dart
Normal 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;
|
||||
}
|
||||
265
test/device_keys_list_test.dart
Normal file
265
test/device_keys_list_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
275
test/encryption/bootstrap_test.dart
Normal file
275
test/encryption/bootstrap_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
122
test/encryption/cross_signing_test.dart
Normal file
122
test/encryption/cross_signing_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
107
test/encryption/encrypt_decrypt_room_message_test.dart
Normal file
107
test/encryption/encrypt_decrypt_room_message_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
124
test/encryption/encrypt_decrypt_to_device_test.dart
Normal file
124
test/encryption/encrypt_decrypt_to_device_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
577
test/encryption/key_manager_test.dart
Normal file
577
test/encryption/key_manager_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
404
test/encryption/key_request_test.dart
Normal file
404
test/encryption/key_request_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
486
test/encryption/key_verification_test.dart
Normal file
486
test/encryption/key_verification_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
264
test/encryption/olm_manager_test.dart
Normal file
264
test/encryption/olm_manager_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
124
test/encryption/online_key_backup_test.dart
Normal file
124
test/encryption/online_key_backup_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
501
test/encryption/ssss_test.dart
Normal file
501
test/encryption/ssss_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
76
test/encryption/utils_test.dart
Normal file
76
test/encryption/utils_test.dart
Normal 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
1625
test/event_test.dart
Normal file
File diff suppressed because it is too large
Load diff
50
test/fake_client.dart
Normal file
50
test/fake_client.dart
Normal 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
55
test/fake_database.dart
Normal 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
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
101
test/html_to_text_test.dart
Normal 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>>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
266
test/image_pack_test.dart
Normal 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
130
test/markdown_test.dart
Normal 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">@[">]</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>');
|
||||
});
|
||||
});
|
||||
}
|
||||
39
test/matrix_api/map_copy_extension_test.dart
Normal file
39
test/matrix_api/map_copy_extension_test.dart
Normal 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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
45
test/matrix_api/try_get_map_extension_test.dart
Normal file
45
test/matrix_api/try_get_map_extension_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
175
test/matrix_database_test.dart
Normal file
175
test/matrix_database_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
233
test/matrix_default_localizations.dart
Normal file
233
test/matrix_default_localizations.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
68
test/matrix_exception_test.dart
Normal file
68
test/matrix_exception_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
49
test/matrix_file_test.dart
Normal file
49
test/matrix_file_test.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
119
test/matrix_id_string_extension_test.dart
Normal file
119
test/matrix_id_string_extension_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
60
test/matrix_localizations_test.dart
Normal file
60
test/matrix_localizations_test.dart
Normal 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
62
test/multilock_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
88
test/mxc_uri_extension_test.dart
Normal file
88
test/mxc_uri_extension_test.dart
Normal 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
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
169
test/sync_filter_test.dart
Normal 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
620
test/timeline_test.dart
Normal 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
107
test/uia_test.dart
Normal 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
168
test/user_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue