Files
sharedinbox/test/integration/sync_reliability_test.dart
T

211 lines
7.0 KiB
Dart

import 'dart:io';
import 'package:drift/drift.dart' show Value;
import 'package:enough_mail/enough_mail.dart';
import 'package:sharedinbox/core/models/account.dart' as model;
import 'package:sharedinbox/data/db/database.dart' hide Account;
import 'package:sharedinbox/data/repositories/account_repository_impl.dart';
import 'package:sharedinbox/data/repositories/email_repository_impl.dart';
import 'package:sharedinbox/data/repositories/mailbox_repository_impl.dart';
import 'package:test/test.dart';
import '../unit/account_repository_impl_test.dart' show MapSecureStorage;
import '../unit/db_test_helper.dart';
String _env(String key, [String fallback = '']) =>
Platform.environment[key] ?? fallback;
Future<ImapClient> _imapConnectPlain(
model.Account account,
String username,
String password,
) async {
final client = ImapClient(
defaultResponseTimeout: const Duration(seconds: 20),
);
await client.connectToServer(
account.imapHost,
account.imapPort,
isSecure: false,
);
await client.login(username, password);
return client;
}
void main() {
late String imapHost;
late int imapPort;
late String userEmail;
late String userPass;
late model.Account account;
late AppDatabase db;
late EmailRepositoryImpl repo;
late MapSecureStorage secureStorage;
setUpAll(() {
configureSqliteForTests();
imapHost = _env('STALWART_IMAP_HOST', '127.0.0.1');
imapPort = int.parse(_env('STALWART_IMAP_PORT', '1430'));
userEmail = _env('STALWART_USER_B', 'alice@example.com');
userPass = _env('STALWART_PASS_B', 'secret');
account = model.Account(
id: 'test',
displayName: 'Alice',
email: userEmail,
imapHost: imapHost,
imapPort: imapPort,
imapSsl: false,
smtpHost: '127.0.0.1',
smtpPort: 1025,
);
});
setUp(() async {
db = openTestDatabase();
secureStorage = MapSecureStorage();
final accounts = AccountRepositoryImpl(db, secureStorage);
await accounts.addAccount(account, userPass);
repo = EmailRepositoryImpl(db, accounts, imapConnect: _imapConnectPlain);
final client = await _imapConnectPlain(account, userEmail, userPass);
await client.selectMailboxByPath('INBOX');
final result = await client.uidSearchMessages(searchCriteria: 'ALL');
final uids = result.matchingSequence?.toList() ?? [];
if (uids.isNotEmpty) {
final seq = MessageSequence.fromIds(uids, isUid: true);
await client.uidMarkDeleted(seq);
await client.uidExpunge(seq);
}
await client.logout();
});
tearDown(() => db.close());
test('verifySyncReliability identifies missing local emails', () async {
// 1. Inject an email directly into the server via IMAP
final client = await _imapConnectPlain(account, userEmail, userPass);
await client.selectMailboxByPath('INBOX');
final builder = MessageBuilder()
..from = [const MailAddress('Sender', 'sender@example.com')]
..to = [MailAddress('Alice', userEmail)]
..subject = 'Ground Truth Test'
..text = 'Hello';
await client.appendMessage(
builder.buildMimeMessage(),
targetMailboxPath: 'INBOX',
);
await client.logout();
// 2. Verify reliability (local DB is empty)
final result = await repo.verifySyncReliability('test', 'INBOX');
expect(result.isHealthy, isFalse);
expect(result.missingLocally, hasLength(1));
expect(result.missingOnServer, isEmpty);
});
test(
'verifySyncReliability identifies extra local emails (missing on server)',
() async {
// 1. Manually insert a row into local DB that doesn't exist on server
await db.into(db.emails).insert(
EmailsCompanion.insert(
id: 'test:999',
accountId: 'test',
mailboxPath: 'INBOX',
uid: 999,
subject: const Value('Ghost'),
receivedAt: DateTime.now(),
),
);
// 2. Verify reliability
final result = await repo.verifySyncReliability('test', 'INBOX');
expect(result.isHealthy, isFalse);
expect(result.missingOnServer, contains('test:999'));
expect(result.missingLocally, isEmpty);
},
);
test('verifySyncReliability identifies flag mismatches', () async {
// 1. Sync one email
final client = await _imapConnectPlain(account, userEmail, userPass);
await client.selectMailboxByPath('INBOX');
await client.appendMessage(
(MessageBuilder()
..subject = 'Flag Test'
..text = 'Body')
.buildMimeMessage(),
targetMailboxPath: 'INBOX',
);
await client.logout();
await repo.syncEmails('test', 'INBOX');
final emails = await repo.observeEmails('test', 'INBOX').first;
expect(emails, hasLength(1));
final emailId = emails.first.id;
expect(emails.first.isSeen, isFalse);
// 2. Mark as seen on server only
final client2 = await _imapConnectPlain(account, userEmail, userPass);
await client2.selectMailboxByPath('INBOX');
await client2.uidMarkSeen(MessageSequence.fromAll());
await client2.logout();
// 3. Verify reliability
final result = await repo.verifySyncReliability('test', 'INBOX');
expect(result.isHealthy, isFalse);
expect(result.flagMismatches, hasLength(1));
expect(result.flagMismatches.first.id, emailId);
expect(result.flagMismatches.first.serverSeen, isTrue);
expect(result.flagMismatches.first.localSeen, isFalse);
});
group('JMAP Reliability', () {
late String stalwartUrl;
late model.Account jmapAccount;
setUp(() async {
stalwartUrl = _env('STALWART_URL', 'http://127.0.0.1:8080');
jmapAccount = model.Account(
id: 'test-jmap',
displayName: 'Alice JMAP',
email: userEmail,
type: model.AccountType.jmap,
jmapUrl: '$stalwartUrl/.well-known/jmap',
imapHost: imapHost,
imapPort: imapPort,
imapSsl: false,
smtpHost: imapHost,
smtpPort: 1025,
);
final accounts = AccountRepositoryImpl(db, secureStorage);
await accounts.addAccount(jmapAccount, userPass);
});
test('identifies missing local emails in JMAP', () async {
// 1. Inject via IMAP (Stalwart reflects it in JMAP)
final client = await _imapConnectPlain(account, userEmail, userPass);
await client.selectMailboxByPath('INBOX');
await client.appendMessage(
(MessageBuilder()..subject = 'JMAP Ground Truth').buildMimeMessage(),
targetMailboxPath: 'INBOX',
);
await client.logout();
// 2. Need to find the JMAP mailbox ID for INBOX
final mailboxRepo = MailboxRepositoryImpl(
db,
AccountRepositoryImpl(db, secureStorage),
);
await mailboxRepo.syncMailboxes('test-jmap');
final mailboxes = await mailboxRepo.observeMailboxes('test-jmap').first;
final inbox = mailboxes.firstWhere((m) => m.role == 'inbox');
// 3. Verify
final result = await repo.verifySyncReliability('test-jmap', inbox.path);
expect(result.isHealthy, isFalse);
expect(result.missingLocally, hasLength(1));
});
});
}