fix: resolve dart analyze failures in chaos_monkey_test.dart (#458)
## Summary Fixes CI failures introduced by PR #455 (chaos monkey backend test). The `dart analyze --fatal-infos` step in CI was failing because `test/backend/chaos_monkey_test.dart` had: - **`avoid_print`** (5 instances): replaced `print(...)` with `stdout.writeln(...)` — `dart:io` is already imported - **`avoid_redundant_argument_values`**: removed redundant `''` from `_env('CHAOS_SEED', '')` since `''` is the parameter default - **`dart format`**: applied formatter fixes (trailing commas, line wrapping for long `connectToServer` calls) ## Verification ``` $ nix develop --command bash -c "fvm dart analyze --fatal-infos" Analyzing 456... No issues found! $ nix develop --command bash -c "fvm dart format --output=none --set-exit-if-changed test/backend/chaos_monkey_test.dart" Formatted 1 file (0 changed) in 0.01 seconds. ``` Closes #456 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de> Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/458
This commit was merged in pull request #458.
This commit is contained in:
committed by
guettli
co-authored by
guettli
Thomas SharedInbox
parent
985bac7022
commit
6a60c8d73b
@@ -0,0 +1,221 @@
|
||||
// Chaos monkey test — drives the email repository through random operations
|
||||
// against a live Stalwart instance to surface crashes and data-corruption bugs.
|
||||
//
|
||||
// Run via: stalwart-dev/test.sh
|
||||
//
|
||||
// Environment variables:
|
||||
// STALWART_IMAP_HOST, STALWART_IMAP_PORT
|
||||
// STALWART_SMTP_HOST, STALWART_SMTP_PORT
|
||||
// STALWART_USER_B / STALWART_PASS_B (alice@example.com)
|
||||
// CHAOS_ROUNDS (default: 30) — number of random operations to perform
|
||||
// CHAOS_SEED (default: current epoch ms) — seed for reproducibility
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:enough_mail/enough_mail.dart';
|
||||
import 'package:sharedinbox/core/models/account.dart';
|
||||
import 'package:sharedinbox/core/models/email.dart' as email_model;
|
||||
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 'package:test/test.dart';
|
||||
|
||||
import '../unit/account_repository_impl_test.dart' show MapSecureStorage;
|
||||
import '../unit/db_test_helper.dart';
|
||||
|
||||
String _env(String key, [String fallback = '']) =>
|
||||
Platform.environment[key] ?? fallback;
|
||||
|
||||
Future<ImapClient> _imapConnectPlain(
|
||||
Account account,
|
||||
String username,
|
||||
String password,
|
||||
) async {
|
||||
final client =
|
||||
ImapClient(defaultResponseTimeout: const Duration(seconds: 20));
|
||||
await client.connectToServer(
|
||||
account.imapHost,
|
||||
account.imapPort,
|
||||
isSecure: false,
|
||||
);
|
||||
await client.login(username, password);
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<SmtpClient> _smtpConnectPlain(
|
||||
Account account,
|
||||
String username,
|
||||
String password,
|
||||
) async {
|
||||
final atIndex = account.email.lastIndexOf('@');
|
||||
final domain =
|
||||
atIndex != -1 ? account.email.substring(atIndex + 1) : account.smtpHost;
|
||||
final client = SmtpClient(domain);
|
||||
await client.connectToServer(
|
||||
account.smtpHost,
|
||||
account.smtpPort,
|
||||
isSecure: false,
|
||||
);
|
||||
await client.ehlo();
|
||||
await client.authenticate(username, password);
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<void> _clearMailbox(
|
||||
Account account,
|
||||
String userEmail,
|
||||
String userPass,
|
||||
String mailboxPath,
|
||||
) async {
|
||||
final client = await _imapConnectPlain(account, userEmail, userPass);
|
||||
try {
|
||||
final box = await client.selectMailboxByPath(mailboxPath);
|
||||
if (box.messagesExists == 0) return;
|
||||
final result = await client.uidSearchMessages(searchCriteria: 'ALL');
|
||||
final uids = result.matchingSequence?.toList() ?? [];
|
||||
if (uids.isEmpty) return;
|
||||
final seq = MessageSequence.fromIds(uids, isUid: true);
|
||||
await client.uidMarkDeleted(seq);
|
||||
await client.uidExpunge(seq);
|
||||
} finally {
|
||||
await client.logout();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late String imapHost;
|
||||
late int imapPort;
|
||||
late String smtpHost;
|
||||
late int smtpPort;
|
||||
late String userEmail;
|
||||
late String userPass;
|
||||
late Account account;
|
||||
late AppDatabase db;
|
||||
late EmailRepositoryImpl emails;
|
||||
|
||||
setUpAll(configureSqliteForTests);
|
||||
|
||||
setUp(() async {
|
||||
imapHost = _env('STALWART_IMAP_HOST', '127.0.0.1');
|
||||
imapPort = int.parse(_env('STALWART_IMAP_PORT', '1430'));
|
||||
smtpHost = _env('STALWART_SMTP_HOST', '127.0.0.1');
|
||||
smtpPort = int.parse(_env('STALWART_SMTP_PORT', '1025'));
|
||||
userEmail = _env('STALWART_USER_B', 'alice@example.com');
|
||||
userPass = _env('STALWART_PASS_B', 'secret');
|
||||
|
||||
account = Account(
|
||||
id: 'chaos',
|
||||
displayName: 'Chaos',
|
||||
email: userEmail,
|
||||
imapHost: imapHost,
|
||||
imapPort: imapPort,
|
||||
imapSsl: false,
|
||||
smtpHost: smtpHost,
|
||||
smtpPort: smtpPort,
|
||||
);
|
||||
|
||||
db = openTestDatabase();
|
||||
final secureStorage = MapSecureStorage();
|
||||
final accounts = AccountRepositoryImpl(db, secureStorage);
|
||||
await accounts.addAccount(account, userPass);
|
||||
emails = EmailRepositoryImpl(
|
||||
db,
|
||||
accounts,
|
||||
imapConnect: _imapConnectPlain,
|
||||
smtpConnect: _smtpConnectPlain,
|
||||
);
|
||||
|
||||
await _clearMailbox(account, userEmail, userPass, 'INBOX');
|
||||
});
|
||||
|
||||
tearDown(() => db.close());
|
||||
|
||||
test('chaos monkey — random operations do not crash the repository',
|
||||
() async {
|
||||
final seedStr = _env('CHAOS_SEED');
|
||||
final seed = seedStr.isEmpty
|
||||
? DateTime.now().millisecondsSinceEpoch
|
||||
: int.parse(seedStr);
|
||||
final rounds = int.parse(_env('CHAOS_ROUNDS', '30'));
|
||||
final rng = Random(seed);
|
||||
|
||||
stdout.writeln('chaos-monkey: seed=$seed rounds=$rounds');
|
||||
|
||||
// Seed INBOX with a few messages so early rounds have something to act on.
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await emails.sendEmail(
|
||||
account.id,
|
||||
email_model.EmailDraft(
|
||||
from: email_model.EmailAddress(name: 'Chaos', email: userEmail),
|
||||
to: [email_model.EmailAddress(email: userEmail)],
|
||||
cc: [],
|
||||
subject: 'seed-$i',
|
||||
body: 'Seed email $i.',
|
||||
),
|
||||
);
|
||||
}
|
||||
await emails.syncEmails(account.id, 'INBOX');
|
||||
|
||||
for (var round = 0; round < rounds; round++) {
|
||||
final action = rng.nextInt(8);
|
||||
stdout.writeln('chaos-monkey: round=$round action=$action');
|
||||
|
||||
switch (action) {
|
||||
case 0: // sync INBOX
|
||||
await emails.syncEmails(account.id, 'INBOX');
|
||||
|
||||
case 1: // sync Sent
|
||||
await emails.syncEmails(account.id, 'Sent');
|
||||
|
||||
case 2: // send email to self
|
||||
final subject = 'chaos-$round-${rng.nextInt(9999)}';
|
||||
await emails.sendEmail(
|
||||
account.id,
|
||||
email_model.EmailDraft(
|
||||
from: email_model.EmailAddress(name: 'Chaos', email: userEmail),
|
||||
to: [email_model.EmailAddress(email: userEmail)],
|
||||
cc: [],
|
||||
subject: subject,
|
||||
body: 'Round $round. Value: ${rng.nextInt(1000000)}.',
|
||||
),
|
||||
);
|
||||
|
||||
case 3: // mark random email seen
|
||||
final inbox = await emails.observeEmails(account.id, 'INBOX').first;
|
||||
if (inbox.isEmpty) break;
|
||||
final e = inbox[rng.nextInt(inbox.length)];
|
||||
await emails.setFlag(e.id, seen: true);
|
||||
|
||||
case 4: // mark random email unseen
|
||||
final inbox = await emails.observeEmails(account.id, 'INBOX').first;
|
||||
if (inbox.isEmpty) break;
|
||||
final e = inbox[rng.nextInt(inbox.length)];
|
||||
await emails.setFlag(e.id, seen: false);
|
||||
|
||||
case 5: // toggle flagged on random email
|
||||
final inbox = await emails.observeEmails(account.id, 'INBOX').first;
|
||||
if (inbox.isEmpty) break;
|
||||
final e = inbox[rng.nextInt(inbox.length)];
|
||||
await emails.setFlag(e.id, flagged: !e.isFlagged);
|
||||
|
||||
case 6: // flush pending changes to server
|
||||
final flushed =
|
||||
await emails.flushPendingChanges(account.id, userPass);
|
||||
stdout.writeln('chaos-monkey: flushed $flushed pending changes');
|
||||
|
||||
case 7: // delete random email
|
||||
final inbox = await emails.observeEmails(account.id, 'INBOX').first;
|
||||
if (inbox.isEmpty) break;
|
||||
final e = inbox[rng.nextInt(inbox.length)];
|
||||
await emails.deleteEmail(e.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Final flush and sync to confirm the server is in a consistent state.
|
||||
final flushed = await emails.flushPendingChanges(account.id, userPass);
|
||||
stdout.writeln('chaos-monkey: final flush flushed=$flushed');
|
||||
final result = await emails.syncEmails(account.id, 'INBOX');
|
||||
stdout.writeln('chaos-monkey: final sync fetched=${result.fetched}');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user