1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import 'package:flutter_test/flutter_test.dart';
import 'package:firefox_vpn/core/core_api.dart';
import 'package:firefox_vpn/core/core_controller.dart';
import 'package:firefox_vpn/core/models.dart';
/// A core whose next [failNext] reads fail as if the connection dropped.
class _FlakyApi implements CoreApi {
int failNext = 0;
@override
Future<CoreStatus> status() async {
if (failNext > 0) {
failNext--;
throw const CoreUnavailable(CoreProblem.notAnswering, 'connection reset');
}
return CoreStatus.fromJson({
'state': {'phase': 'off', 'message': 'Proxy off'},
});
}
@override
Future<AccountList> accounts() async => AccountList.fromJson({});
@override
void close() {}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
/// A running core whose auto-connect tests the connection; it counts the
/// app's own tests.
class _AutoConnectingApi implements CoreApi {
int tests = 0;
bool autoConnecting = true;
@override
Future<CoreStatus> status() async => CoreStatus.fromJson({
'state': {'phase': 'running', 'message': 'Proxy running'},
'system_proxy': 'set',
'system_proxy_supported': true,
'auto_connecting': autoConnecting,
});
@override
Future<ConnectionTest> test() async {
tests++;
return ConnectionTest.fromJson({'ok': false});
}
@override
Future<AccountList> accounts() async => AccountList.fromJson({});
@override
void close() {}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
// The core's auto-connect tests by itself; the app adds none meanwhile.
test('no tests of its own while the core auto-connects', () async {
final api = _AutoConnectingApi();
final core = CoreController(
() async => api,
pollInterval: const Duration(hours: 1),
);
await core.refresh();
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(core.status?.autoConnecting, isTrue);
expect(api.tests, 0);
api.autoConnecting = false;
await core.refresh();
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(api.tests, 1, reason: 'unconfirmed once auto-connect is over');
core.dispose();
});
// Android closes the app's connections when the phone's network goes
// away; one failed read must not show the core as lost.
test('one failed read is retried before the core counts as lost', () async {
final api = _FlakyApi();
final core = CoreController(
() async => api,
pollInterval: const Duration(hours: 1),
);
await core.refresh();
expect(core.unavailable, isNull);
api.failNext = 1;
await core.refresh();
expect(core.unavailable, isNull, reason: 'a single failure is retried');
await core.refresh();
expect(core.unavailable, isNull);
api.failNext = 2;
await core.refresh();
await core.refresh();
expect(core.unavailable?.problem, CoreProblem.notAnswering);
await core.refresh();
expect(core.unavailable, isNull, reason: 'it answers again');
core.dispose();
});
}