304 lines
9.1 KiB
Dart
304 lines
9.1 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:drift/drift.dart' show Value;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:sharedinbox/core/models/account.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';
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
const _account = Account(
|
|
id: 'sieve-acc',
|
|
displayName: 'Sieve Test',
|
|
email: 'sieve@example.com',
|
|
imapHost: 'imap.example.com',
|
|
smtpHost: 'smtp.example.com',
|
|
);
|
|
|
|
Future<(AppDatabase, EmailRepositoryImpl)> _makeSetup() async {
|
|
final db = openTestDatabase();
|
|
final storage = MapSecureStorage();
|
|
final accounts = AccountRepositoryImpl(db, storage);
|
|
await accounts.addAccount(_account, 'password');
|
|
|
|
final repo = EmailRepositoryImpl(db, accounts);
|
|
return (db, repo);
|
|
}
|
|
|
|
/// Inserts a minimal email row in the INBOX. Returns the row id.
|
|
Future<String> _insertInboxEmail(
|
|
AppDatabase db, {
|
|
required String id,
|
|
required String messageId,
|
|
String subject = 'Test',
|
|
String from = 'sender@example.com',
|
|
String mailboxPath = 'INBOX',
|
|
}) async {
|
|
await db.into(db.emails).insert(
|
|
EmailsCompanion.insert(
|
|
id: id,
|
|
accountId: _account.id,
|
|
mailboxPath: mailboxPath,
|
|
uid: int.parse(id.split(':').last),
|
|
subject: Value(subject),
|
|
receivedAt: DateTime.now(),
|
|
fromJson: Value(
|
|
jsonEncode([
|
|
{'name': '', 'email': from},
|
|
]),
|
|
),
|
|
messageId: Value(messageId),
|
|
),
|
|
);
|
|
// Insert a thread row so _updateThread does not throw.
|
|
await db.into(db.threads).insertOnConflictUpdate(
|
|
ThreadsCompanion.insert(
|
|
id: id,
|
|
accountId: _account.id,
|
|
mailboxPath: mailboxPath,
|
|
latestDate: DateTime.now(),
|
|
latestEmailId: id,
|
|
),
|
|
);
|
|
return id;
|
|
}
|
|
|
|
/// Creates an active Sieve script for the test account.
|
|
Future<void> _insertSieveScript(AppDatabase db, String content) async {
|
|
await db.into(db.localSieveScripts).insert(
|
|
LocalSieveScriptsCompanion.insert(
|
|
accountId: _account.id,
|
|
name: 'test-script',
|
|
content: Value(content),
|
|
isActive: const Value(true),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
void main() {
|
|
setUpAll(configureSqliteForTests);
|
|
|
|
group('applySieveRules', () {
|
|
test('returns 0 when no active script exists', () async {
|
|
final (_, repo) = await _makeSetup();
|
|
expect(await repo.applySieveRules(_account.id), 0);
|
|
});
|
|
|
|
test('returns 0 when script has no matching rules', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["NEVER_MATCHES_XYZ"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'Hello world',
|
|
);
|
|
expect(await repo.applySieveRules(_account.id), 0);
|
|
});
|
|
|
|
test('applies fileinto rule and queues a move pending change', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'THIS IS SPAM',
|
|
);
|
|
|
|
final count = await repo.applySieveRules(_account.id);
|
|
expect(count, 1);
|
|
|
|
final pending = await db.select(db.pendingChanges).get();
|
|
expect(pending, hasLength(1));
|
|
expect(pending.first.changeType, 'move');
|
|
final payload = jsonDecode(pending.first.payload) as Map<String, dynamic>;
|
|
expect(payload['dest'], 'Archive');
|
|
});
|
|
|
|
test('applies discard rule and queues a delete pending change', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
if header :contains "from" ["spam@evil.com"] {
|
|
discard;
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
from: 'spam@evil.com',
|
|
);
|
|
|
|
final count = await repo.applySieveRules(_account.id);
|
|
expect(count, 1);
|
|
|
|
final pending = await db.select(db.pendingChanges).get();
|
|
expect(pending, hasLength(1));
|
|
expect(pending.first.changeType, 'delete');
|
|
});
|
|
|
|
test('records email in LocalSieveApplied after processing', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'SPAM email',
|
|
);
|
|
|
|
await repo.applySieveRules(_account.id);
|
|
|
|
final applied = await db.select(db.localSieveApplied).get();
|
|
expect(applied, hasLength(1));
|
|
expect(applied.first.messageId, '<msg1@test>');
|
|
expect(applied.first.accountId, _account.id);
|
|
});
|
|
|
|
test('does not reprocess an email already in LocalSieveApplied', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'SPAM email',
|
|
);
|
|
|
|
// First run applies the rule.
|
|
expect(await repo.applySieveRules(_account.id), 1);
|
|
|
|
// Restore the email to INBOX to simulate a re-sync (e.g. second device
|
|
// moved it back). The LocalSieveApplied record prevents reprocessing.
|
|
await (db.update(db.emails)..where((t) => t.id.equals('sieve-acc:1')))
|
|
.write(const EmailsCompanion(mailboxPath: Value('INBOX')));
|
|
|
|
// Second run must not produce another pending change.
|
|
expect(await repo.applySieveRules(_account.id), 0);
|
|
final pending = await db.select(db.pendingChanges).get();
|
|
// Only the original move from the first run; no duplicates.
|
|
expect(pending, hasLength(1));
|
|
});
|
|
|
|
test('skips emails with no messageId', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
// Insert without messageId.
|
|
await db.into(db.emails).insert(
|
|
EmailsCompanion.insert(
|
|
id: 'sieve-acc:2',
|
|
accountId: _account.id,
|
|
mailboxPath: 'INBOX',
|
|
uid: 2,
|
|
subject: const Value('SPAM without id'),
|
|
receivedAt: DateTime.now(),
|
|
),
|
|
);
|
|
await db.into(db.threads).insertOnConflictUpdate(
|
|
ThreadsCompanion.insert(
|
|
id: 'sieve-acc:2',
|
|
accountId: _account.id,
|
|
mailboxPath: 'INBOX',
|
|
latestDate: DateTime.now(),
|
|
latestEmailId: 'sieve-acc:2',
|
|
),
|
|
);
|
|
|
|
expect(await repo.applySieveRules(_account.id), 0);
|
|
expect(await db.select(db.pendingChanges).get(), isEmpty);
|
|
});
|
|
|
|
test('emails not in INBOX are ignored', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'SPAM in Sent',
|
|
mailboxPath: 'Sent',
|
|
);
|
|
|
|
expect(await repo.applySieveRules(_account.id), 0);
|
|
expect(await db.select(db.pendingChanges).get(), isEmpty);
|
|
});
|
|
|
|
test('processes multiple emails independently', () async {
|
|
final (db, repo) = await _makeSetup();
|
|
await _insertSieveScript(db, '''
|
|
require ["fileinto"];
|
|
if header :contains "subject" ["SPAM"] {
|
|
fileinto "Archive";
|
|
}
|
|
''');
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:1',
|
|
messageId: '<msg1@test>',
|
|
subject: 'SPAM',
|
|
);
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:2',
|
|
messageId: '<msg2@test>',
|
|
subject: 'Hello',
|
|
);
|
|
await _insertInboxEmail(
|
|
db,
|
|
id: 'sieve-acc:3',
|
|
messageId: '<msg3@test>',
|
|
subject: 'More SPAM',
|
|
);
|
|
|
|
final count = await repo.applySieveRules(_account.id);
|
|
expect(count, 2);
|
|
|
|
final applied = await db.select(db.localSieveApplied).get();
|
|
// All three emails should be in LocalSieveApplied.
|
|
expect(applied, hasLength(3));
|
|
|
|
final pending = await db.select(db.pendingChanges).get();
|
|
expect(pending, hasLength(2));
|
|
});
|
|
});
|
|
}
|