fixed test.

This commit is contained in:
Thomas Güttler
2026-04-25 07:07:05 +02:00
parent 92a8a79952
commit 518eb5ccc8
3 changed files with 141 additions and 54 deletions
+15 -8
View File
@@ -18,14 +18,14 @@ tasks:
run: once
deps: [_nix-check]
cmds:
- cmd: fvm install --skip-pub-get
- cmd: scripts/silent_on_success.sh fvm install --skip-pub-get
_pub-get:
internal: true
run: once
deps: [_flutter-check]
cmds:
- fvm flutter pub get --suppress-analytics
- scripts/silent_on_success.sh fvm flutter pub get --suppress-analytics
_linux-deps-check:
internal: true
@@ -33,7 +33,7 @@ tasks:
preconditions:
- sh: command -v clang >/dev/null 2>&1
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
- sh: pkg-config --exists gtk+-3.0 2>/dev/null
- sh: /usr/bin/pkg-config --exists gtk+-3.0 2>/dev/null
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
install-hooks:
@@ -41,6 +41,13 @@ tasks:
cmds:
- pre-commit install
_codegen:
internal: true
run: once
deps: [_pub-get]
cmds:
- scripts/silent_on_success.sh fvm flutter pub run build_runner build --delete-conflicting-outputs
codegen:
desc: Generate Drift DB code (run after any schema change)
deps: [_nix-check, _pub-get]
@@ -49,7 +56,7 @@ tasks:
analyze:
desc: Static analysis (flutter analyze)
deps: [_nix-check, _pub-get]
deps: [_nix-check, _codegen]
cmds:
- scripts/run_analyze.sh
@@ -67,13 +74,13 @@ tasks:
test:
desc: Unit tests + coverage gate (fails if any non-excluded lib/ file is missing)
deps: [_nix-check, _pub-get]
deps: [_nix-check, _codegen]
cmds:
- scripts/run_unit_tests.sh
test-widget:
desc: Widget tests — headless, no display or network required
deps: [_nix-check, _pub-get]
deps: [_nix-check, _codegen]
cmds:
- scripts/run_widget_tests.sh
@@ -97,9 +104,9 @@ tasks:
build-linux:
desc: Build the Linux desktop app (debug)
deps: [_nix-check, _linux-deps-check, _pub-get]
deps: [_nix-check, _linux-deps-check, _codegen]
cmds:
- fvm flutter build linux --debug --no-pub
- scripts/silent_on_success.sh fvm flutter build linux --debug --no-pub
_android-sdk-check:
internal: true
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Run a command silently. On failure, replay captured output and exit with the original code.
# When DEBUG is set, print start time, command, end time, and duration.
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
if [ -n "${DEBUG:-}" ]; then
start=$(date +%s)
echo "[$(date '+%H:%M:%S')] + $*"
fi
if "$@" > "$tmp" 2>&1; then
code=0
else
code=$?
cat "$tmp"
fi
if [ -n "${DEBUG:-}" ]; then
end=$(date +%s)
echo "[$(date '+%H:%M:%S')] = $* (${code}) $((end - start))s"
fi
exit $code
+85 -29
View File
@@ -7,7 +7,8 @@
import 'dart:async';
import 'dart:io';
import 'package:enough_mail/enough_mail.dart' show ImapClient;
import 'package:enough_mail/enough_mail.dart'
show ImapClient, SmtpClient, MessageBuilder, MailAddress;
import 'package:flutter_test/flutter_test.dart';
import 'package:sharedinbox/core/models/account.dart';
@@ -129,19 +130,25 @@ class _FakeEmails implements EmailRepository {
Future<void> retryMutation(int id) async {}
}
// Plain (non-TLS) IMAP connect for the local dev Stalwart, which has no TLS.
// Production connectImap() rejects imapSsl:false, so tests inject this instead.
Future<ImapClient> _connectImapPlain(
Account account,
String username,
String password,
) async {
final client = ImapClient(
defaultResponseTimeout: const Duration(seconds: 20),
);
await client.connectToServer(account.imapHost, account.imapPort);
await client.login(username, password);
return client;
Future<void> _sendMessage({
required String host,
required int port,
required String from,
required String pass,
required String to,
required String subject,
}) async {
final smtp = SmtpClient('sharedinbox-test');
await smtp.connectToServer(host, port, isSecure: false);
await smtp.ehlo();
await smtp.authenticate(from, pass);
final builder = MessageBuilder()
..from = [MailAddress('', from)]
..to = [MailAddress('', to)]
..subject = subject
..text = 'IDLE wake-up test body';
await smtp.sendMessage(builder.buildMimeMessage());
await smtp.quit();
}
// ── Tests ─────────────────────────────────────────────────────────────────────
@@ -160,12 +167,42 @@ void main() {
pass = _env('STALWART_PASS_B');
});
test('IDLE connects, wakes on new message, and shuts down cleanly', () async {
final fakeAccounts = _FakeAccounts()..password = pass;
test(
'IDLE connects, wakes on new message, and shuts down cleanly',
timeout: const Timeout(Duration(seconds: 30)),
() async {
final firstIdleConnected = Completer<void>();
final secondIdleConnected = Completer<void>();
Object? connectError;
// Stalwart's memory directory authenticates by principal name ('alice'),
// not by email address ('alice@example.com'). connectImap() passes
// account.email as the IMAP login username, so use the bare name here.
Future<ImapClient> trackingConnect(
Account account,
String username,
String password,
) async {
try {
final client = ImapClient(
defaultResponseTimeout: const Duration(seconds: 20),
);
await client.connectToServer(
account.imapHost,
account.imapPort,
isSecure: false,
);
await client.login(username, password);
if (!firstIdleConnected.isCompleted) {
firstIdleConnected.complete();
} else if (!secondIdleConnected.isCompleted) {
secondIdleConnected.complete();
}
return client;
} catch (e) {
connectError ??= e;
rethrow;
}
}
final fakeAccounts = _FakeAccounts()..password = pass;
final account = Account(
id: 'integration-test',
displayName: 'Integration Test',
@@ -181,21 +218,40 @@ void main() {
fakeAccounts,
_FakeMailboxes(),
_FakeEmails(),
imapConnect: _connectImapPlain,
imapConnect: trackingConnect,
);
addTearDown(mgr.dispose);
mgr.start();
// Push the account — this triggers _sync() then _idle() in the background.
fakeAccounts.push([account]);
// Give the manager time to connect and enter IDLE.
await Future<void>.delayed(const Duration(seconds: 2));
// 1. IDLE connects
await firstIdleConnected.future.timeout(
const Duration(seconds: 5),
onTimeout: () => fail('IDLE did not connect within 5s; error: $connectError'),
);
expect(connectError, isNull, reason: 'IMAP connect should succeed');
// Shut down — stop() completes the _stopSignal completer so _idle() exits
// immediately without waiting for the 25-minute cap.
// 2. Wakes on new message — deliver a message and wait for IDLE to
// reconnect, which proves the manager woke up and re-entered IDLE.
await _sendMessage(
host: imapHost,
port: smtpPort,
from: user,
pass: pass,
to: user,
subject: 'wake-idle-${DateTime.now().millisecondsSinceEpoch}',
);
await secondIdleConnected.future.timeout(
const Duration(seconds: 10),
onTimeout: () =>
fail('IDLE did not reconnect after new message within 10s'),
);
expect(connectError, isNull, reason: 'reconnect should succeed');
// 3. Shuts down cleanly — dispose() must return quickly without hanging
// on the 25-minute IDLE cap.
mgr.dispose();
// Let all in-flight async work (idleDone, logout) finish.
await Future<void>.delayed(const Duration(seconds: 1));
});
},
);
}