fixed test.
This commit is contained in:
+15
-8
@@ -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
|
||||
|
||||
Executable
+24
@@ -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
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user