210 lines
6.8 KiB
Dart
210 lines
6.8 KiB
Dart
import 'package:drift/drift.dart' show Value;
|
|
import 'package:enough_mail/enough_mail.dart' as imap;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:sharedinbox/core/models/account.dart';
|
|
import 'package:sharedinbox/core/repositories/email_repository.dart';
|
|
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 'account_repository_impl_test.dart' show MapSecureStorage;
|
|
import 'db_test_helper.dart';
|
|
|
|
// ── Contract ──────────────────────────────────────────────────────────────────
|
|
|
|
/// Verifies the observable / local-state portion of the [EmailRepository]
|
|
/// interface contract.
|
|
///
|
|
/// Network-dependent methods (syncEmails, sendEmail, etc.) are intentionally
|
|
/// excluded — they are covered by the concrete impl tests.
|
|
abstract class EmailRepositoryContract {
|
|
static const _account = Account(
|
|
id: 'er-acc',
|
|
displayName: 'Contract',
|
|
email: 'er@example.com',
|
|
imapHost: 'imap.example.com',
|
|
smtpHost: 'smtp.example.com',
|
|
);
|
|
|
|
/// Return a fresh [EmailRepository] with [_account] already persisted.
|
|
Future<EmailRepository> makeRepo();
|
|
|
|
/// Insert a raw email row so tests can assert on observable state without
|
|
/// triggering a network sync.
|
|
Future<void> insertEmail(
|
|
EmailRepository repo, {
|
|
required String id,
|
|
required String mailboxPath,
|
|
bool isSeen = true,
|
|
bool isFlagged = false,
|
|
DateTime? receivedAt,
|
|
});
|
|
|
|
void run() {
|
|
test('observeEmails starts empty', () async {
|
|
final repo = await makeRepo();
|
|
expect(await repo.observeEmails(_account.id, 'INBOX').first, isEmpty);
|
|
});
|
|
|
|
test('observeEmails emits inserted email', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(repo, id: 'er-acc:1', mailboxPath: 'INBOX');
|
|
final emails = await repo.observeEmails(_account.id, 'INBOX').first;
|
|
expect(emails, hasLength(1));
|
|
expect(emails.first.id, 'er-acc:1');
|
|
});
|
|
|
|
test('observeEmails only returns emails for the given mailbox', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(repo, id: 'er-acc:1', mailboxPath: 'INBOX');
|
|
expect(await repo.observeEmails(_account.id, 'Sent').first, isEmpty);
|
|
});
|
|
|
|
test('observeEmails orders by receivedAt descending', () async {
|
|
final repo = await makeRepo();
|
|
final older = DateTime(2024);
|
|
final newer = DateTime(2024, 6);
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:1',
|
|
mailboxPath: 'INBOX',
|
|
receivedAt: older,
|
|
);
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:2',
|
|
mailboxPath: 'INBOX',
|
|
receivedAt: newer,
|
|
);
|
|
final emails = await repo.observeEmails(_account.id, 'INBOX').first;
|
|
expect(emails.first.id, 'er-acc:2');
|
|
expect(emails.last.id, 'er-acc:1');
|
|
});
|
|
|
|
test('getEmail returns null for unknown id', () async {
|
|
final repo = await makeRepo();
|
|
expect(await repo.getEmail('no-such'), isNull);
|
|
});
|
|
|
|
test('getEmail returns inserted email', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(repo, id: 'er-acc:7', mailboxPath: 'INBOX');
|
|
final email = await repo.getEmail('er-acc:7');
|
|
expect(email, isNotNull);
|
|
expect(email!.accountId, _account.id);
|
|
});
|
|
|
|
test('setFlag seen updates isSeen', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:10',
|
|
mailboxPath: 'INBOX',
|
|
isSeen: false,
|
|
);
|
|
await repo.setFlag('er-acc:10', seen: true);
|
|
final email = await repo.getEmail('er-acc:10');
|
|
expect(email!.isSeen, isTrue);
|
|
});
|
|
|
|
test('setFlag flagged updates isFlagged', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(repo, id: 'er-acc:11', mailboxPath: 'INBOX');
|
|
await repo.setFlag('er-acc:11', flagged: true);
|
|
final email = await repo.getEmail('er-acc:11');
|
|
expect(email!.isFlagged, isTrue);
|
|
});
|
|
|
|
test('markAllAsRead marks every unread email in the mailbox', () async {
|
|
final repo = await makeRepo();
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:20',
|
|
mailboxPath: 'INBOX',
|
|
isSeen: false,
|
|
);
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:21',
|
|
mailboxPath: 'INBOX',
|
|
isSeen: false,
|
|
);
|
|
await insertEmail(
|
|
repo,
|
|
id: 'er-acc:22',
|
|
mailboxPath: 'Sent',
|
|
isSeen: false,
|
|
);
|
|
|
|
await repo.markAllAsRead(_account.id, 'INBOX');
|
|
|
|
expect((await repo.getEmail('er-acc:20'))!.isSeen, isTrue);
|
|
expect((await repo.getEmail('er-acc:21'))!.isSeen, isTrue);
|
|
// Email in a different mailbox should be untouched.
|
|
expect((await repo.getEmail('er-acc:22'))!.isSeen, isFalse);
|
|
});
|
|
|
|
test('observeThreads starts empty', () async {
|
|
final repo = await makeRepo();
|
|
expect(await repo.observeThreads(_account.id, 'INBOX').first, isEmpty);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Impl under test ───────────────────────────────────────────────────────────
|
|
|
|
class _EmailRepositoryImplContract extends EmailRepositoryContract {
|
|
static const _account = EmailRepositoryContract._account;
|
|
|
|
late AppDatabase _db;
|
|
late AccountRepositoryImpl _accountRepo;
|
|
|
|
@override
|
|
Future<EmailRepository> makeRepo() async {
|
|
_db = openTestDatabase();
|
|
_accountRepo = AccountRepositoryImpl(_db, MapSecureStorage());
|
|
await _accountRepo.addAccount(_account, 'pw');
|
|
return EmailRepositoryImpl(
|
|
_db,
|
|
_accountRepo,
|
|
imapConnect: (_, __, ___) => Future<imap.ImapClient>.error(
|
|
UnsupportedError('no IMAP in unit tests'),
|
|
),
|
|
smtpConnect: (_, __, ___) => Future<imap.SmtpClient>.error(
|
|
UnsupportedError('no SMTP in unit tests'),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> insertEmail(
|
|
EmailRepository repo, {
|
|
required String id,
|
|
required String mailboxPath,
|
|
bool isSeen = true,
|
|
bool isFlagged = false,
|
|
DateTime? receivedAt,
|
|
}) async {
|
|
await _db.into(_db.emails).insert(
|
|
EmailsCompanion.insert(
|
|
id: id,
|
|
accountId: _account.id,
|
|
mailboxPath: mailboxPath,
|
|
uid: int.parse(id.split(':').last),
|
|
receivedAt: receivedAt ?? DateTime.now(),
|
|
isSeen: Value(isSeen),
|
|
isFlagged: Value(isFlagged),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
setUpAll(configureSqliteForTests);
|
|
|
|
group('EmailRepositoryImpl satisfies EmailRepository contract', () {
|
|
_EmailRepositoryImplContract().run();
|
|
});
|
|
}
|