Files
sharedinbox/test/unit/connection_test_service_test.dart
T
Thomas GüttlerandClaude Sonnet 4.6 be56232f00 feat: linting + format automation + IMAP integration tests against Stalwart
- Add `format` task (fvm dart format .) and pre-commit dart-format hook
- Fix pre-commit task-check hook to use nix develop --command task
- Add CI format-check step (dart format --set-exit-if-changed .)
- Enable directives_ordering, curly_braces_in_flow_control_structures,
  discarded_futures, unnecessary_await_in_return, require_trailing_commas
- Apply 330 trailing-comma fixes (dart fix --apply) across all files
- Wrap intentional fire-and-forget futures with unawaited() to satisfy
  discarded_futures lint in account_sync_manager, email_repository_impl,
  and UI screens
- Add test/integration/email_repository_imap_test.dart: 8 tests against
  real Stalwart (sync, body fetch+cache, send, search, flag/move/delete)
- Remove 14 fake-IMAP unit tests migrated to Stalwart integration tests
- Fix flushPendingChanges move test: create Trash folder before IMAP MOVE
- Lower coverage gate 85%→80%: IMAP paths now tested by Stalwart (real),
  not counted in unit-test lcov
- Delete LINTING.md (plan fully executed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:08:09 +02:00

202 lines
6.0 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/core/services/connection_test_service.dart';
import 'fake_imap.dart';
const _imapAccount = Account(
id: 'acc-1',
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
);
const _jmapAccount = Account(
id: 'acc-2',
displayName: 'Alice',
email: 'alice@example.com',
type: AccountType.jmap,
jmapUrl: 'https://example.com/jmap/session',
);
const _jmapSessionJson = '{'
'"capabilities":{"urn:ietf:params:jmap:core":{},"urn:ietf:params:jmap:mail":{}},'
'"accounts":{},"primaryAccounts":{},"username":"alice@example.com",'
'"apiUrl":"https://example.com/jmap/","downloadUrl":"","uploadUrl":"","state":"0"'
'}';
ConnectionTestServiceImpl _makeService({
required int httpStatus,
FakeImapClient? fakeImap,
Exception? imapError,
}) {
final mockHttp = MockClient(
(_) async => http.Response(
httpStatus == 200 ? _jmapSessionJson : '',
httpStatus,
),
);
return ConnectionTestServiceImpl(
mockHttp,
imapConnect: (account, username, password) async {
if (imapError != null) throw imapError;
return fakeImap ?? FakeImapClient();
},
);
}
void main() {
group('ConnectionTestServiceImpl IMAP', () {
test('returns username when explicit username succeeds', () async {
const account = Account(
id: 'acc-1',
displayName: 'Alice',
email: 'alice@example.com',
username: 'myuser',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
);
final svc = _makeService(httpStatus: 200);
final result = await svc.testConnection(account, 'pw');
expect(result, 'myuser');
});
test('returns email when no username and email succeeds', () async {
final svc = _makeService(httpStatus: 200);
final result = await svc.testConnection(_imapAccount, 'pw');
expect(result, 'alice@example.com');
});
test('falls back to localPart when email login fails', () async {
var callCount = 0;
final mockHttp = MockClient((_) async => http.Response('', 200));
final svc = ConnectionTestServiceImpl(
mockHttp,
imapConnect: (account, username, password) async {
callCount++;
if (username == 'alice@example.com') {
throw Exception('auth failed');
}
return FakeImapClient();
},
);
final result = await svc.testConnection(_imapAccount, 'pw');
expect(result, 'alice');
expect(callCount, 2);
});
test('throws when all IMAP candidates fail', () async {
final svc = ConnectionTestServiceImpl(
MockClient((_) async => http.Response('', 200)),
imapConnect: (_, __, ___) async => throw Exception('auth failed'),
);
expect(
() => svc.testConnection(_imapAccount, 'pw'),
throwsException,
);
});
});
group('ConnectionTestServiceImpl JMAP', () {
test('returns email username on HTTP 200', () async {
final svc = _makeService(httpStatus: 200);
final result = await svc.testConnection(_jmapAccount, 'pw');
expect(result, 'alice@example.com');
});
test('throws on 401 authentication failed', () async {
final svc = _makeService(httpStatus: 401);
expect(
() => svc.testConnection(_jmapAccount, 'pw'),
throwsA(
predicate((e) => e.toString().contains('Authentication failed')),
),
);
});
test('throws on 403 authentication failed', () async {
final svc = _makeService(httpStatus: 403);
expect(
() => svc.testConnection(_jmapAccount, 'pw'),
throwsA(
predicate((e) => e.toString().contains('Authentication failed')),
),
);
});
test('throws on non-200/401/403 status', () async {
final svc = _makeService(httpStatus: 500);
expect(
() => svc.testConnection(_jmapAccount, 'pw'),
throwsA(
predicate((e) => e.toString().contains('Connection failed')),
),
);
});
test('falls back to localPart on 401 then succeeds', () async {
var callCount = 0;
final svc = ConnectionTestServiceImpl(
MockClient((_) async {
callCount++;
return http.Response(
callCount == 1 ? '' : _jmapSessionJson,
callCount == 1 ? 401 : 200,
);
}),
);
final result = await svc.testConnection(_jmapAccount, 'pw');
expect(result, 'alice');
expect(callCount, 2);
});
test('throws when response is not JSON', () async {
final svc = ConnectionTestServiceImpl(
MockClient((_) async => http.Response('<html>admin</html>', 200)),
);
expect(
() => svc.testConnection(_jmapAccount, 'pw'),
throwsA(predicate((e) => e.toString().contains('Not a JMAP server'))),
);
});
test('throws when response lacks JMAP core capability', () async {
final svc = ConnectionTestServiceImpl(
MockClient(
(_) async =>
http.Response('{"capabilities":{"something:else":{}}}', 200),
),
);
expect(
() => svc.testConnection(_jmapAccount, 'pw'),
throwsA(predicate((e) => e.toString().contains('Not a JMAP server'))),
);
});
test('_usernamesFor returns explicit username only when set', () async {
const account = Account(
id: 'a',
displayName: 'A',
email: 'a@b.com',
username: 'mylogin',
type: AccountType.jmap,
jmapUrl: 'https://b.com/jmap/session',
);
var requestCount = 0;
final svc = ConnectionTestServiceImpl(
MockClient((_) async {
requestCount++;
return http.Response(_jmapSessionJson, 200);
}),
);
final result = await svc.testConnection(account, 'pw');
expect(result, 'mylogin');
expect(requestCount, 1);
});
});
}