fixed test.
This commit is contained in:
+15
-8
@@ -18,14 +18,14 @@ tasks:
|
|||||||
run: once
|
run: once
|
||||||
deps: [_nix-check]
|
deps: [_nix-check]
|
||||||
cmds:
|
cmds:
|
||||||
- cmd: fvm install --skip-pub-get
|
- cmd: scripts/silent_on_success.sh fvm install --skip-pub-get
|
||||||
|
|
||||||
_pub-get:
|
_pub-get:
|
||||||
internal: true
|
internal: true
|
||||||
run: once
|
run: once
|
||||||
deps: [_flutter-check]
|
deps: [_flutter-check]
|
||||||
cmds:
|
cmds:
|
||||||
- fvm flutter pub get --suppress-analytics
|
- scripts/silent_on_success.sh fvm flutter pub get --suppress-analytics
|
||||||
|
|
||||||
_linux-deps-check:
|
_linux-deps-check:
|
||||||
internal: true
|
internal: true
|
||||||
@@ -33,7 +33,7 @@ tasks:
|
|||||||
preconditions:
|
preconditions:
|
||||||
- sh: command -v clang >/dev/null 2>&1
|
- 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"
|
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"
|
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:
|
install-hooks:
|
||||||
@@ -41,6 +41,13 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- pre-commit install
|
- 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:
|
codegen:
|
||||||
desc: Generate Drift DB code (run after any schema change)
|
desc: Generate Drift DB code (run after any schema change)
|
||||||
deps: [_nix-check, _pub-get]
|
deps: [_nix-check, _pub-get]
|
||||||
@@ -49,7 +56,7 @@ tasks:
|
|||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
desc: Static analysis (flutter analyze)
|
desc: Static analysis (flutter analyze)
|
||||||
deps: [_nix-check, _pub-get]
|
deps: [_nix-check, _codegen]
|
||||||
cmds:
|
cmds:
|
||||||
- scripts/run_analyze.sh
|
- scripts/run_analyze.sh
|
||||||
|
|
||||||
@@ -67,13 +74,13 @@ tasks:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
desc: Unit tests + coverage gate (fails if any non-excluded lib/ file is missing)
|
desc: Unit tests + coverage gate (fails if any non-excluded lib/ file is missing)
|
||||||
deps: [_nix-check, _pub-get]
|
deps: [_nix-check, _codegen]
|
||||||
cmds:
|
cmds:
|
||||||
- scripts/run_unit_tests.sh
|
- scripts/run_unit_tests.sh
|
||||||
|
|
||||||
test-widget:
|
test-widget:
|
||||||
desc: Widget tests — headless, no display or network required
|
desc: Widget tests — headless, no display or network required
|
||||||
deps: [_nix-check, _pub-get]
|
deps: [_nix-check, _codegen]
|
||||||
cmds:
|
cmds:
|
||||||
- scripts/run_widget_tests.sh
|
- scripts/run_widget_tests.sh
|
||||||
|
|
||||||
@@ -97,9 +104,9 @@ tasks:
|
|||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
desc: Build the Linux desktop app (debug)
|
desc: Build the Linux desktop app (debug)
|
||||||
deps: [_nix-check, _linux-deps-check, _pub-get]
|
deps: [_nix-check, _linux-deps-check, _codegen]
|
||||||
cmds:
|
cmds:
|
||||||
- fvm flutter build linux --debug --no-pub
|
- scripts/silent_on_success.sh fvm flutter build linux --debug --no-pub
|
||||||
|
|
||||||
_android-sdk-check:
|
_android-sdk-check:
|
||||||
internal: true
|
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:async';
|
||||||
import 'dart:io';
|
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:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:sharedinbox/core/models/account.dart';
|
import 'package:sharedinbox/core/models/account.dart';
|
||||||
@@ -129,19 +130,25 @@ class _FakeEmails implements EmailRepository {
|
|||||||
Future<void> retryMutation(int id) async {}
|
Future<void> retryMutation(int id) async {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain (non-TLS) IMAP connect for the local dev Stalwart, which has no TLS.
|
Future<void> _sendMessage({
|
||||||
// Production connectImap() rejects imapSsl:false, so tests inject this instead.
|
required String host,
|
||||||
Future<ImapClient> _connectImapPlain(
|
required int port,
|
||||||
Account account,
|
required String from,
|
||||||
String username,
|
required String pass,
|
||||||
String password,
|
required String to,
|
||||||
) async {
|
required String subject,
|
||||||
final client = ImapClient(
|
}) async {
|
||||||
defaultResponseTimeout: const Duration(seconds: 20),
|
final smtp = SmtpClient('sharedinbox-test');
|
||||||
);
|
await smtp.connectToServer(host, port, isSecure: false);
|
||||||
await client.connectToServer(account.imapHost, account.imapPort);
|
await smtp.ehlo();
|
||||||
await client.login(username, password);
|
await smtp.authenticate(from, pass);
|
||||||
return client;
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -160,42 +167,91 @@ void main() {
|
|||||||
pass = _env('STALWART_PASS_B');
|
pass = _env('STALWART_PASS_B');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('IDLE connects, wakes on new message, and shuts down cleanly', () async {
|
test(
|
||||||
final fakeAccounts = _FakeAccounts()..password = pass;
|
'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'),
|
Future<ImapClient> trackingConnect(
|
||||||
// not by email address ('alice@example.com'). connectImap() passes
|
Account account,
|
||||||
// account.email as the IMAP login username, so use the bare name here.
|
String username,
|
||||||
final account = Account(
|
String password,
|
||||||
id: 'integration-test',
|
) async {
|
||||||
displayName: 'Integration Test',
|
try {
|
||||||
email: user,
|
final client = ImapClient(
|
||||||
imapHost: imapHost,
|
defaultResponseTimeout: const Duration(seconds: 20),
|
||||||
imapPort: imapPort,
|
);
|
||||||
imapSsl: false,
|
await client.connectToServer(
|
||||||
smtpHost: imapHost,
|
account.imapHost,
|
||||||
smtpPort: smtpPort,
|
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 mgr = AccountSyncManager(
|
final fakeAccounts = _FakeAccounts()..password = pass;
|
||||||
fakeAccounts,
|
final account = Account(
|
||||||
_FakeMailboxes(),
|
id: 'integration-test',
|
||||||
_FakeEmails(),
|
displayName: 'Integration Test',
|
||||||
imapConnect: _connectImapPlain,
|
email: user,
|
||||||
);
|
imapHost: imapHost,
|
||||||
mgr.start();
|
imapPort: imapPort,
|
||||||
|
imapSsl: false,
|
||||||
|
smtpHost: imapHost,
|
||||||
|
smtpPort: smtpPort,
|
||||||
|
);
|
||||||
|
|
||||||
// Push the account — this triggers _sync() then _idle() in the background.
|
final mgr = AccountSyncManager(
|
||||||
fakeAccounts.push([account]);
|
fakeAccounts,
|
||||||
|
_FakeMailboxes(),
|
||||||
|
_FakeEmails(),
|
||||||
|
imapConnect: trackingConnect,
|
||||||
|
);
|
||||||
|
addTearDown(mgr.dispose);
|
||||||
|
mgr.start();
|
||||||
|
fakeAccounts.push([account]);
|
||||||
|
|
||||||
// Give the manager time to connect and enter IDLE.
|
// 1. IDLE connects
|
||||||
await Future<void>.delayed(const Duration(seconds: 2));
|
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
|
// 2. Wakes on new message — deliver a message and wait for IDLE to
|
||||||
// immediately without waiting for the 25-minute cap.
|
// reconnect, which proves the manager woke up and re-entered IDLE.
|
||||||
mgr.dispose();
|
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');
|
||||||
|
|
||||||
// Let all in-flight async work (idleDone, logout) finish.
|
// 3. Shuts down cleanly — dispose() must return quickly without hanging
|
||||||
await Future<void>.delayed(const Duration(seconds: 1));
|
// on the 25-minute IDLE cap.
|
||||||
});
|
mgr.dispose();
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user