Skip to content

Commit 61d9927

Browse files
Doug Borgclaude
andcommitted
Add tests for AuthService, AuthState, and SuspectedLocationState
32 new tests covering: - AuthService: restoreLogin with cached fallback, isLoggedIn for all modes, getAccessToken, logout clearing cached display name - AuthState: init/restore, login/logout, mode change auth refresh - SuspectedLocationState: initLocal, refreshIfNeeded loading states, selection/deselection Also make keys.dart return empty strings instead of throwing when dart-defines are missing, so tests can run with plain `flutter test` without needing --dart-define flags. OAuth login will still fail at token request time if keys are empty, which is expected in tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b668f95 commit 61d9927

4 files changed

Lines changed: 516 additions & 6 deletions

File tree

lib/keys.dart

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
// OpenStreetMap OAuth client IDs for this app.
22
// These must be provided via --dart-define at build time.
3+
// When not configured (e.g. in tests), empty strings are returned —
4+
// OAuth login will fail at token request time, which is the expected
5+
// behavior for test environments.
6+
7+
import 'package:flutter/foundation.dart';
38

49
String get kOsmProdClientId {
510
const fromBuild = String.fromEnvironment('OSM_PROD_CLIENTID');
6-
if (fromBuild.isNotEmpty) return fromBuild;
7-
8-
throw Exception('OSM_PROD_CLIENTID not configured. Use --dart-define=OSM_PROD_CLIENTID=your_id');
11+
if (fromBuild.isEmpty) {
12+
debugPrint('[keys] OSM_PROD_CLIENTID not configured — OAuth login will fail');
13+
}
14+
return fromBuild;
915
}
1016

