213 lines
7.0 KiB
Dart
213 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));
|
|
});
|
|
});
|
|
}
|