Files
sharedinbox/test/integration/account_sync_manager_test.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 f0c1b0c2c0 feat(R2): add Force full sync button to account edit screen
Adds an escape hatch for when local DB falls out of sync with server:
- New clearForResync() on EmailRepository/MailboxRepository truncates
  local emails, mailboxes, pending changes and sync-state checkpoints.
  EmailBodies are preserved (FK disabled during delete) so viewed content
  is not re-downloaded after re-sync.
- AccountSyncManager.forceResync() orchestrates clear + loop restart.
- Edit account screen gets an "Force full sync" OutlinedButton with a
  confirmation dialog; shows spinner while the operation runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:53:41 +02:00

224 lines
5.9 KiB
Dart

import 'dart:async';
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/repositories/sync_log_repository.dart';
import 'package:sharedinbox/core/sync/account_sync_manager.dart';
void main() {
test('AccountSyncManager schedules 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 = _account('1');
final a2 = _account('2');
manager.start();
accounts.push([a1, a2]);
// Allow some time for listeners to fire.
await Future<void>.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,
);
class _FakeAccounts implements AccountRepository {
_FakeAccounts(this.password);
final String password;
final _ctrl = StreamController<List<Account>>.broadcast();
@override
Stream<List<Account>> observeAccounts() => _ctrl.stream;
@override
Future<Account?> getAccount(String id) async => null;
@override
Future<String> getPassword(String accountId) async => password;
@override
Future<void> addAccount(Account a, String p) async {}
@override
Future<void> removeAccount(String id) async {}
@override
Future<void> updateAccount(Account a, {String? password}) async {}
void push(List<Account> accounts) => _ctrl.add(accounts);
}
class _FakeMailboxes implements MailboxRepository {
@override
Stream<List<Mailbox>> observeMailboxes(String? accountId) => Stream.value([
Mailbox(
id: '$accountId:INBOX',
accountId: accountId ?? '',
path: 'INBOX',
name: 'INBOX',
unreadCount: 0,
totalCount: 0,
role: 'inbox',
),
]);
@override
Future<int> syncMailboxes(String accountId) async => 0;
@override
Future<Mailbox?> findMailboxByRole(String accountId, String role) async =>
null;
@override
Future<void> clearForResync(String accountId) async {}
}
class _FakeEmails implements EmailRepository {
final syncCounts = <String, int>{};
@override
Stream<List<Email>> observeEmails(String a, String m) => Stream.value([]);
@override
Stream<List<EmailThread>> observeThreads(String a, String m) =>
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 {
syncCounts[a] = (syncCounts[a] ?? 0) + 1;
return SyncEmailsResult.zero;
}
@override
Future<void> setFlag(String id, {bool? seen, bool? flagged}) 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<String?> deleteEmail(String id) async => null;
@override
Stream<String> get onChangesQueued => const Stream.empty();
@override
Future<int> flushPendingChanges(String accountId, String password) async => 0;
@override
Future<void> sendEmail(String a, EmailDraft d) async {}
@override
Future<String> downloadAttachment(
String emailId,
EmailAttachment attachment,
) async =>
'/tmp/${attachment.filename}';
@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
Stream<void> watchJmapPush(String accountId, String password) =>
const Stream.empty();
@override
Future<ReliabilityResult> verifySyncReliability(
String accountId,
String mailboxPath,
) async =>
ReliabilityResult.healthy;
@override
Stream<List<FailedMutation>> observeFailedMutations(String accountId) =>
Stream.value([]);
@override
Future<void> discardMutation(int id) async {}
@override
Future<void> retryMutation(int id) async {}
@override
Future<void> clearForResync(String accountId) async {}
}
class _FakeLogs implements SyncLogRepository {
@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 {}
@override
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
Stream.value([]);
}