Files
IKAR/test/message_ranges_test.dart

354 lines
9.8 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:ikar/backend/modules/chats.dart';
import 'package:ikar/backend/modules/messages.dart';
import 'package:ikar/core/storage/app_database.dart';
import 'package:ikar/core/storage/message_ranges.dart';
import 'package:ikar/frontend/screens/chats/chat/chat_controller.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class _SyntheticPathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
final String directory;
_SyntheticPathProvider(this.directory);
@override
Future<String?> getApplicationSupportPath() async => directory;
}
const _accountId = 1;
const _chatId = 700001;
MessageRange _r(int start, int end) => MessageRange(start, end);
Map<String, dynamic> _row(int n) => CachedMessage(
id: '$n',
accountId: _accountId,
chatId: _chatId,
senderId: 5,
text: 'synthetic $n',
time: n * 10,
status: 'sent',
).toDbRow();
void main() {
group('coverage of a history fetch', () {
test('a full backward page covers from its oldest message to from', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: 500,
forward: 0,
backward: 3,
times: [470, 480, 490],
),
_r(470, 500),
);
});
test('a short backward page reaches the start of the chat', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: 500,
forward: 0,
backward: 30,
times: [470, 480],
),
_r(0, 500),
);
});
test('the latest page ends at its newest message, not in the future', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: null,
forward: 0,
backward: 3,
times: [900, 910, 920],
),
_r(900, 920),
);
});
test('an empty latest page records nothing', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: null,
forward: 0,
backward: 50,
times: const [],
),
isNull,
);
});
test('a window around a message spans both sides', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: 501,
forward: 2,
backward: 3,
times: [480, 490, 500, 510, 520],
),
_r(480, 520),
);
});
test('a forward page starts at from', () {
expect(
MessageRanges.coverageOfFetch(
fromTime: 500,
forward: 3,
backward: 0,
times: [510, 520],
),
_r(500, 520),
);
});
});
group('merging ranges', () {
test('touching ranges fuse, distant ones stay apart', () {
final merged = MessageRanges.merge([
_r(0, 100),
_r(300, 400),
], _r(100, 200));
expect(merged, [_r(0, 200), _r(300, 400)]);
});
test('a range bridging two others joins all three', () {
final merged = MessageRanges.merge([
_r(0, 100),
_r(300, 400),
], _r(50, 350));
expect(merged, [_r(0, 400)]);
});
});
group('trusting pages', () {
final ranges = MessageRanges([_r(100, 500), _r(800, 900)]);
test('an older page inside the range is trusted', () {
expect(
ranges.coversOlderPage(
edgeTime: 300,
pageTimes: [290, 280, 270],
limit: 3,
),
isTrue,
);
});
test('an older page crossing the start of the range is not', () {
expect(
ranges.coversOlderPage(
edgeTime: 120,
pageTimes: [110, 100, 60],
limit: 3,
),
isFalse,
);
expect(ranges.clipOlder([110, 100, 60], 120, (t) => t), [110, 100]);
});
test('a short older page is trusted only at the start of the chat', () {
expect(
ranges.coversOlderPage(edgeTime: 120, pageTimes: [110], limit: 3),
isFalse,
);
expect(
MessageRanges([
_r(0, 500),
]).coversOlderPage(edgeTime: 120, pageTimes: [110], limit: 3),
isTrue,
);
});
test('a newer page may not jump over the hole to the next range', () {
expect(
ranges.coversNewerPage(
edgeTime: 480,
pageTimes: [490, 500, 810],
limit: 3,
newestKnownTime: 900,
),
isFalse,
);
expect(ranges.clipNewer([490, 500, 810], 480, (t) => t), [490, 500]);
});
test('the latest page drops an island below the newest range', () {
expect(ranges.clipLatest([900, 850, 800, 500, 400], (t) => t), [
900,
850,
800,
]);
expect(ranges.coversNewest(900), isTrue);
expect(ranges.coversNewest(950), isFalse);
});
test('a window must sit inside one range and be complete', () {
expect(
ranges.coversWindow(
centerTime: 300,
windowTimes: [280, 290, 300, 310],
before: 3,
after: 1,
newestKnownTime: 900,
),
isTrue,
);
expect(
ranges.coversWindow(
centerTime: 490,
windowTimes: [480, 490, 500, 810],
before: 2,
after: 2,
newestKnownTime: 900,
),
isFalse,
);
});
});
group('stored ranges', () {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
final directory = Directory.systemTemp.createTempSync(
'synthetic_message_ranges_test',
);
PathProviderPlatform.instance = _SyntheticPathProvider(directory.path);
addTearDown(() async {
await AppDatabase.close();
if (directory.existsSync()) directory.deleteSync(recursive: true);
});
await AppDatabase.init();
await AppDatabase.saveProfile(
ProfileData(
id: _accountId,
firstName: 'Synthetic owner',
phone: 100000,
country: 'ZZ',
accountStatus: 0,
updateTime: 1,
),
);
await chats.cacheServerChat({
'id': _chatId,
'type': 'CHAT',
'status': 'ACTIVE',
'title': 'Synthetic group',
'participants': {'$_accountId': 0},
}, _accountId);
});
test('ranges merge as they are recorded', () async {
await AppDatabase.addMessageRange(_accountId, _chatId, _r(100, 200));
await AppDatabase.addMessageRange(_accountId, _chatId, _r(500, 600));
await AppDatabase.addMessageRange(_accountId, _chatId, _r(200, 300));
final ranges = await AppDatabase.loadMessageRanges(_accountId, _chatId);
expect(ranges.ranges, [_r(100, 300), _r(500, 600)]);
});
test(
'upgrading version 27 preserves existing messages and profile',
() async {
await AppDatabase.saveMessages([_row(1)]);
await AppDatabase.close();
final directory = await PathProviderPlatform.instance
.getApplicationSupportPath();
final db = await databaseFactoryFfi.openDatabase('$directory/qlyra.db');
await db.execute('DROP TABLE message_ranges');
await db.setVersion(27);
await db.close();
await AppDatabase.init();
expect(
await AppDatabase.loadMessage(_accountId, _chatId, '1'),
isNotNull,
);
expect(await AppDatabase.loadChat(_accountId, _chatId), isNotEmpty);
await AppDatabase.addMessageRange(_accountId, _chatId, _r(10, 20));
expect(
(await AppDatabase.loadMessageRanges(_accountId, _chatId)).ranges,
[_r(10, 20)],
);
},
);
test(
'a live message extends only a range that reached the last one',
() async {
await AppDatabase.addMessageRange(_accountId, _chatId, _r(100, 300));
await AppDatabase.extendMessageRange(
_accountId,
_chatId,
previousLastTime: 400,
time: 410,
);
expect(
(await AppDatabase.loadMessageRanges(_accountId, _chatId)).ranges,
[_r(100, 300)],
);
await AppDatabase.extendMessageRange(
_accountId,
_chatId,
previousLastTime: 300,
time: 310,
);
expect(
(await AppDatabase.loadMessageRanges(_accountId, _chatId)).ranges,
[_r(100, 310)],
);
},
);
test('clearing the history forgets its ranges', () async {
await AppDatabase.addMessageRange(_accountId, _chatId, _r(100, 300));
await AppDatabase.clearMessages(_accountId, _chatId);
expect(
(await AppDatabase.loadMessageRanges(_accountId, _chatId)).isEmpty,
isTrue,
);
});
test('scrolling up stops at a hole instead of joining an island', () async {
await AppDatabase.saveMessages([
for (var n = 1; n <= 20; n++) _row(n),
for (var n = 60; n <= 100; n++) _row(n),
]);
await AppDatabase.addMessageRange(_accountId, _chatId, _r(10, 200));
await AppDatabase.addMessageRange(_accountId, _chatId, _r(600, 1000));
final controller = ChatController()
..myId = _accountId
..chatId = _chatId;
addTearDown(controller.dispose);
final latest = await controller.loadInitialFromDb(onlyVisible: true);
controller.messages = latest.reversed.toList();
expect(controller.latestCovered, isTrue);
expect(controller.messages.first.time, 600);
expect(controller.messages.last.time, 1000);
await controller.loadMoreHistory(
onLoadingStarted: () {},
onLoaded: (_) {},
onError: (_) {},
pageSize: 30,
);
expect(controller.messages.first.time, 600);
expect(controller.messages.any((m) => m.time < 600), isFalse);
expect(controller.hasMoreHistory, isTrue);
});
});
}