···
group('CovesAuthService', () {
test('should throw ArgumentError when handle is empty', () async {
39
-
() => authService.signIn(''),
40
-
throwsA(isA<ArgumentError>()),
38
+
expect(() => authService.signIn(''), throwsA(isA<ArgumentError>()));
44
-
test('should throw ArgumentError when handle is whitespace-only',
47
-
() => authService.signIn(' '),
48
-
throwsA(isA<ArgumentError>()),
42
+
'should throw ArgumentError when handle is whitespace-only',
45
+
() => authService.signIn(' '),
46
+
throwsA(isA<ArgumentError>()),
52
-
test('should throw appropriate error when user cancels sign-in',
51
+
test('should throw appropriate error when user cancels sign-in', () async {
// Note: FlutterWebAuth2.authenticate is not easily mockable as it's a static method
// This test documents expected behavior when authentication is cancelled
// In practice, this would throw with CANCELED/cancelled in the message
···
// Skipping for now as it requires more complex mocking infrastructure
63
-
test('should throw Exception when network error occurs during OAuth',
65
-
// Note: Similar to above, FlutterWebAuth2 static methods are difficult to mock
66
-
// This test documents expected behavior
67
-
// The actual implementation catches exceptions and rethrows with context
62
+
'should throw Exception when network error occurs during OAuth',
64
+
// Note: Similar to above, FlutterWebAuth2 static methods are difficult to mock
65
+
// This test documents expected behavior
66
+
// The actual implementation catches exceptions and rethrows with context
test('should trim handle before processing', () async {
// This test verifies the handle trimming logic
···
final jsonString = session.toJsonString();
89
-
when(mockStorage.read(key: storageKey))
90
-
.thenAnswer((_) async => jsonString);
90
+
mockStorage.read(key: storageKey),
91
+
).thenAnswer((_) async => jsonString);
final result = await authService.restoreSession();
···
test('should return null when no stored session exists', () async {
106
-
when(mockStorage.read(key: storageKey))
107
-
.thenAnswer((_) async => null);
107
+
when(mockStorage.read(key: storageKey)).thenAnswer((_) async => null);
final result = await authService.restoreSession();
···
test('should handle corrupted storage data gracefully', () async {
const corruptedJson = 'not-valid-json{]';
120
-
when(mockStorage.read(key: storageKey))
121
-
.thenAnswer((_) async => corruptedJson);
122
-
when(mockStorage.delete(key: storageKey))
123
-
.thenAnswer((_) async => {});
121
+
mockStorage.read(key: storageKey),
122
+
).thenAnswer((_) async => corruptedJson);
123
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
final result = await authService.restoreSession();
···
verify(mockStorage.delete(key: storageKey)).called(1);
134
-
test('should handle session JSON with missing required fields gracefully',
137
-
const invalidJson = '{"token": "test"}'; // Missing required fields (did, session_id)
138
-
when(mockStorage.read(key: storageKey))
139
-
.thenAnswer((_) async => invalidJson);
140
-
when(mockStorage.delete(key: storageKey))
141
-
.thenAnswer((_) async => {});
135
+
'should handle session JSON with missing required fields gracefully',
138
+
const invalidJson =
139
+
'{"token": "test"}'; // Missing required fields (did, session_id)
141
+
mockStorage.read(key: storageKey),
142
+
).thenAnswer((_) async => invalidJson);
143
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
144
-
final result = await authService.restoreSession();
146
+
final result = await authService.restoreSession();
147
-
// Should return null and clear corrupted storage
148
-
expect(result, isNull);
149
-
verify(mockStorage.read(key: storageKey)).called(1);
150
-
verify(mockStorage.delete(key: storageKey)).called(1);
149
+
// Should return null and clear corrupted storage
150
+
expect(result, isNull);
151
+
verify(mockStorage.read(key: storageKey)).called(1);
152
+
verify(mockStorage.delete(key: storageKey)).called(1);
test('should handle storage read errors gracefully', () async {
155
-
when(mockStorage.read(key: storageKey))
156
-
.thenThrow(Exception('Storage error'));
157
-
when(mockStorage.delete(key: storageKey))
158
-
.thenAnswer((_) async => {});
159
+
mockStorage.read(key: storageKey),
160
+
).thenThrow(Exception('Storage error'));
161
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
final result = await authService.restoreSession();
···
group('refreshToken()', () {
test('should throw StateError when no session exists', () async {
173
-
() => authService.refreshToken(),
174
-
throwsA(isA<StateError>()),
175
+
expect(() => authService.refreshToken(), throwsA(isA<StateError>()));
178
-
test('should successfully refresh token and return updated session',
180
-
// Arrange - First restore a session
181
-
const initialSession = CovesSession(
182
-
token: 'old-token',
183
-
did: 'did:plc:test123',
184
-
sessionId: 'session-123',
185
-
handle: 'alice.bsky.social',
187
-
when(mockStorage.read(key: storageKey))
188
-
.thenAnswer((_) async => initialSession.toJsonString());
189
-
await authService.restoreSession();
179
+
'should successfully refresh token and return updated session',
181
+
// Arrange - First restore a session
182
+
const initialSession = CovesSession(
183
+
token: 'old-token',
184
+
did: 'did:plc:test123',
185
+
sessionId: 'session-123',
186
+
handle: 'alice.bsky.social',
189
+
mockStorage.read(key: storageKey),
190
+
).thenAnswer((_) async => initialSession.toJsonString());
191
+
await authService.restoreSession();
191
-
// Mock successful refresh response
192
-
const newToken = 'new-refreshed-token';
193
-
when(mockDio.post<Map<String, dynamic>>(
195
-
data: anyNamed('data'),
196
-
)).thenAnswer((_) async => Response(
193
+
// Mock successful refresh response
194
+
const newToken = 'new-refreshed-token';
196
+
mockDio.post<Map<String, dynamic>>(
198
+
data: anyNamed('data'),
201
+
(_) async => Response(
requestOptions: RequestOptions(path: '/oauth/refresh'),
199
-
data: {'sealed_token': newToken, 'access_token': 'some-access-token'},
205
+
'sealed_token': newToken,
206
+
'access_token': 'some-access-token',
202
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
203
-
.thenAnswer((_) async => {});
212
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
213
+
).thenAnswer((_) async => {});
206
-
final result = await authService.refreshToken();
216
+
final result = await authService.refreshToken();
209
-
expect(result.token, newToken);
210
-
expect(result.did, 'did:plc:test123');
211
-
expect(result.sessionId, 'session-123');
212
-
expect(result.handle, 'alice.bsky.social');
213
-
verify(mockDio.post<Map<String, dynamic>>(
215
-
data: anyNamed('data'),
217
-
verify(mockStorage.write(
219
-
value: anyNamed('value'),
219
+
expect(result.token, newToken);
220
+
expect(result.did, 'did:plc:test123');
221
+
expect(result.sessionId, 'session-123');
222
+
expect(result.handle, 'alice.bsky.social');
224
+
mockDio.post<Map<String, dynamic>>(
226
+
data: anyNamed('data'),
230
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
test('should throw "Session expired" on 401 response', () async {
// Arrange - First restore a session
···
sessionId: 'session-123',
230
-
when(mockStorage.read(key: storageKey))
231
-
.thenAnswer((_) async => session.toJsonString());
243
+
mockStorage.read(key: storageKey),
244
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
235
-
when(mockDio.post<Map<String, dynamic>>(
237
-
data: anyNamed('data'),
249
+
mockDio.post<Map<String, dynamic>>(
251
+
data: anyNamed('data'),
requestOptions: RequestOptions(path: '/oauth/refresh'),
type: DioExceptionType.badResponse,
···
() => authService.refreshToken(),
254
-
e is Exception && e.toString().contains('Session expired')),
269
+
(e) => e is Exception && e.toString().contains('Session expired'),
···
sessionId: 'session-123',
266
-
when(mockStorage.read(key: storageKey))
267
-
.thenAnswer((_) async => session.toJsonString());
283
+
mockStorage.read(key: storageKey),
284
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
271
-
when(mockDio.post<Map<String, dynamic>>(
273
-
data: anyNamed('data'),
289
+
mockDio.post<Map<String, dynamic>>(
291
+
data: anyNamed('data'),
requestOptions: RequestOptions(path: '/oauth/refresh'),
type: DioExceptionType.connectionError,
···
() => authService.refreshToken(),
288
-
e.toString().contains('Token refresh failed')),
308
+
e.toString().contains('Token refresh failed'),
293
-
test('should throw Exception when response is missing sealed_token', () async {
294
-
// Arrange - First restore a session
295
-
const session = CovesSession(
296
-
token: 'old-token',
297
-
did: 'did:plc:test123',
298
-
sessionId: 'session-123',
300
-
when(mockStorage.read(key: storageKey))
301
-
.thenAnswer((_) async => session.toJsonString());
302
-
await authService.restoreSession();
315
+
'should throw Exception when response is missing sealed_token',
317
+
// Arrange - First restore a session
318
+
const session = CovesSession(
319
+
token: 'old-token',
320
+
did: 'did:plc:test123',
321
+
sessionId: 'session-123',
324
+
mockStorage.read(key: storageKey),
325
+
).thenAnswer((_) async => session.toJsonString());
326
+
await authService.restoreSession();
304
-
// Mock response without sealed_token
305
-
when(mockDio.post<Map<String, dynamic>>(
307
-
data: anyNamed('data'),
308
-
)).thenAnswer((_) async => Response(
328
+
// Mock response without sealed_token
330
+
mockDio.post<Map<String, dynamic>>(
332
+
data: anyNamed('data'),
335
+
(_) async => Response(
requestOptions: RequestOptions(path: '/oauth/refresh'),
data: {'access_token': 'some-token'}, // No sealed_token field
316
-
() => authService.refreshToken(),
320
-
e.toString().contains('Invalid refresh response')),
344
+
() => authService.refreshToken(),
349
+
e.toString().contains('Invalid refresh response'),
325
-
test('should throw Exception when response sealed_token is empty', () async {
326
-
// Arrange - First restore a session
327
-
const session = CovesSession(
328
-
token: 'old-token',
329
-
did: 'did:plc:test123',
330
-
sessionId: 'session-123',
332
-
when(mockStorage.read(key: storageKey))
333
-
.thenAnswer((_) async => session.toJsonString());
334
-
await authService.restoreSession();
357
+
'should throw Exception when response sealed_token is empty',
359
+
// Arrange - First restore a session
360
+
const session = CovesSession(
361
+
token: 'old-token',
362
+
did: 'did:plc:test123',
363
+
sessionId: 'session-123',
366
+
mockStorage.read(key: storageKey),
367
+
).thenAnswer((_) async => session.toJsonString());
368
+
await authService.restoreSession();
336
-
// Mock response with empty sealed_token
337
-
when(mockDio.post<Map<String, dynamic>>(
339
-
data: anyNamed('data'),
340
-
)).thenAnswer((_) async => Response(
370
+
// Mock response with empty sealed_token
372
+
mockDio.post<Map<String, dynamic>>(
374
+
data: anyNamed('data'),
377
+
(_) async => Response(
requestOptions: RequestOptions(path: '/oauth/refresh'),
343
-
data: {'sealed_token': '', 'access_token': 'some-token'}, // Empty sealed_token
381
+
'sealed_token': '',
382
+
'access_token': 'some-token',
383
+
}, // Empty sealed_token
348
-
() => authService.refreshToken(),
352
-
e.toString().contains('Invalid refresh response')),
389
+
() => authService.refreshToken(),
394
+
e.toString().contains('Invalid refresh response'),
359
-
test('should clear session and storage on successful server-side logout',
361
-
// Arrange - First restore a session
362
-
const session = CovesSession(
363
-
token: 'test-token',
364
-
did: 'did:plc:test123',
365
-
sessionId: 'session-123',
367
-
when(mockStorage.read(key: storageKey))
368
-
.thenAnswer((_) async => session.toJsonString());
369
-
await authService.restoreSession();
404
+
'should clear session and storage on successful server-side logout',
406
+
// Arrange - First restore a session
407
+
const session = CovesSession(
408
+
token: 'test-token',
409
+
did: 'did:plc:test123',
410
+
sessionId: 'session-123',
413
+
mockStorage.read(key: storageKey),
414
+
).thenAnswer((_) async => session.toJsonString());
415
+
await authService.restoreSession();
371
-
// Mock successful logout
372
-
when(mockDio.post<void>(
374
-
options: anyNamed('options'),
375
-
)).thenAnswer((_) async => Response(
417
+
// Mock successful logout
419
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
421
+
(_) async => Response(
requestOptions: RequestOptions(path: '/oauth/logout'),
380
-
when(mockStorage.delete(key: storageKey))
381
-
.thenAnswer((_) async => {});
427
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
384
-
await authService.signOut();
430
+
await authService.signOut();
387
-
expect(authService.session, isNull);
388
-
expect(authService.isAuthenticated, isFalse);
389
-
verify(mockDio.post<void>(
391
-
options: anyNamed('options'),
393
-
verify(mockStorage.delete(key: storageKey)).called(1);
433
+
expect(authService.session, isNull);
434
+
expect(authService.isAuthenticated, isFalse);
436
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
438
+
verify(mockStorage.delete(key: storageKey)).called(1);
396
-
test('should clear local state even when server revocation fails',
398
-
// Arrange - First restore a session
399
-
const session = CovesSession(
400
-
token: 'test-token',
401
-
did: 'did:plc:test123',
402
-
sessionId: 'session-123',
404
-
when(mockStorage.read(key: storageKey))
405
-
.thenAnswer((_) async => session.toJsonString());
406
-
await authService.restoreSession();
443
+
'should clear local state even when server revocation fails',
445
+
// Arrange - First restore a session
446
+
const session = CovesSession(
447
+
token: 'test-token',
448
+
did: 'did:plc:test123',
449
+
sessionId: 'session-123',
452
+
mockStorage.read(key: storageKey),
453
+
).thenAnswer((_) async => session.toJsonString());
454
+
await authService.restoreSession();
408
-
// Mock server error
409
-
when(mockDio.post<void>(
411
-
options: anyNamed('options'),
414
-
requestOptions: RequestOptions(path: '/oauth/logout'),
415
-
type: DioExceptionType.connectionError,
416
-
message: 'Connection failed',
456
+
// Mock server error
458
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
461
+
requestOptions: RequestOptions(path: '/oauth/logout'),
462
+
type: DioExceptionType.connectionError,
463
+
message: 'Connection failed',
420
-
when(mockStorage.delete(key: storageKey))
421
-
.thenAnswer((_) async => {});
467
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
424
-
await authService.signOut();
470
+
await authService.signOut();
427
-
expect(authService.session, isNull);
428
-
expect(authService.isAuthenticated, isFalse);
429
-
verify(mockStorage.delete(key: storageKey)).called(1);
473
+
expect(authService.session, isNull);
474
+
expect(authService.isAuthenticated, isFalse);
475
+
verify(mockStorage.delete(key: storageKey)).called(1);
test('should work even when no session exists', () async {
434
-
when(mockStorage.delete(key: storageKey))
435
-
.thenAnswer((_) async => {});
481
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
await authService.signOut();
···
expect(authService.session, isNull);
expect(authService.isAuthenticated, isFalse);
verify(mockStorage.delete(key: storageKey)).called(1);
444
-
verifyNever(mockDio.post<void>(
446
-
options: anyNamed('options'),
491
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
test('should clear local state even when storage delete fails', () async {
···
sessionId: 'session-123',
457
-
when(mockStorage.read(key: storageKey))
458
-
.thenAnswer((_) async => session.toJsonString());
503
+
mockStorage.read(key: storageKey),
504
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
461
-
when(mockDio.post<void>(
463
-
options: anyNamed('options'),
464
-
)).thenAnswer((_) async => Response(
465
-
requestOptions: RequestOptions(path: '/oauth/logout'),
508
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
510
+
(_) async => Response(
511
+
requestOptions: RequestOptions(path: '/oauth/logout'),
469
-
when(mockStorage.delete(key: storageKey))
470
-
.thenThrow(Exception('Storage error'));
517
+
mockStorage.delete(key: storageKey),
518
+
).thenThrow(Exception('Storage error'));
// Act & Assert - Should not throw
expect(() => authService.signOut(), throwsA(isA<Exception>()));
···
sessionId: 'session-123',
488
-
when(mockStorage.read(key: storageKey))
489
-
.thenAnswer((_) async => session.toJsonString());
537
+
mockStorage.read(key: storageKey),
538
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
···
sessionId: 'session-123',
514
-
when(mockStorage.read(key: storageKey))
515
-
.thenAnswer((_) async => session.toJsonString());
564
+
mockStorage.read(key: storageKey),
565
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
518
-
when(mockDio.post<void>(
520
-
options: anyNamed('options'),
521
-
)).thenAnswer((_) async => Response(
522
-
requestOptions: RequestOptions(path: '/oauth/logout'),
525
-
when(mockStorage.delete(key: storageKey))
526
-
.thenAnswer((_) async => {});
569
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
571
+
(_) async => Response(
572
+
requestOptions: RequestOptions(path: '/oauth/logout'),
576
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
await authService.signOut();
···
sessionId: 'session-123',
550
-
when(mockStorage.read(key: storageKey))
551
-
.thenAnswer((_) async => session.toJsonString());
601
+
mockStorage.read(key: storageKey),
602
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
···
sessionId: 'session-123',
565
-
when(mockStorage.read(key: storageKey))
566
-
.thenAnswer((_) async => session.toJsonString());
617
+
mockStorage.read(key: storageKey),
618
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
569
-
when(mockDio.post<void>(
571
-
options: anyNamed('options'),
572
-
)).thenAnswer((_) async => Response(
573
-
requestOptions: RequestOptions(path: '/oauth/logout'),
576
-
when(mockStorage.delete(key: storageKey))
577
-
.thenAnswer((_) async => {});
622
+
mockDio.post<void>('/oauth/logout', options: anyNamed('options')),
624
+
(_) async => Response(
625
+
requestOptions: RequestOptions(path: '/oauth/logout'),
629
+
when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {});
await authService.signOut();
···
sessionId: 'session-123',
595
-
when(mockStorage.read(key: storageKey))
596
-
.thenAnswer((_) async => session.toJsonString());
648
+
mockStorage.read(key: storageKey),
649
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
···
sessionId: 'session-123',
614
-
when(mockStorage.read(key: storageKey))
615
-
.thenAnswer((_) async => initialSession.toJsonString());
668
+
mockStorage.read(key: storageKey),
669
+
).thenAnswer((_) async => initialSession.toJsonString());
await authService.restoreSession();
const newToken = 'new-token';
619
-
when(mockDio.post<Map<String, dynamic>>(
621
-
data: anyNamed('data'),
622
-
)).thenAnswer((_) async => Response(
623
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
625
-
data: {'sealed_token': newToken, 'access_token': 'some-access-token'},
627
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
628
-
.thenAnswer((_) async => {});
674
+
mockDio.post<Map<String, dynamic>>(
676
+
data: anyNamed('data'),
679
+
(_) async => Response(
680
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
683
+
'sealed_token': newToken,
684
+
'access_token': 'some-access-token',
689
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
690
+
).thenAnswer((_) async => {});
await authService.refreshToken();
···
group('refreshToken() - Concurrency Protection', () {
640
-
test('should only make one API request for concurrent refresh calls',
642
-
// Arrange - First restore a session
643
-
const initialSession = CovesSession(
644
-
token: 'old-token',
645
-
did: 'did:plc:test123',
646
-
sessionId: 'session-123',
647
-
handle: 'alice.bsky.social',
649
-
when(mockStorage.read(key: storageKey))
650
-
.thenAnswer((_) async => initialSession.toJsonString());
651
-
await authService.restoreSession();
703
+
'should only make one API request for concurrent refresh calls',
705
+
// Arrange - First restore a session
706
+
const initialSession = CovesSession(
707
+
token: 'old-token',
708
+
did: 'did:plc:test123',
709
+
sessionId: 'session-123',
710
+
handle: 'alice.bsky.social',
713
+
mockStorage.read(key: storageKey),
714
+
).thenAnswer((_) async => initialSession.toJsonString());
715
+
await authService.restoreSession();
653
-
const newToken = 'new-refreshed-token';
717
+
const newToken = 'new-refreshed-token';
655
-
// Mock refresh response with a delay to simulate network latency
656
-
when(mockDio.post<Map<String, dynamic>>(
658
-
data: anyNamed('data'),
659
-
)).thenAnswer((_) async {
660
-
await Future.delayed(const Duration(milliseconds: 100));
662
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
664
-
data: {'sealed_token': newToken, 'access_token': 'some-access-token'},
719
+
// Mock refresh response with a delay to simulate network latency
721
+
mockDio.post<Map<String, dynamic>>(
723
+
data: anyNamed('data'),
725
+
).thenAnswer((_) async {
726
+
await Future.delayed(const Duration(milliseconds: 100));
728
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
731
+
'sealed_token': newToken,
732
+
'access_token': 'some-access-token',
668
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
669
-
.thenAnswer((_) async => {});
738
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
739
+
).thenAnswer((_) async => {});
671
-
// Act - Launch 3 concurrent refresh calls
672
-
final results = await Future.wait([
673
-
authService.refreshToken(),
674
-
authService.refreshToken(),
675
-
authService.refreshToken(),
741
+
// Act - Launch 3 concurrent refresh calls
742
+
final results = await Future.wait([
743
+
authService.refreshToken(),
744
+
authService.refreshToken(),
745
+
authService.refreshToken(),
678
-
// Assert - All calls should return the same refreshed session
679
-
expect(results.length, 3);
680
-
expect(results[0].token, newToken);
681
-
expect(results[1].token, newToken);
682
-
expect(results[2].token, newToken);
748
+
// Assert - All calls should return the same refreshed session
749
+
expect(results.length, 3);
750
+
expect(results[0].token, newToken);
751
+
expect(results[1].token, newToken);
752
+
expect(results[2].token, newToken);
684
-
// Verify only one API call was made
685
-
verify(mockDio.post<Map<String, dynamic>>(
687
-
data: anyNamed('data'),
754
+
// Verify only one API call was made
756
+
mockDio.post<Map<String, dynamic>>(
758
+
data: anyNamed('data'),
690
-
// Verify only one storage write was made
691
-
verify(mockStorage.write(
693
-
value: anyNamed('value'),
762
+
// Verify only one storage write was made
764
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
test('should propagate errors to all concurrent waiters', () async {
// Arrange - First restore a session
···
sessionId: 'session-123',
704
-
when(mockStorage.read(key: storageKey))
705
-
.thenAnswer((_) async => session.toJsonString());
777
+
mockStorage.read(key: storageKey),
778
+
).thenAnswer((_) async => session.toJsonString());
await authService.restoreSession();
// Mock 401 response with delay
709
-
when(mockDio.post<Map<String, dynamic>>(
711
-
data: anyNamed('data'),
712
-
)).thenAnswer((_) async {
783
+
mockDio.post<Map<String, dynamic>>(
785
+
data: anyNamed('data'),
787
+
).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 100));
requestOptions: RequestOptions(path: '/oauth/refresh'),
···
// Verify only one API call was made
747
-
verify(mockDio.post<Map<String, dynamic>>(
749
-
data: anyNamed('data'),
823
+
mockDio.post<Map<String, dynamic>>(
825
+
data: anyNamed('data'),
test('should allow new refresh after previous one completes', () async {
···
sessionId: 'session-123',
760
-
when(mockStorage.read(key: storageKey))
761
-
.thenAnswer((_) async => initialSession.toJsonString());
838
+
mockStorage.read(key: storageKey),
839
+
).thenAnswer((_) async => initialSession.toJsonString());
await authService.restoreSession();
const newToken1 = 'new-token-1';
const newToken2 = 'new-token-2';
768
-
when(mockDio.post<Map<String, dynamic>>(
770
-
data: anyNamed('data'),
771
-
)).thenAnswer((_) async => Response(
772
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
774
-
data: {'sealed_token': newToken1, 'access_token': 'some-access-token'},
847
+
mockDio.post<Map<String, dynamic>>(
849
+
data: anyNamed('data'),
852
+
(_) async => Response(
853
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
856
+
'sealed_token': newToken1,
857
+
'access_token': 'some-access-token',
777
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
778
-
.thenAnswer((_) async => {});
863
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
864
+
).thenAnswer((_) async => {});
final result1 = await authService.refreshToken();
···
expect(result1.token, newToken1);
// Now update the mock for the second refresh
787
-
when(mockDio.post<Map<String, dynamic>>(
789
-
data: anyNamed('data'),
790
-
)).thenAnswer((_) async => Response(
791
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
793
-
data: {'sealed_token': newToken2, 'access_token': 'some-access-token'},
874
+
mockDio.post<Map<String, dynamic>>(
876
+
data: anyNamed('data'),
879
+
(_) async => Response(
880
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
883
+
'sealed_token': newToken2,
884
+
'access_token': 'some-access-token',
// Act - Second refresh (should be allowed since first completed)
final result2 = await authService.refreshToken();
···
expect(result2.token, newToken2);
// Verify two separate API calls were made
803
-
verify(mockDio.post<Map<String, dynamic>>(
805
-
data: anyNamed('data'),
897
+
mockDio.post<Map<String, dynamic>>(
899
+
data: anyNamed('data'),
test('should allow new refresh after previous one fails', () async {
···
sessionId: 'session-123',
816
-
when(mockStorage.read(key: storageKey))
817
-
.thenAnswer((_) async => initialSession.toJsonString());
912
+
mockStorage.read(key: storageKey),
913
+
).thenAnswer((_) async => initialSession.toJsonString());
await authService.restoreSession();
// Mock first refresh to fail
821
-
when(mockDio.post<Map<String, dynamic>>(
823
-
data: anyNamed('data'),
918
+
mockDio.post<Map<String, dynamic>>(
920
+
data: anyNamed('data'),
requestOptions: RequestOptions(path: '/oauth/refresh'),
type: DioExceptionType.connectionError,
···
// Now mock a successful second refresh
const newToken = 'new-token-after-retry';
848
-
when(mockDio.post<Map<String, dynamic>>(
850
-
data: anyNamed('data'),
851
-
)).thenAnswer((_) async => Response(
852
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
854
-
data: {'sealed_token': newToken, 'access_token': 'some-access-token'},
947
+
mockDio.post<Map<String, dynamic>>(
949
+
data: anyNamed('data'),
952
+
(_) async => Response(
953
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
956
+
'sealed_token': newToken,
957
+
'access_token': 'some-access-token',
857
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
858
-
.thenAnswer((_) async => {});
963
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
964
+
).thenAnswer((_) async => {});
// Act - Second refresh (should be allowed and succeed)
final result = await authService.refreshToken();
···
expect(result.token, newToken);
// Verify two separate API calls were made
867
-
verify(mockDio.post<Map<String, dynamic>>(
869
-
data: anyNamed('data'),
974
+
mockDio.post<Map<String, dynamic>>(
976
+
data: anyNamed('data'),
874
-
'should handle concurrent calls where one arrives after refresh completes',
876
-
// Arrange - First restore a session
877
-
const initialSession = CovesSession(
878
-
token: 'old-token',
879
-
did: 'did:plc:test123',
880
-
sessionId: 'session-123',
882
-
when(mockStorage.read(key: storageKey))
883
-
.thenAnswer((_) async => initialSession.toJsonString());
884
-
await authService.restoreSession();
982
+
'should handle concurrent calls where one arrives after refresh completes',
984
+
// Arrange - First restore a session
985
+
const initialSession = CovesSession(
986
+
token: 'old-token',
987
+
did: 'did:plc:test123',
988
+
sessionId: 'session-123',
991
+
mockStorage.read(key: storageKey),
992
+
).thenAnswer((_) async => initialSession.toJsonString());
993
+
await authService.restoreSession();
886
-
const newToken1 = 'new-token-1';
887
-
const newToken2 = 'new-token-2';
995
+
const newToken1 = 'new-token-1';
996
+
const newToken2 = 'new-token-2';
891
-
// Mock refresh with different responses
892
-
when(mockDio.post<Map<String, dynamic>>(
894
-
data: anyNamed('data'),
895
-
)).thenAnswer((_) async {
897
-
await Future.delayed(const Duration(milliseconds: 50));
899
-
requestOptions: RequestOptions(path: '/oauth/refresh'),
901
-
data: {'sealed_token': callCount == 1 ? newToken1 : newToken2, 'access_token': 'some-access-token'},
1000
+
// Mock refresh with different responses
1002
+
mockDio.post<Map<String, dynamic>>(
1004
+
data: anyNamed('data'),
1006
+
).thenAnswer((_) async {
1008
+
await Future.delayed(const Duration(milliseconds: 50));
1010
+
requestOptions: RequestOptions(path: '/oauth/refresh'),
1013
+
'sealed_token': callCount == 1 ? newToken1 : newToken2,
1014
+
'access_token': 'some-access-token',
905
-
when(mockStorage.write(key: storageKey, value: anyNamed('value')))
906
-
.thenAnswer((_) async => {});
1020
+
mockStorage.write(key: storageKey, value: anyNamed('value')),
1021
+
).thenAnswer((_) async => {});
908
-
// Act - Start first refresh
909
-
final future1 = authService.refreshToken();
1023
+
// Act - Start first refresh
1024
+
final future1 = authService.refreshToken();
911
-
// Wait for it to complete
912
-
final result1 = await future1;
1026
+
// Wait for it to complete
1027
+
final result1 = await future1;
914
-
// Start second refresh after first completes
915
-
final result2 = await authService.refreshToken();
1029
+
// Start second refresh after first completes
1030
+
final result2 = await authService.refreshToken();
918
-
expect(result1.token, newToken1);
919
-
expect(result2.token, newToken2);
1033
+
expect(result1.token, newToken1);
1034
+
expect(result2.token, newToken2);
921
-
// Verify two separate API calls were made
922
-
verify(mockDio.post<Map<String, dynamic>>(
924
-
data: anyNamed('data'),
1036
+
// Verify two separate API calls were made
1038
+
mockDio.post<Map<String, dynamic>>(
1040
+
data: anyNamed('data'),