255 lines
8.2 KiB
Dart
255 lines
8.2 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();
|
|
},
|
|
smtpConnect: (account, username, password) async => FakeSmtpClient(),
|
|
);
|
|
}
|
|
|
|
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();
|
|
},
|
|
smtpConnect: (_, __, ___) async => FakeSmtpClient(),
|
|
);
|
|
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'),
|
|
smtpConnect: (_, __, ___) async => FakeSmtpClient(),
|
|
);
|
|
expect(() => svc.testConnection(_imapAccount, 'pw'), throwsException);
|
|
});
|
|
|
|
test('reports SMTP failure after IMAP success', () async {
|
|
final svc = ConnectionTestServiceImpl(
|
|
MockClient((_) async => http.Response('', 200)),
|
|
imapConnect: (_, __, ___) async => FakeImapClient(),
|
|
smtpConnect: (_, __, ___) async => throw Exception('smtp boom'),
|
|
);
|
|
expect(
|
|
() => svc.testConnection(_imapAccount, 'pw'),
|
|
throwsA(predicate((e) => e.toString().contains('SMTP: '))),
|
|
);
|
|
});
|
|
|
|
test('skips ManageSieve when manageSieveHost is empty', () async {
|
|
var sieveCalled = false;
|
|
final svc = ConnectionTestServiceImpl(
|
|
MockClient((_) async => http.Response('', 200)),
|
|
imapConnect: (_, __, ___) async => FakeImapClient(),
|
|
smtpConnect: (_, __, ___) async => FakeSmtpClient(),
|
|
manageSieveConnect: ({
|
|
required String host,
|
|
required int port,
|
|
required bool useTls,
|
|
}) async {
|
|
sieveCalled = true;
|
|
throw Exception('should not be called');
|
|
},
|
|
);
|
|
await svc.testConnection(_imapAccount, 'pw');
|
|
expect(sieveCalled, false);
|
|
});
|
|
|
|
test('reports ManageSieve failure when host is set', () async {
|
|
const accountWithSieve = Account(
|
|
id: 'acc-1',
|
|
displayName: 'Alice',
|
|
email: 'alice@example.com',
|
|
imapHost: 'imap.example.com',
|
|
smtpHost: 'smtp.example.com',
|
|
manageSieveHost: 'sieve.example.com',
|
|
);
|
|
final svc = ConnectionTestServiceImpl(
|
|
MockClient((_) async => http.Response('', 200)),
|
|
imapConnect: (_, __, ___) async => FakeImapClient(),
|
|
smtpConnect: (_, __, ___) async => FakeSmtpClient(),
|
|
manageSieveConnect: ({
|
|
required String host,
|
|
required int port,
|
|
required bool useTls,
|
|
}) async =>
|
|
throw Exception('sieve boom'),
|
|
);
|
|
expect(
|
|
() => svc.testConnection(accountWithSieve, 'pw'),
|
|
throwsA(predicate((e) => e.toString().contains('ManageSieve: '))),
|
|
);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|