import 'dart:async'; import 'dart:io'; import 'package:enough_mail/enough_mail.dart' as imap; import 'package:flutter_test/flutter_test.dart'; import 'package:sharedinbox/core/filter/filter_expression.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/repositories/sync_log_repository.dart'; import 'package:sharedinbox/core/sync/account_sync_manager.dart'; Future _fakeImapConnect( Account account, String username, String password, ) async => throw const SocketException('fake — no real IMAP server in tests'); void main() { test( 'AccountSyncManager schedules IMAP sync for multiple accounts', () async { final accounts = _FakeAccounts('pw'); final mailboxes = _FakeMailboxes(); final emails = _FakeEmails(); final logs = _FakeLogs(); final manager = AccountSyncManager( accounts, mailboxes, emails, syncLog: logs, imapConnect: _fakeImapConnect, ); final a1 = _account('1'); final a2 = _account('2'); manager.start(); accounts.push([a1, a2]); // Allow some time for listeners to fire. await Future.delayed(const Duration(milliseconds: 100)); expect(emails.syncCounts['1'], greaterThanOrEqualTo(1)); expect(emails.syncCounts['2'], greaterThanOrEqualTo(1)); manager.dispose(); }, ); test( 'AccountSyncManager schedules JMAP sync for multiple accounts', () async { final accounts = _FakeAccounts('pw'); final mailboxes = _FakeMailboxes(); final emails = _FakeEmails(); final logs = _FakeLogs(); final manager = AccountSyncManager( accounts, mailboxes, emails, syncLog: logs, ); final a1 = _jmapAccount('1'); final a2 = _jmapAccount('2'); manager.start(); accounts.push([a1, a2]); await Future.delayed(const Duration(milliseconds: 100)); expect(emails.syncCounts['1'], greaterThanOrEqualTo(1)); expect(emails.syncCounts['2'], greaterThanOrEqualTo(1)); manager.dispose(); }, ); } Account _account(String id) => Account( id: id, displayName: 'Account $id', email: '$id@example.com', imapHost: 'localhost', imapPort: 143, imapSsl: false, smtpHost: 'localhost', smtpPort: 25, smtpSsl: false, ); Account _jmapAccount(String id) => Account( id: id, displayName: 'Account $id', email: '$id@example.com', type: AccountType.jmap, jmapUrl: 'http://localhost:8080/.well-known/jmap', smtpHost: 'localhost', smtpPort: 25, smtpSsl: false, ); class _FakeAccounts implements AccountRepository { _FakeAccounts(this.password); final String password; final _ctrl = StreamController>.broadcast(); @override Stream> observeAccounts() => _ctrl.stream; @override Future getAccount(String id) async => null; @override Future getPassword(String accountId) async => password; @override Future addAccount(Account a, String p) async {} @override Future removeAccount(String id) async {} @override Future updateAccount(Account a, {String? password}) async {} void push(List accounts) => _ctrl.add(accounts); } class _FakeMailboxes implements MailboxRepository { @override Stream> observeMailboxes(String? accountId) => Stream.value([ Mailbox( id: '$accountId:INBOX', accountId: accountId ?? '', path: 'INBOX', name: 'INBOX', unreadCount: 0, totalCount: 0, role: 'inbox', ), ]); @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 { final syncCounts = {}; @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 { syncCounts[a] = (syncCounts[a] ?? 0) + 1; return SyncEmailsResult.zero; } @override Future setFlag(String id, {bool? seen, bool? flagged}) async {} @override Future markAllAsRead(String accountId, String mailboxPath) async {} @override Future moveEmail(String id, String dest) async {} @override Future cancelPendingChange(String id, String type) async => false; @override Future snoozeEmail(String emailId, DateTime until) async {} @override Future wakeUpEmails(String accountId) async => 0; @override Future restoreEmails(List emails) async {} @override Future findEmailByMessageId( String accountId, String messageId, ) async => null; @override Future deleteEmail(String id) async => null; @override Stream get onChangesQueued => const Stream.empty(); @override Future flushPendingChanges(String accountId, String password) async => 0; @override Future sendEmail(String a, EmailDraft d) async {} @override Future downloadAttachment( String emailId, EmailAttachment attachment, ) async => '/tmp/${attachment.filename}'; @override Future fetchRawRfc822(String emailId) async => ''; @override Future> searchEmails(String a, String m, String q) async => []; @override Future> searchEmailsGlobal(String? a, String q) async => []; @override Future> searchEmailsStructured( String? a, FilterGroup f, ) async => []; @override Future> getEmailsByAddress(String? a, String address) async => []; @override Future> searchAddresses( String? a, String q, { int limit = 10, }) async => []; @override Stream watchJmapPush(String accountId, String password) => const Stream.empty(); @override Future verifySyncReliability( String accountId, String mailboxPath, ) async => ReliabilityResult.healthy; @override Stream> observeFailedMutations(String accountId) => Stream.value([]); @override Future discardMutation(int id) async {} @override Future retryMutation(int id) async {} @override Future clearForResync(String accountId) async {} @override Future applySieveRules(String accountId) async => 0; } class _FakeLogs implements SyncLogRepository { @override Future log({ required String accountId, required bool success, String? errorMessage, String? stackTrace, bool isPermanent = false, required String protocol, required int emailsFetched, required int emailsSkipped, required int mailboxesSynced, required int pendingFlushed, required int bytesTransferred, required DateTime startedAt, required DateTime finishedAt, List mailboxStats = const [], String? protocolLog, }) async {} @override Stream> observeSyncLogs(String accountId) => Stream.value([]); @override Stream observeLastError(String accountId) => Stream.value(null); }