forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth_refresh_timeout_test.dart
More file actions
98 lines (77 loc) · 3.81 KB
/
Copy pathauth_refresh_timeout_test.dart
File metadata and controls
98 lines (77 loc) · 3.81 KB
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
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/services/auth/auth_token_result.dart';
import 'package:omi/services/auth_service.dart';
/// A forced token refresh that never returns must not hang its caller.
///
/// `backend/http/shared.dart` refreshes on the way into *every* authenticated
/// request, so an unbounded stall in the refresh silently freezes all backend
/// traffic app-wide: no error, no snackbar, nothing to report. Measured on
/// iPhone 17 Pro / iOS 27.0 against a Firebase Auth emulator on a non-loopback
/// host, `forceRefresh()` never returned at all — the app completed a clean run
/// of onboarding requests on its cached token and then went permanently silent.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
SharedPreferences.setMockInitialValues({});
await SharedPreferencesUtil.init();
});
test('a refresh that never returns resolves as a transient failure', () async {
final gateway = _StallingGateway(neverCompletes: true);
final service = _service(gateway, timeout: const Duration(milliseconds: 30));
final result = await service.refreshIdToken();
expect(result, isA<AuthTokenTransientFailure>());
expect(
(result as AuthTokenTransientFailure).failureClass,
'refresh_timeout',
reason: 'a stall must be distinguishable in telemetry from a refresh that actually failed',
);
});
test('a stalled refresh is still retried, not abandoned after one timeout', () async {
final gateway = _StallingGateway(neverCompletes: true);
final service = _service(gateway, timeout: const Duration(milliseconds: 20));
await service.refreshIdToken();
// A timeout is classified transient precisely so the existing retry loop
// still applies; a stall on one attempt may not be a stall on the next.
expect(gateway.refreshCalls, greaterThan(1));
});
test('a slow but successful refresh inside the budget still succeeds', () async {
// The bound must not turn a merely slow network into a failed session.
final gateway = _StallingGateway(latency: const Duration(milliseconds: 10));
final service = _service(gateway, timeout: const Duration(seconds: 5));
final result = await service.refreshIdToken();
expect(result, isA<AuthTokenSuccess>());
expect(result.tokenOrNull, 'slow-token');
});
test('the caller gets a result rather than waiting forever', () async {
// The user-visible property: refreshIdToken() completes. Everything above is
// about *which* result; this is about it returning at all.
final gateway = _StallingGateway(neverCompletes: true);
final service = _service(gateway, timeout: const Duration(milliseconds: 20));
await expectLater(service.refreshIdToken().timeout(const Duration(seconds: 5)), completes);
});
}
AuthService _service(_StallingGateway gateway, {required Duration timeout}) =>
AuthService.forTesting(tokenGateway: gateway, refreshDelay: (_) async {}, refreshAttemptTimeout: timeout);
final class _StallingGateway implements AuthTokenGateway {
_StallingGateway({this.neverCompletes = false, this.latency});
final bool neverCompletes;
final Duration? latency;
int refreshCalls = 0;
@override
AuthUserSnapshot? get currentUser =>
const AuthUserSnapshot(uid: 'user-1', email: 'person@example.com', displayName: 'Person Example');
@override
Future<RefreshedAuthToken?> forceRefresh() {
refreshCalls++;
if (neverCompletes) return Completer<RefreshedAuthToken?>().future;
return Future<RefreshedAuthToken?>.delayed(
latency ?? Duration.zero,
() => RefreshedAuthToken(token: 'slow-token', expirationTime: DateTime.now()),
);
}
@override
Future<void> signOut() async {}
}