226 lines
6.7 KiB
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,
|
|
);
|
|
});
|
|
}
|