twonly-app/test/mocks/test_client.dart
2026-08-29 15:43:32 +02:00

288 lines
8.6 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/api.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart';
import 'user_config.dart';
import 'user_environment.dart';
class RealHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (cert, host, port) {
return true;
};
}
}
class TestClient {
TestClient(this.localIdSeed) {
final timeStr = DateTime.now().millisecondsSinceEpoch.toString();
username = 't_${timeStr.substring(timeStr.length - 6)}$localIdSeed';
}
late final UserEnvironment env;
late final ApiService api;
final int localIdSeed;
late String username;
Group? defaultGroup;
int realUserId = 0;
Future<void> init({bool disablePqc = false}) async {
env = await UserEnvironment.create(localIdSeed, username);
if (disablePqc) {
env.userService.currentUser.signalLastPqcPreKeysUploaded = DateTime.now()
.add(const Duration(days: 365));
}
api = ApiService();
await run(() async {
Log.info('Requesting POW...');
final FrbProofOfWork pow;
try {
pow = await RustApi.getProofOfWork();
} catch (e, st) {
if (kDebugMode) {
print('POW EXCEPTION: $e\n$st');
}
rethrow;
}
Log.info('POW result: $pow');
final proof = await calculatePoW(pow.prefix, pow.difficulty);
realUserId = await RustApi.register(
username: username,
proofOfWork: proof,
langCode: 'en',
isIos: false,
);
final userData = testUserConfig(
userId: realUserId,
username: username,
displayName: username,
subscriptionPlan: 'Free',
currentSetupPage: null,
appVersion: 100,
);
await UserService.save(userData);
});
}
Future<void> initContact(TestClient other) async {
await run(() async {
await env.db.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(
userId: Value(other.realUserId),
username: other.username,
accepted: const Value(true),
),
);
defaultGroup = await env.db.groupsDao.createNewDirectChat(
other.realUserId,
GroupsCompanion(groupName: Value(other.username)),
);
await RustApi.establishSignalSession(contactId: other.realUserId);
});
}
Future<T> run<T>(Future<T> Function() computation) {
return runInZone(env, api, computation);
}
Future<Message> sendText(TestClient target, String text) async {
return run(() async {
final m = await env.db.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(defaultGroup!.groupId),
content: Value(text),
type: Value(MessageType.text.name),
),
);
await RustApi.sendEncryptedContent(
contactId: target.realUserId,
content: pb.EncryptedContent(
groupId: defaultGroup!.groupId,
textMessage: pb.EncryptedContent_TextMessage()
..senderMessageId = m!.messageId
..text = text,
).writeToBuffer(),
messageId: m.messageId,
);
return m;
});
}
Future<void> sendEncryptedContent(
TestClient target,
pb.EncryptedContent content,
) async {
await run(() async {
await RustApi.sendEncryptedContent(
contactId: target.realUserId,
content: content.writeToBuffer(),
);
});
}
Future<void> sendGroupJoin(
TestClient target,
String groupId,
List<int> groupPublicKey,
) async {
final content = pb.EncryptedContent()
..groupId = groupId
..groupJoin = (pb.EncryptedContent_GroupJoin()
..groupPublicKey = groupPublicKey);
await sendEncryptedContent(target, content);
}
Future<void> sendResendGroupPublicKey(
TestClient target,
String groupId,
) async {
final content = pb.EncryptedContent()
..groupId = groupId
..resendGroupPublicKey = pb.EncryptedContent_ResendGroupPublicKey();
await sendEncryptedContent(target, content);
}
Future<void> sendErrorMessages(
TestClient target,
pb.EncryptedContent_ErrorMessages_Type type,
String receiptId,
) async {
final content = pb.EncryptedContent()
..errorMessages = (pb.EncryptedContent_ErrorMessages()
..type = type
..relatedReceiptId = receiptId);
await sendEncryptedContent(target, content);
}
Future<void> sendUserDiscoveryRequest(
TestClient target,
List<int> version,
) async {
final content = pb.EncryptedContent()
..userDiscoveryRequest = (pb.EncryptedContent_UserDiscoveryRequest()
..currentVersion = version);
await sendEncryptedContent(target, content);
}
Future<void> sendUserDiscoveryUpdate(
TestClient target,
List<List<int>> messages,
) async {
final content = pb.EncryptedContent()
..userDiscoveryUpdate = (pb.EncryptedContent_UserDiscoveryUpdate()
..messages.addAll(messages));
await sendEncryptedContent(target, content);
}
Future<void> sendKeyVerificationProof(
TestClient target,
List<int> mac,
) async {
final content = pb.EncryptedContent()
..keyVerificationProof = (pb.EncryptedContent_KeyVerificationProof()
..calculatedMac = mac);
await sendEncryptedContent(target, content);
}
Future<void> sendDeliveryReceipt(TestClient target, String receiptId) async {
final msg = pb.Message()
..type = pb.Message_Type.SENDER_DELIVERY_RECEIPT
..receiptId = receiptId;
await RustApi.sendTextMessage(
userId: target.realUserId,
body: msg.writeToBuffer(),
);
}
Future<void> sendReaction(
TestClient target,
String targetMessageId,
String emoji, {
bool remove = false,
}) async {
final content = pb.EncryptedContent()
..groupId = defaultGroup!.groupId
..reaction = (pb.EncryptedContent_Reaction()
..targetMessageId = targetMessageId
..emoji = emoji
..remove = remove);
await sendEncryptedContent(target, content);
}
Future<void> sendMessageUpdate(
TestClient target,
String targetMessageId,
pb.EncryptedContent_MessageUpdate_Type type, {
String? text,
}) async {
final update = pb.EncryptedContent_MessageUpdate()
..type = type
..senderMessageId = targetMessageId
..timestamp = Int64(DateTime.now().millisecondsSinceEpoch);
if (text != null) update.text = text;
final content = pb.EncryptedContent()
..groupId = defaultGroup!.groupId
..messageUpdate = update;
await sendEncryptedContent(target, content);
}
Future<void> sendMedia(
TestClient target,
String senderMessageId,
pb.EncryptedContent_Media_Type type,
) async {
final content = pb.EncryptedContent()
..groupId = defaultGroup!.groupId
..media = (pb.EncryptedContent_Media()
..senderMessageId = senderMessageId
..type = type
..requiresAuthentication = false
..timestamp = Int64(DateTime.now().millisecondsSinceEpoch));
await sendEncryptedContent(target, content);
}
Future<Message> expectMessage(bool Function(Message) predicate) async {
for (var i = 0; i < 500; i++) {
final msg = await run(() async {
final msgs = await env.db.select(env.db.messages).get();
return msgs.firstWhereOrNull(predicate);
});
if (msg != null) return msg;
await Future.delayed(const Duration(milliseconds: 10));
}
throw Exception('Message matching predicate not received');
}
Future<Reaction> expectReaction(String messageId, String emoji) async {
for (var i = 0; i < 500; i++) {
final reaction = await run(() async {
final reactions = await (env.db.select(
env.db.reactions,
)..where((t) => t.messageId.equals(messageId))).get();
return reactions.firstWhereOrNull((r) => r.emoji == emoji);
});
if (reaction != null) return reaction;
await Future.delayed(const Duration(milliseconds: 10));
}
throw Exception('Reaction $emoji not received on message $messageId');
}
}