Files
sharedinbox/test/unit/account_sync_manager_test.dart
T

257 lines
7.5 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart' show MissingPluginException;
import 'package:mockito/annotations.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';
import 'package:test/test.dart';
@GenerateMocks([AccountRepository, MailboxRepository, EmailRepository])
import 'account_sync_manager_test.mocks.dart';
void main() {
late MockAccountRepository accounts;
late MockMailboxRepository mailboxes;
late MockEmailRepository emails;
late AccountSyncManager manager;
setUp(() {
accounts = MockAccountRepository();
mailboxes = MockMailboxRepository();
emails = MockEmailRepository();
manager = AccountSyncManager(accounts, mailboxes, emails);
});
test('syncNow kicks the active sync loop', () async {
// This is hard to test without real loops, but we can verify it doesn't crash.
manager.syncNow('unknown');
});
// Regression test for issue #200: when flutter_secure_storage throws
// MissingPluginException (channel unavailable on the device), the IMAP sync
// loop must stop permanently instead of retrying indefinitely with backoff.
test(
'MissingPluginException from secure storage stops IMAP sync loop permanently',
() async {
final syncLog = FakeSyncLogRepository();
final m = AccountSyncManager(
_AccountRepositoryWithMissingPlugin(),
FakeMailboxRepositoryWithInbox(),
FakeEmailRepository(),
syncLog: syncLog,
);
m.start();
// Allow the first sync cycle to run and fail.
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(syncLog.logs, hasLength(1));
expect(syncLog.logs.first.success, isFalse);
// Kicking the loop should have no effect once it has stopped permanently.
m.syncNow('1');
await Future<void>.delayed(const Duration(milliseconds: 100));
// Before the fix: kick triggers a retry → 2 log entries.
// After the fix: loop is permanently stopped → still exactly 1 entry.
expect(syncLog.logs, hasLength(1));
m.dispose();
});
}
class FakeEmailRepository implements EmailRepository {
@override
Stream<List<Email>> observeEmails(
String a,
String m, {
int limit = 50,
}) =>
Stream.value([]);
@override
Stream<List<EmailThread>> observeThreads(
String a,
String m, {
int limit = 50,
}) =>
Stream.value([]);
@override
Stream<List<Email>> observeEmailsInThread(String a, String m, String t) =>
Stream.value([]);
@override
Future<Email?> getEmail(String id) async => null;
@override
Future<EmailBody> getEmailBody(String id) async =>
const EmailBody(emailId: '', attachments: []);
@override
Future<SyncEmailsResult> syncEmails(String a, String m) async =>
SyncEmailsResult.zero;
@override
Future<void> setFlag(String id, {bool? seen, bool? flagged}) async {}
@override
Future<void> markAllAsRead(String accountId, String mailboxPath) async {}
@override
Future<void> moveEmail(String id, String dest) async {}
@override
Future<bool> cancelPendingChange(String id, String type) async => false;
@override
Future<void> snoozeEmail(String emailId, DateTime until) async {}
@override
Future<int> wakeUpEmails(String accountId) async => 0;
@override
Future<void> restoreEmails(List<Email> emails) async {}
@override
Future<Email?> findEmailByMessageId(
String accountId,
String messageId,
) async =>
null;
@override
Future<String?> deleteEmail(String id) async => null;
@override
Stream<String> get onChangesQueued => const Stream.empty();
@override
Future<int> flushPendingChanges(String a, String p) async => 0;
@override
Future<void> sendEmail(String a, EmailDraft d) async {}
@override
Future<String> downloadAttachment(String id, EmailAttachment a) async => '';
@override
Future<String> fetchRawRfc822(String emailId) async => '';
@override
Future<List<Email>> searchEmails(String a, String m, String q) async => [];
@override
Future<List<Email>> searchEmailsGlobal(String? a, String q) async => [];
@override
Future<List<Email>> getEmailsByAddress(String? a, String address) async => [];
@override
Future<List<EmailAddress>> searchAddresses(
String? a,
String q, {
int limit = 10,
}) async =>
[];
@override
Stream<void> watchJmapPush(String a, String p) => const Stream.empty();
@override
Stream<List<FailedMutation>> observeFailedMutations(String a) =>
Stream.value([]);
@override
Future<void> discardMutation(int id) async {}
@override
Future<void> retryMutation(int id) async {}
@override
Future<ReliabilityResult> verifySyncReliability(
String accountId,
String mailboxPath,
) async =>
ReliabilityResult.healthy;
@override
Future<void> clearForResync(String accountId) async {}
@override
Future<int> applySieveRules(String accountId) async => 0;
}
class _Log {
_Log({required this.success});
final bool success;
}
class FakeSyncLogRepository implements SyncLogRepository {
final logs = <_Log>[];
@override
Future<void> log({
required String accountId,
required bool success,
String? errorMessage,
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<MailboxSyncStats> mailboxStats = const [],
String? protocolLog,
}) async {
logs.add(_Log(success: success));
}
@override
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
Stream.value([]);
@override
Stream<String?> observeLastError(String accountId) => Stream.value(null);
}
class FakeMailboxRepositoryWithInbox implements MailboxRepository {
@override
Stream<List<Mailbox>> observeMailboxes(String? accountId) => Stream.value([
const Mailbox(
id: '1:INBOX',
accountId: '1',
path: 'INBOX',
name: 'INBOX',
unreadCount: 0,
totalCount: 0,
role: 'inbox',
),
]);
@override
Future<int> syncMailboxes(String id) async => 1;
@override
Future<Mailbox?> findMailboxByRole(String id, String role) async => null;
@override
Future<void> clearForResync(String accountId) async {}
}
class _AccountRepositoryWithMissingPlugin implements AccountRepository {
static const _account = Account(
id: '1',
displayName: 'Test',
email: 'test@example.com',
);
@override
Stream<List<Account>> observeAccounts() => Stream.value([_account]);
@override
Future<Account?> getAccount(String id) async => _account;
@override
Future<String> getPassword(String accountId) => Future.error(
MissingPluginException(
'No implementation found for method read on channel '
'plugins.it.nomads.com/flutter_secure_storage',
),
);
@override
Future<void> addAccount(Account account, String password) async {}
@override
Future<void> updateAccount(Account account, {String? password}) async {}
@override
Future<void> removeAccount(String id) async {}
}