// Tests for ReliabilityRunner.checkNow() — the manual "Verify sync health" // trigger. Specifically guards against regression of issue #95 where // checkNow() silently did nothing because it delegated to _runAll(), which // checked the _running flag (only true after start() is called). import 'package:flutter_test/flutter_test.dart'; import 'package:sharedinbox/core/models/account.dart'; import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/mailbox.dart'; import 'package:sharedinbox/core/repositories/account_repository.dart'; import 'package:sharedinbox/core/repositories/email_repository.dart'; import 'package:sharedinbox/core/repositories/mailbox_repository.dart'; import 'package:sharedinbox/core/sync/reliability_runner.dart'; import 'package:sharedinbox/data/db/database.dart' hide Account, Email, EmailBody; import 'db_test_helper.dart'; // --------------------------------------------------------------------------- // Minimal fakes // --------------------------------------------------------------------------- const _kAccount = Account( id: 'test-account', displayName: 'Test', email: 'test@example.com', imapHost: 'localhost', ); const _kMailbox = Mailbox( id: 'test-account:INBOX', accountId: 'test-account', path: 'INBOX', name: 'INBOX', unreadCount: 0, totalCount: 0, ); class _FakeAccounts implements AccountRepository { @override Stream> observeAccounts() => Stream.value([_kAccount]); @override Future getAccount(String id) async => _kAccount; @override Future addAccount(Account account, String password) async {} @override Future updateAccount(Account account, {String? password}) async {} @override Future removeAccount(String id) async {} @override Future getPassword(String id) async => 'secret'; } class _FakeMailboxes implements MailboxRepository { @override Stream> observeMailboxes(String? accountId) => Stream.value([_kMailbox]); @override Future syncMailboxes(String accountId) async => 0; @override Future findMailboxByRole(String accountId, String role) async => null; @override Future clearForResync(String accountId) async {} @override Future createMailboxWithRole( String accountId, String name, String role, ) async => Mailbox( id: '$accountId:$name', accountId: accountId, path: name, name: name, role: role, unreadCount: 0, totalCount: 0, ); @override Future createMailbox(String accountId, String name) async => Mailbox( id: '$accountId:$name', accountId: accountId, path: name, name: name, unreadCount: 0, totalCount: 0, ); } class _FakeEmails implements EmailRepository { int verifyCallCount = 0; @override Future verifySyncReliability( String accountId, String mailboxPath, ) async { verifyCallCount++; return ReliabilityResult.healthy; } // All remaining methods are unused by ReliabilityRunner. @override Stream> observeEmails(String a, String m, {int limit = 50}) => Stream.value([]); @override Stream> observeThreads( String a, String m, { int limit = 50, }) => Stream.value([]); @override Stream> observeAllInboxThreads({int limit = 50}) => Stream.value([]); @override Stream> observeEmailsInThread(String a, String m, String t) => Stream.value([]); @override Future getEmail(String id) async => null; @override Future getEmailBody(String id) async => const EmailBody(emailId: '', attachments: []); @override Future syncEmails(String a, String m) async => SyncEmailsResult.zero; @override Future setFlag(String id, {bool? seen, bool? flagged}) async {} @override Future markAllAsRead(String a, String m) async {} @override Future moveEmail(String id, String dest) async {} @override Future deleteEmail(String id) async => null; @override Future sendEmail(String a, EmailDraft d) async {} @override Future downloadAttachment(String id, EmailAttachment att) async => ''; @override Future fetchRawRfc822(String id) async => ''; @override Future> searchEmails(String a, String m, String q) async => []; @override Future> searchEmailsGlobal(String? a, String q) async => []; @override Future> getEmailsByAddress(String? a, String addr) async => []; @override Future> searchAddresses( String? a, String q, { int limit = 10, }) async => []; @override Stream> observeFailedMutations(String a) => Stream.value([]); @override Future discardMutation(int id) async {} @override Future retryMutation(int id) async {} @override Future cancelPendingChange(String id, String type) async => false; @override Future snoozeEmail(String id, DateTime until) async {} @override Future restoreEmails(List emails) async {} @override Future findEmailByMessageId(String a, String messageId) async => null; @override Stream get onChangesQueued => const Stream.empty(); @override Stream watchJmapPush(String a, String password) => const Stream.empty(); @override Future flushPendingChanges(String a, String password) async => 0; @override Future wakeUpEmails(String accountId) async => 0; @override Future clearForResync(String accountId) async {} @override Future applySieveRules(String accountId) async => 0; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { configureSqliteForTests(); group('ReliabilityRunner.checkNow()', () { late AppDatabase db; late _FakeEmails emails; late ReliabilityRunner runner; setUp(() { db = openTestDatabase(); emails = _FakeEmails(); runner = ReliabilityRunner(db, _FakeAccounts(), _FakeMailboxes(), emails); }); tearDown(() => db.close()); test('writes sync-health row even when start() was never called', () async { // Do NOT call runner.start() — this was the bug: checkNow() only ran // when _running was true, which required start() to have been called. await runner.checkNow(); final rows = await db.select(db.syncHealth).get(); expect(rows, hasLength(1), reason: 'checkNow() must write to the DB'); expect(rows.first.accountId, 'test-account'); expect(rows.first.isHealthy, isTrue); }); test('calls verifySyncReliability for each mailbox', () async { await runner.checkNow(); expect( emails.verifyCallCount, 1, reason: 'one mailbox → one verifySyncReliability call', ); }); test('also works when start() was called beforehand', () async { runner.start(); await runner.checkNow(); final rows = await db.select(db.syncHealth).get(); expect(rows, hasLength(1)); }); }); }