1117
String get kOsmSandboxClientId {
1218
const fromBuild = String.fromEnvironment('OSM_SANDBOX_CLIENTID');
13-
if (fromBuild.isNotEmpty) return fromBuild;
14-
15-
throw Exception('OSM_SANDBOX_CLIENTID not configured. Use --dart-define=OSM_SANDBOX_CLIENTID=your_id');
19+
if (fromBuild.isEmpty) {
20+
debugPrint('[keys] OSM_SANDBOX_CLIENTID not configured — OAuth login will fail');
21+
}
22+
return fromBuild;
1623
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/services.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:http/http.dart' as http;
6+
import 'package:mocktail/mocktail.dart';
7+
import 'package:shared_preferences/shared_preferences.dart';
8+
9+
import 'package:deflockapp/app_state.dart' show UploadMode;
10+
import 'package:deflockapp/services/auth_service.dart';
11+
12+
class MockHttpClient extends Mock implements http.Client {}
13+
14+
void main() {
15+
late MockHttpClient mockClient;
16+
late AuthService service;
17+
18+
TestWidgetsFlutterBinding.ensureInitialized();
19+
20+
setUpAll(() {
21+
registerFallbackValue(Uri.parse('https://example.com'));
22+
23+
// Mock FlutterSecureStorage platform channel so OAuth2Helper.removeAllTokens()
24+
// doesn't throw MissingPluginException in tests.
25+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
26+
.setMockMethodCallHandler(
27+
const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'),
28+
(MethodCall methodCall) async => null,
29+
);
30+
});
31+
32+
setUp(() {
33+
SharedPreferences.setMockInitialValues({});
34+
mockClient = MockHttpClient();
35+
});
36+
37+
AuthService createService({UploadMode mode = UploadMode.production}) {
38+
return AuthService(mode: mode, client: mockClient);
39+
}
40+
41+
group('restoreLogin', () {
42+
test('returns username when token exists and fetch succeeds', () async {
43+
SharedPreferences.setMockInitialValues({
44+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
45+
});
46+
service = createService();
47+
48+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
49+
.thenAnswer((_) async => http.Response(
50+
jsonEncode({
51+
'user': {'display_name': 'TestUser'}
52+
}),
53+
200,
54+
));
55+
56+
final result = await service.restoreLogin();
57+
58+
expect(result, equals('TestUser'));
59+
expect(service.displayName, equals('TestUser'));
60+
});
61+
62+
test('caches display name on successful fetch', () async {
63+
SharedPreferences.setMockInitialValues({
64+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
65+
});
66+
service = createService();
67+
68+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
69+
.thenAnswer((_) async => http.Response(
70+
jsonEncode({
71+
'user': {'display_name': 'CachedUser'}
72+
}),
73+
200,
74+
));
75+
76+
await service.restoreLogin();
77+
78+
final prefs = await SharedPreferences.getInstance();
79+
expect(prefs.getString('cached_display_name'), equals('CachedUser'));
80+
});
81+
82+
test('falls back to cached name on HTTP error', () async {
83+
SharedPreferences.setMockInitialValues({
84+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
85+
'cached_display_name': 'PreviousUser',
86+
});
87+
service = createService();
88+
89+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
90+
.thenAnswer((_) async => http.Response('Server Error', 500));
91+
92+
final result = await service.restoreLogin();
93+
94+
expect(result, equals('PreviousUser'));
95+
expect(service.displayName, equals('PreviousUser'));
96+
});
97+
98+
test('falls back to cached name on timeout', () async {
99+
SharedPreferences.setMockInitialValues({
100+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
101+
'cached_display_name': 'TimeoutUser',
102+
});
103+
service = createService();
104+
105+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
106+
.thenThrow(http.ClientException('Connection timed out'));
107+
108+
final result = await service.restoreLogin();
109+
110+
expect(result, equals('TimeoutUser'));
111+
expect(service.displayName, equals('TimeoutUser'));
112+
});
113+
114+
test('returns empty string when fetch fails and no cached name', () async {
115+
SharedPreferences.setMockInitialValues({
116+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
117+
});
118+
service = createService();
119+
120+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
121+
.thenAnswer((_) async => http.Response('Server Error', 500));
122+
123+
final result = await service.restoreLogin();
124+
125+
expect(result, equals(''));
126+
expect(service.displayName, equals(''));
127+
});
128+
129+
test('returns null when no token stored', () async {
130+
SharedPreferences.setMockInitialValues({});
131+
service = createService();
132+
133+
final result = await service.restoreLogin();
134+
135+
expect(result, isNull);
136+
verifyNever(() => mockClient.get(any(), headers: any(named: 'headers')));
137+
});
138+
139+
test('returns Demo User in simulate mode when logged in', () async {
140+
SharedPreferences.setMockInitialValues({
141+
'sim_user_logged_in': true,
142+
});
143+
service = createService(mode: UploadMode.simulate);
144+
145+
final result = await service.restoreLogin();
146+
147+
expect(result, equals('Demo User'));
148+
verifyNever(() => mockClient.get(any(), headers: any(named: 'headers')));
149+
});
150+
151+
test('returns null in simulate mode when not logged in', () async {
152+
SharedPreferences.setMockInitialValues({});
153+
service = createService(mode: UploadMode.simulate);
154+
155+
final result = await service.restoreLogin();
156+
157+
expect(result, isNull);
158+
});
159+
});
160+
161+
group('isLoggedIn', () {
162+
test('returns true when valid token exists', () async {
163+
SharedPreferences.setMockInitialValues({
164+
'osm_token_prod': jsonEncode({'accessToken': 'valid-token'}),
165+
});
166+
service = createService();
167+
168+
expect(await service.isLoggedIn(), isTrue);
169+
});
170+
171+
test('returns false when no token stored', () async {
172+
SharedPreferences.setMockInitialValues({});
173+
service = createService();
174+
175+
expect(await service.isLoggedIn(), isFalse);
176+
});
177+
178+
test('returns false for malformed JSON token', () async {
179+
SharedPreferences.setMockInitialValues({
180+
'osm_token_prod': 'not-valid-json',
181+
});
182+
service = createService();
183+
184+
expect(await service.isLoggedIn(), isFalse);
185+
});
186+
187+
test('sandbox mode uses correct key', () async {
188+
SharedPreferences.setMockInitialValues({
189+
'osm_token_sandbox': jsonEncode({'accessToken': 'sandbox-token'}),
190+
});
191+
service = createService(mode: UploadMode.sandbox);
192+
193+
expect(await service.isLoggedIn(), isTrue);
194+
});
195+
196+
test('returns true in simulate mode when sim_user_logged_in', () async {
197+
SharedPreferences.setMockInitialValues({
198+
'sim_user_logged_in': true,
199+
});
200+
service = createService(mode: UploadMode.simulate);
201+
202+
expect(await service.isLoggedIn(), isTrue);
203+
});
204+
});
205+
206+
group('getAccessToken', () {
207+
test('returns stored token', () async {
208+
SharedPreferences.setMockInitialValues({
209+
'osm_token_prod': jsonEncode({'accessToken': 'my-token'}),
210+
});
211+
service = createService();
212+
213+
expect(await service.getAccessToken(), equals('my-token'));
214+
});
215+
216+
test('returns sim-user-token in simulate mode', () async {
217+
SharedPreferences.setMockInitialValues({});
218+
service = createService(mode: UploadMode.simulate);
219+
220+
expect(await service.getAccessToken(), equals('sim-user-token'));
221+
});
222+
223+
test('returns null when no token stored', () async {
224+
SharedPreferences.setMockInitialValues({});
225+
service = createService();
226+
227+
expect(await service.getAccessToken(), isNull);
228+
});
229+
});
230+
231+
group('logout', () {
232+
test('clears token and cached display name', () async {
233+
SharedPreferences.setMockInitialValues({
234+
'osm_token_prod': jsonEncode({'accessToken': 'token'}),
235+
'cached_display_name': 'SomeUser',
236+
});
237+
service = createService();
238+
239+
// First restore to set _displayName
240+
when(() => mockClient.get(any(), headers: any(named: 'headers')))
241+
.thenAnswer((_) async => http.Response(
242+
jsonEncode({
243+
'user': {'display_name': 'SomeUser'}
244+
}),
245+
200,
246+
));
247+
await service.restoreLogin();
248+
expect(service.displayName, equals('SomeUser'));
249+
250+
await service.logout();
251+
252+
expect(service.displayName, isNull);
253+
final prefs = await SharedPreferences.getInstance();
254+
expect(prefs.getString('osm_token_prod'), isNull);
255+
expect(prefs.getString('cached_display_name'), isNull);
256+
});
257+
258+
test('clears sim_user_logged_in in simulate mode', () async {
259+
SharedPreferences.setMockInitialValues({
260+
'sim_user_logged_in': true,
261+
});
262+
service = createService(mode: UploadMode.simulate);
263+
264+
await service.restoreLogin();
265+
expect(service.displayName, equals('Demo User'));
266+
267+
await service.logout();
268+
269+
expect(service.displayName, isNull);
270+
final prefs = await SharedPreferences.getInstance();
271+
expect(prefs.getBool('sim_user_logged_in'), isNull);
272+
});
273+
});
274+
}

0 commit comments

Comments
 (0)