stalwart-dev/integration_ui_test.sh working! great!
This commit is contained in:
@@ -61,6 +61,39 @@ jobs:
|
||||
stalwart-dev/test.sh
|
||||
"
|
||||
|
||||
integration-ui:
|
||||
name: UI Integration tests (Stalwart + Xvfb)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v14
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Install Flutter Linux build dependencies
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-3-dev pkg-config cmake ninja-build clang \
|
||||
libsecret-1-dev
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.41.6"
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Run UI integration tests
|
||||
run: |
|
||||
nix develop --command bash -c "
|
||||
flutter pub get &&
|
||||
flutter pub run build_runner build --delete-conflicting-outputs &&
|
||||
stalwart-dev/integration_ui_test.sh
|
||||
"
|
||||
|
||||
build-linux:
|
||||
name: Build Linux desktop
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
# Local IMAP/SMTP dev server for integration tests
|
||||
stalwart-mail
|
||||
|
||||
# Headless display for UI integration tests
|
||||
xvfb-run # wraps Xvfb; xvfb-run --auto-servernum ...
|
||||
|
||||
# Utilities
|
||||
git
|
||||
curl
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// E2E integration tests — requires a running Stalwart instance and a display.
|
||||
// Run via: stalwart-dev/integration_ui_test.sh
|
||||
//
|
||||
// Environment variables (set by the runner script):
|
||||
// STALWART_IMAP_HOST, STALWART_IMAP_PORT
|
||||
// STALWART_SMTP_HOST, STALWART_SMTP_PORT
|
||||
// STALWART_USER_B / STALWART_PASS_B (alice@localhost)
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:sharedinbox/core/storage/secure_storage.dart';
|
||||
import 'package:sharedinbox/di.dart';
|
||||
import 'package:sharedinbox/main.dart' as app;
|
||||
|
||||
/// In-memory drop-in for SecureStorage — no D-Bus or keyring daemon required.
|
||||
class _InMemorySecureStorage implements SecureStorage {
|
||||
final _store = <String, String>{};
|
||||
|
||||
@override
|
||||
Future<void> write({required String key, required String? value}) async {
|
||||
if (value == null) {
|
||||
_store.remove(key);
|
||||
} else {
|
||||
_store[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> read({required String key}) async => _store[key];
|
||||
|
||||
@override
|
||||
Future<void> delete({required String key}) async => _store.remove(key);
|
||||
}
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late String imapHost;
|
||||
late int imapPort;
|
||||
late String smtpHost;
|
||||
late int smtpPort;
|
||||
late String userEmail;
|
||||
late String userPass;
|
||||
|
||||
setUpAll(() {
|
||||
imapHost = Platform.environment['STALWART_IMAP_HOST'] ?? '127.0.0.1';
|
||||
imapPort = int.parse(Platform.environment['STALWART_IMAP_PORT'] ?? '1430');
|
||||
smtpHost = Platform.environment['STALWART_SMTP_HOST'] ?? '127.0.0.1';
|
||||
smtpPort = int.parse(Platform.environment['STALWART_SMTP_PORT'] ?? '1025');
|
||||
userEmail = Platform.environment['STALWART_USER_B'] ?? 'alice@localhost';
|
||||
userPass = Platform.environment['STALWART_PASS_B'] ?? 'secret';
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'E2E: add account, send mail to self, verify sent/inbox, search',
|
||||
(tester) async {
|
||||
// The Flutter Linux test runner defaults to a 1×1 window; give it a
|
||||
// real size so widgets are laid out and hittable.
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
app.main(overrides: [
|
||||
secureStorageProvider.overrideWithValue(_InMemorySecureStorage()),
|
||||
]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// ── Add account ────────────────────────────────────────────────────────
|
||||
expect(find.text('No accounts yet.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithIcon(FloatingActionButton, Icons.add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Add account'), findsOneWidget);
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Display name'), 'Alice');
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Email address'), userEmail);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Password'), userPass);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'IMAP host'), imapHost);
|
||||
|
||||
// The form has two "Port" fields: index 0 = IMAP, index 1 = SMTP.
|
||||
final imapPortField =
|
||||
find.widgetWithText(TextFormField, 'Port').at(0);
|
||||
await tester.ensureVisible(imapPortField);
|
||||
await tester.enterText(imapPortField, imapPort.toString());
|
||||
|
||||
// IMAP SSL defaults to on — turn it off for the plaintext dev server.
|
||||
final imapSslSwitch =
|
||||
find.widgetWithText(SwitchListTile, 'SSL/TLS').at(0);
|
||||
await tester.ensureVisible(imapSslSwitch);
|
||||
await tester.tap(imapSslSwitch);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'SMTP host'), smtpHost);
|
||||
|
||||
final smtpPortField =
|
||||
find.widgetWithText(TextFormField, 'Port').at(1);
|
||||
await tester.ensureVisible(smtpPortField);
|
||||
await tester.enterText(smtpPortField, smtpPort.toString());
|
||||
|
||||
final saveButton = find.widgetWithText(FilledButton, 'Save');
|
||||
await tester.ensureVisible(saveButton);
|
||||
await tester.tap(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Back at account list.
|
||||
expect(find.text('Alice'), findsOneWidget);
|
||||
expect(find.text(userEmail), findsOneWidget);
|
||||
|
||||
// ── Navigate to mailboxes ──────────────────────────────────────────────
|
||||
await tester.tap(find.text('Alice'));
|
||||
// Give the background sync time to populate mailboxes from IMAP.
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('INBOX'), findsOneWidget);
|
||||
|
||||
// ── Compose and send email to self ─────────────────────────────────────
|
||||
await tester.tap(find.text('INBOX'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.edit));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final subject = 'E2E-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'To'), userEmail);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Subject'), subject);
|
||||
|
||||
final bodyField = find.widgetWithText(TextFormField, 'Body');
|
||||
await tester.ensureVisible(bodyField);
|
||||
await tester.enterText(bodyField, 'Hello from integration test!');
|
||||
|
||||
await tester.tap(find.byIcon(Icons.send));
|
||||
// Wait for SMTP send + IMAP APPEND to complete.
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// ComposeScreen pops back to EmailListScreen (INBOX) after send.
|
||||
|
||||
// ── Check Sent folder ──────────────────────────────────────────────────
|
||||
// Go back to MailboxListScreen.
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Sent'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Sync Sent folder to fetch the appended message.
|
||||
await tester.tap(find.byIcon(Icons.sync));
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(subject), findsOneWidget);
|
||||
|
||||
// ── Check Inbox ────────────────────────────────────────────────────────
|
||||
await tester.pageBack(); // Sent EmailList → MailboxList
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('INBOX'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Sync INBOX — Stalwart delivers to self near-instantly.
|
||||
await tester.tap(find.byIcon(Icons.sync));
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(subject), findsOneWidget);
|
||||
|
||||
// ── Search ─────────────────────────────────────────────────────────────
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Search by the 'E2E-' prefix — should match the message we just sent.
|
||||
await tester.enterText(find.byType(TextField), 'E2E-');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(subject), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io' show HandshakeException;
|
||||
|
||||
import 'package:enough_mail/enough_mail.dart';
|
||||
|
||||
import '../../core/models/account.dart';
|
||||
@@ -25,7 +27,7 @@ Future<SmtpClient> connectSmtp(Account account, String password) async {
|
||||
final clientDomain =
|
||||
atIndex != -1 ? account.email.substring(atIndex + 1) : account.smtpHost;
|
||||
|
||||
final client = SmtpClient(clientDomain);
|
||||
var client = SmtpClient(clientDomain);
|
||||
await client.connectToServer(
|
||||
account.smtpHost,
|
||||
account.smtpPort,
|
||||
@@ -33,9 +35,20 @@ Future<SmtpClient> connectSmtp(Account account, String password) async {
|
||||
);
|
||||
await client.ehlo();
|
||||
if (!account.smtpSsl) {
|
||||
// Opportunistic TLS on submission port (587)
|
||||
// Opportunistic TLS on submission port (587).
|
||||
try {
|
||||
await client.startTls();
|
||||
} on HandshakeException catch (e) {
|
||||
// TLS handshake failure (e.g. self-signed cert) breaks the socket.
|
||||
// Reconnect plaintext so authenticate() can still proceed.
|
||||
log('STARTTLS handshake failed on ${account.smtpHost}: $e — reconnecting without TLS');
|
||||
client = SmtpClient(clientDomain);
|
||||
await client.connectToServer(
|
||||
account.smtpHost,
|
||||
account.smtpPort,
|
||||
isSecure: false,
|
||||
);
|
||||
await client.ehlo();
|
||||
} catch (e) {
|
||||
log('STARTTLS not available on ${account.smtpHost}: $e — continuing without TLS');
|
||||
}
|
||||
|
||||
@@ -234,22 +234,36 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
Future<void> sendEmail(String accountId, model.EmailDraft draft) async {
|
||||
final account = (await _accounts.getAccount(accountId))!;
|
||||
final password = await _accounts.getPassword(accountId);
|
||||
final builder = imap.MessageBuilder()
|
||||
..from = [imap.MailAddress(draft.from.name, draft.from.email)]
|
||||
..to = draft.to.map((a) => imap.MailAddress(a.name, a.email)).toList()
|
||||
..cc = draft.cc.map((a) => imap.MailAddress(a.name, a.email)).toList()
|
||||
..subject = draft.subject
|
||||
..text = draft.body;
|
||||
final mimeMessage = builder.buildMimeMessage();
|
||||
final smtpClient = await _smtpConnect(account, password);
|
||||
try {
|
||||
final builder = imap.MessageBuilder()
|
||||
..from = [imap.MailAddress(draft.from.name, draft.from.email)]
|
||||
..to = draft.to
|
||||
.map((a) => imap.MailAddress(a.name, a.email))
|
||||
.toList()
|
||||
..cc = draft.cc
|
||||
.map((a) => imap.MailAddress(a.name, a.email))
|
||||
.toList()
|
||||
..subject = draft.subject
|
||||
..text = draft.body;
|
||||
await smtpClient.sendMessage(builder.buildMimeMessage());
|
||||
await smtpClient.sendMessage(mimeMessage);
|
||||
} finally {
|
||||
await smtpClient.quit();
|
||||
}
|
||||
// Save a copy to the Sent folder via IMAP APPEND.
|
||||
// Create the folder first — many servers don't pre-create it.
|
||||
final imapClient = await _imapConnect(account, password);
|
||||
try {
|
||||
try {
|
||||
await imapClient.createMailbox('Sent');
|
||||
} catch (_) {
|
||||
// Already exists — that's fine.
|
||||
}
|
||||
await imapClient.appendMessage(
|
||||
mimeMessage,
|
||||
targetMailboxPath: 'Sent',
|
||||
flags: [r'\Seen'],
|
||||
);
|
||||
} finally {
|
||||
await imapClient.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'core/repositories/account_repository.dart';
|
||||
import 'core/repositories/email_repository.dart';
|
||||
import 'core/repositories/mailbox_repository.dart';
|
||||
import 'core/storage/secure_storage.dart';
|
||||
import 'core/sync/account_sync_manager.dart';
|
||||
import 'data/db/database.dart';
|
||||
import 'data/repositories/account_repository_impl.dart';
|
||||
@@ -16,10 +17,14 @@ final dbProvider = Provider<AppDatabase>((ref) {
|
||||
return db;
|
||||
});
|
||||
|
||||
final secureStorageProvider = Provider<SecureStorage>((ref) {
|
||||
return const FlutterSecureStorageImpl();
|
||||
});
|
||||
|
||||
final accountRepositoryProvider = Provider<AccountRepository>((ref) {
|
||||
return AccountRepositoryImpl(
|
||||
ref.watch(dbProvider),
|
||||
const FlutterSecureStorageImpl(),
|
||||
ref.watch(secureStorageProvider),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'di.dart';
|
||||
import 'ui/router.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: SharedInboxApp()));
|
||||
void main({List<Override> overrides = const []}) {
|
||||
runApp(ProviderScope(overrides: overrides, child: const SharedInboxApp()));
|
||||
}
|
||||
|
||||
class SharedInboxApp extends ConsumerStatefulWidget {
|
||||
|
||||
@@ -34,6 +34,8 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^4.0.0
|
||||
drift_dev: ^2.20.3
|
||||
build_runner: ^2.4.13
|
||||
|
||||
@@ -48,13 +48,13 @@ type = "memory"
|
||||
|
||||
[[directory."memory".principals]]
|
||||
class = "individual"
|
||||
name = "alice"
|
||||
name = "alice@localhost"
|
||||
secret = "secret"
|
||||
email = ["alice@localhost"]
|
||||
|
||||
[[directory."memory".principals]]
|
||||
class = "individual"
|
||||
name = "bob"
|
||||
name = "bob@localhost"
|
||||
secret = "secret"
|
||||
email = ["bob@localhost"]
|
||||
|
||||
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Starts Stalwart on random ports, then runs Flutter UI integration tests inside
|
||||
# a virtual X server (Xvfb). Works on a local desktop and in headless CI.
|
||||
# No D-Bus or keyring daemon is required — tests inject an in-memory SecureStorage.
|
||||
#
|
||||
# Run inside nix develop: stalwart-dev/integration_ui_test.sh
|
||||
set -Eeuo pipefail
|
||||
|
||||
export STALWART_USER_B="${STALWART_USER_B:-alice@localhost}"
|
||||
export STALWART_PASS_B="${STALWART_PASS_B:-secret}"
|
||||
export STALWART_USER_C="${STALWART_USER_C:-bob@localhost}"
|
||||
export STALWART_PASS_C="${STALWART_PASS_C:-secret}"
|
||||
export STALWART_RANDOM_PORTS=1
|
||||
STALWART_TMPDIR="$(mktemp -d /tmp/stalwart-dev-XXXXXX)"
|
||||
export STALWART_TMPDIR
|
||||
|
||||
# Isolate the app database: fresh HOME → fresh path_provider directory.
|
||||
TEST_HOME="$(mktemp -d /tmp/sharedinbox-test-home-XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
kill "${STALWART_PID:-}" 2>/dev/null || true
|
||||
wait "${STALWART_PID:-}" 2>/dev/null || true
|
||||
rm -rf "$TEST_HOME"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
command -v stalwart >/dev/null || {
|
||||
echo "stalwart not in PATH."
|
||||
echo "Run inside the nix dev shell:"
|
||||
echo " nix develop --command stalwart-dev/integration_ui_test.sh"
|
||||
exit 1
|
||||
}
|
||||
command -v xvfb-run >/dev/null || {
|
||||
echo "xvfb-run not in PATH."
|
||||
echo "Run inside the nix dev shell:"
|
||||
echo " nix develop --command stalwart-dev/integration_ui_test.sh"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Pre-seed spam-filter version so Stalwart does not fetch it on first boot.
|
||||
mkdir -p "$STALWART_TMPDIR"
|
||||
sqlite3 "${STALWART_TMPDIR}/data.sqlite" \
|
||||
"CREATE TABLE IF NOT EXISTS s (k BLOB PRIMARY KEY, v BLOB NOT NULL);
|
||||
INSERT OR REPLACE INTO s VALUES ('version.spam-filter', 'dev');" 2>/dev/null || true
|
||||
|
||||
LOGFILE="${STALWART_TMPDIR}/stalwart.log"
|
||||
rm -f "$LOGFILE"
|
||||
|
||||
"$(dirname "$0")/start" >"$LOGFILE" 2>&1 &
|
||||
STALWART_PID=$!
|
||||
|
||||
# Wait until Stalwart is accepting connections (up to 10 s).
|
||||
for _i in $(seq 1 20); do
|
||||
# shellcheck source=/dev/null
|
||||
[ -f "${STALWART_TMPDIR}/ports.env" ] && . "${STALWART_TMPDIR}/ports.env"
|
||||
grep -E "Configuration build error|Build error for key|already in use" "$LOGFILE" >/dev/null 2>&1 && {
|
||||
cat "$LOGFILE"; echo "Stalwart reported a startup error"; exit 1
|
||||
}
|
||||
kill -0 "$STALWART_PID" 2>/dev/null || {
|
||||
cat "$LOGFILE"; echo "Stalwart process died unexpectedly"; exit 1
|
||||
}
|
||||
if [ -n "${STALWART_URL:-}" ] && \
|
||||
curl -s --max-time 1 -o /dev/null "${STALWART_URL}/.well-known/jmap" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
[ -n "${STALWART_URL:-}" ] || { cat "$LOGFILE"; echo "Stalwart did not publish its chosen ports"; exit 1; }
|
||||
curl -s --max-time 1 -o /dev/null "${STALWART_URL}/.well-known/jmap" || {
|
||||
cat "$LOGFILE"; echo "Stalwart did not become ready"; exit 1
|
||||
}
|
||||
|
||||
echo "Stalwart ready — IMAP=:${STALWART_IMAP_PORT:-?} SMTP=:${STALWART_SMTP_PORT:-?}"
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export STALWART_IMAP_HOST="127.0.0.1"
|
||||
export STALWART_SMTP_HOST="127.0.0.1"
|
||||
export HOME="$TEST_HOME"
|
||||
|
||||
START=$(date +%s)
|
||||
|
||||
# xvfb-run provides a virtual framebuffer so the Flutter Linux runner has a
|
||||
# display without requiring a real desktop session. No D-Bus or keyring daemon
|
||||
# is needed because the integration tests inject an in-memory SecureStorage.
|
||||
xvfb-run --auto-servernum fvm flutter test integration_test/ -d linux
|
||||
|
||||
END=$(date +%s)
|
||||
echo "ui-integration: $((END - START))s"
|
||||
@@ -4,9 +4,9 @@
|
||||
set -Eeuo pipefail
|
||||
trap 'echo "Warning: A command failed ($0:$LINENO)"; exit 3' ERR
|
||||
|
||||
export STALWART_USER_B="${STALWART_USER_B:-alice}"
|
||||
export STALWART_USER_B="${STALWART_USER_B:-alice@localhost}"
|
||||
export STALWART_PASS_B="${STALWART_PASS_B:-secret}"
|
||||
export STALWART_USER_C="${STALWART_USER_C:-bob}"
|
||||
export STALWART_USER_C="${STALWART_USER_C:-bob@localhost}"
|
||||
export STALWART_PASS_C="${STALWART_PASS_C:-secret}"
|
||||
export STALWART_RANDOM_PORTS=1
|
||||
STALWART_TMPDIR="$(mktemp -d /tmp/stalwart-dev-XXXXXX)"
|
||||
@@ -32,6 +32,7 @@ trap 'kill "$STALWART_PID" 2>/dev/null || true; wait "$STALWART_PID" 2>/dev/null
|
||||
|
||||
# Wait until Stalwart is accepting connections (up to 10 s).
|
||||
for _i in $(seq 1 20); do
|
||||
# shellcheck source=/dev/null
|
||||
[ -f "${STALWART_TMPDIR}/ports.env" ] && . "${STALWART_TMPDIR}/ports.env"
|
||||
grep -E "Configuration build error|Build error for key|already in use" "$LOGFILE" >/dev/null 2>&1 && {
|
||||
cat "$LOGFILE"; echo "Stalwart reported a startup error"; exit 1
|
||||
@@ -51,7 +52,7 @@ curl -s --max-time 1 -o /dev/null "${STALWART_URL}/.well-known/jmap" || {
|
||||
cat "$LOGFILE"; echo "Stalwart did not become ready"; exit 1
|
||||
}
|
||||
|
||||
echo "Stalwart ready — IMAP=:${STALWART_IMAP_PORT} SMTP=:${STALWART_SMTP_PORT}"
|
||||
echo "Stalwart ready — IMAP=:${STALWART_IMAP_PORT:-?} SMTP=:${STALWART_SMTP_PORT:-?}"
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
@@ -66,8 +66,8 @@ void main() {
|
||||
await smtpClient.authenticate(userA, passA);
|
||||
|
||||
final builder = MessageBuilder()
|
||||
..from = [MailAddress('Alice', '$userA@localhost')]
|
||||
..to = [MailAddress('Bob', '$userB@localhost')]
|
||||
..from = [MailAddress('Alice', userA)]
|
||||
..to = [MailAddress('Bob', userB)]
|
||||
..subject = 'Integration test ${DateTime.now().millisecondsSinceEpoch}'
|
||||
..text = 'Hello from SharedInbox integration test.';
|
||||
await smtpClient.sendMessage(builder.buildMimeMessage());
|
||||
|
||||
@@ -371,7 +371,7 @@ void main() {
|
||||
expect(r.fakeImap.logoutCalled, isTrue);
|
||||
});
|
||||
|
||||
test('sendEmail calls SMTP sendMessage and quit', () async {
|
||||
test('sendEmail sends via SMTP and appends copy to Sent folder', () async {
|
||||
final r = _makeReposWithFakes();
|
||||
await r.accounts.addAccount(_account, 'pw');
|
||||
|
||||
@@ -388,6 +388,9 @@ void main() {
|
||||
|
||||
expect(r.fakeSmtp.messageSent, isTrue);
|
||||
expect(r.fakeSmtp.quitCalled, isTrue);
|
||||
expect(r.fakeImap.appendCalls, 1);
|
||||
expect(r.fakeImap.lastAppendMailboxPath, 'Sent');
|
||||
expect(r.fakeImap.logoutCalled, isTrue);
|
||||
});
|
||||
|
||||
test('searchEmails returns emails matching IMAP search', () async {
|
||||
|
||||
@@ -17,6 +17,9 @@ class FakeImapClient extends imap.ImapClient {
|
||||
int markDeletedCalls = 0;
|
||||
int expungeCalls = 0;
|
||||
int moveEmailCalls = 0;
|
||||
int appendCalls = 0;
|
||||
String? lastAppendMailboxPath;
|
||||
int createMailboxCalls = 0;
|
||||
|
||||
@override
|
||||
Future<imap.Mailbox> selectMailboxByPath(
|
||||
@@ -107,6 +110,30 @@ class FakeImapClient extends imap.ImapClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<imap.Mailbox> createMailbox(String path) async {
|
||||
createMailboxCalls++;
|
||||
return imap.Mailbox(
|
||||
encodedName: path,
|
||||
encodedPath: path,
|
||||
flags: [],
|
||||
pathSeparator: '/',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<imap.GenericImapResult> appendMessage(
|
||||
imap.MimeMessage message, {
|
||||
List<String>? flags,
|
||||
imap.Mailbox? targetMailbox,
|
||||
String? targetMailboxPath,
|
||||
Duration? responseTimeout,
|
||||
}) async {
|
||||
appendCalls++;
|
||||
lastAppendMailboxPath = targetMailboxPath;
|
||||
return imap.GenericImapResult();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<imap.GenericImapResult> uidMove(
|
||||
imap.MessageSequence sequence, {
|
||||
|
||||
Reference in New Issue
Block a user