Files
IKAR/test/first_dialog_message_test.dart

226 lines
6.7 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:ikar/backend/api.dart';
import 'package:ikar/backend/modules/chats.dart';
import 'package:ikar/backend/modules/messages.dart';
import 'package:ikar/backend/modules/outbox.dart';
import 'package:ikar/backend/modules/outgoing_text.dart';
import 'package:ikar/core/protocol/opcode_map.dart';
import 'package:ikar/core/protocol/packet.dart';
import 'package:ikar/core/storage/app_database.dart';
import 'package:ikar/core/storage/token_storage.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class _Paths extends PathProviderPlatform with MockPlatformInterfaceMixin {
_Paths(this.directory);
final String directory;
@override
Future<String?> getApplicationSupportPath() async => directory;
}
class _Api extends Api {
final requests = <Map<dynamic, dynamic>>[];
@override
SessionState get state => SessionState.online;
@override
Future<Packet> sendRequest(
int opcode,
Map<dynamic, dynamic> payload, {
bool silent = false,
void Function()? beforeSend,
}) async {
beforeSend?.call();
expect(opcode, Opcode.msgSend);
requests.add(payload);
return Packet(
cmd: CmdType.ok,
opcode: opcode,
payload: {
'message': {'id': 702},
},
);
}
}
const _account = 42;
const _peer = 17;
const _chat = _account ^ _peer;
CachedMessage _text({int chatId = _chat}) => OutgoingText.prepare(
accountId: _account,
chatId: chatId,
wireText: 'synthetic prepared ciphertext',
encrypted: true,
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
final directory = Directory.systemTemp.createTempSync('ikar_first_dialog_');
final paths = PathProviderPlatform.instance;
PathProviderPlatform.instance = _Paths(directory.path);
SharedPreferences.setMockInitialValues({});
await AppDatabase.init();
await AppDatabase.saveProfile(ProfileData.stub(_account));
await TokenStorage.setActiveAccount(_account);
addTearDown(() async {
await OutboxService.instance.dispose();
await AppDatabase.close();
PathProviderPlatform.instance = paths;
directory.deleteSync(recursive: true);
});
});
test(
'first dialog text survives foreign keys and is sent from the outbox',
() async {
final message = _text();
// This is the old first-message path: no server chat card exists yet.
await expectLater(
AppDatabase.saveMessages([message.toDbRow()]),
throwsA(isA<DatabaseException>()),
);
final api = _Api();
addTearDown(api.dispose);
var authenticated = false;
OutboxService.instance.init(
api,
MessagesModule(api),
isAuthenticated: () => authenticated,
);
await OutboxService.instance.enqueue(
message,
createDialog: true,
dialogTitle: 'Synthetic peer',
);
expect(
(await AppDatabase.loadPendingMessages(_account)).single['id'],
message.id,
);
expect(
await AppDatabase.findDialogChatByParticipant(_account, _peer),
_chat,
);
authenticated = true;
await OutboxService.instance.flush();
expect(api.requests.single['chatId'], _chat);
expect((api.requests.single['message'] as Map)['text'], message.text);
expect(
(api.requests.single['message'] as Map)['cid'],
OutgoingText.metadata(message)!['cid'],
);
expect(await AppDatabase.loadPendingMessages(_account), isEmpty);
final sent = await AppDatabase.loadMessage(_account, _chat, '702');
expect(sent?['status'], 'sent');
expect(sent?['text'], message.text);
await Future<void>.delayed(Duration.zero);
},
);
test('saved messages can start with chat zero and one participant', () async {
await AppDatabase.saveOutgoingMessage(
_text(chatId: 0).toDbRow(),
createDialog: true,
);
final row = (await AppDatabase.loadChat(_account, 0)).single;
expect(CachedChat.fromDbRow(row).participants, {_account: 0});
expect(
await AppDatabase.findDialogChatByParticipant(_account, _account),
0,
);
});
test('first attachment persists after preparing its dialog', () async {
await AppDatabase.ensureDialogCached(_account, _chat);
final message = CachedMessage(
id: 'synthetic_sticker',
accountId: _account,
chatId: _chat,
senderId: _account,
time: 1000,
status: 'sent',
payload: {
'attaches': [
{'_type': 'STICKER', 'stickerId': 101},
],
},
);
await AppDatabase.saveMessages([message.toDbRow()]);
expect(
await AppDatabase.loadMessage(_account, _chat, message.id),
isNotNull,
);
});
test(
'draft creation preserves an existing server chat and its messages',
() async {
await chats.cacheServerChat(
{
'id': _chat,
'type': 'DIALOG',
'title': 'Server title',
'participants': {'$_account': 123, '$_peer': 456},
},
_account,
inList: false,
);
final before = (await AppDatabase.loadChat(_account, _chat)).single;
await AppDatabase.ensureDialogCached(
_account,
_chat,
title: 'Draft title',
);
await AppDatabase.saveOutgoingMessage(
_text().toDbRow(),
createDialog: true,
);
expect((await AppDatabase.loadChat(_account, _chat)).single, before);
},
);
test(
'failed first message rolls back its dialog and participant index',
() async {
final invalid = _text().toDbRow()..remove('sender_id');
await expectLater(
AppDatabase.saveOutgoingMessage(invalid, createDialog: true),
throwsA(isA<DatabaseException>()),
);
expect(await AppDatabase.loadChat(_account, _chat), isEmpty);
expect(
await AppDatabase.findDialogChatByParticipant(_account, _peer),
isNull,
);
expect(await AppDatabase.loadPendingMessages(_account), isEmpty);
},
);
test('failed replacement retains the original pending message', () async {
final original = _text();
await AppDatabase.saveOutgoingMessage(
original.toDbRow(),
createDialog: true,
);
final invalid = original.toDbRow()
..['id'] = 'synthetic_ack'
..remove('sender_id');
await expectLater(
AppDatabase.saveOutgoingMessage(invalid, removeId: original.id),
throwsA(isA<DatabaseException>()),
);
expect(
await AppDatabase.loadMessage(_account, _chat, original.id),
isNotNull,
);
});
}