at main 6.8 kB view raw
1import 'package:coves_flutter/config/environment_config.dart'; 2import 'package:coves_flutter/models/coves_session.dart'; 3import 'package:coves_flutter/services/coves_auth_service.dart'; 4import 'package:dio/dio.dart'; 5import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 6import 'package:flutter_test/flutter_test.dart'; 7import 'package:mockito/annotations.dart'; 8import 'package:mockito/mockito.dart'; 9 10import 'coves_auth_service_test.mocks.dart'; 11 12/// Test suite to verify that sessions are namespaced per environment. 13/// 14/// This prevents a critical bug where switching between dev/prod builds 15/// could send prod tokens to dev servers (or vice versa), causing 401 loops. 16@GenerateMocks([Dio, FlutterSecureStorage]) 17void main() { 18 late MockDio mockDio; 19 late MockFlutterSecureStorage mockStorage; 20 late CovesAuthService authService; 21 22 setUp(() { 23 CovesAuthService.resetInstance(); 24 mockDio = MockDio(); 25 mockStorage = MockFlutterSecureStorage(); 26 authService = CovesAuthService.createTestInstance( 27 dio: mockDio, 28 storage: mockStorage, 29 ); 30 }); 31 32 tearDown(() { 33 CovesAuthService.resetInstance(); 34 }); 35 36 group('CovesAuthService - Environment Isolation', () { 37 test('should use environment-specific storage keys', () { 38 // This test documents the expected storage key format 39 // The actual environment is determined at compile time via --dart-define 40 // In tests without specific environment configuration, it defaults to production 41 final currentEnv = EnvironmentConfig.current.environment.name; 42 final expectedKey = 'coves_session_$currentEnv'; 43 44 // The storage key should include the environment name 45 expect(expectedKey, contains('coves_session_')); 46 expect(expectedKey, contains(currentEnv)); 47 48 // For production environment (default in tests) 49 if (currentEnv == 'production') { 50 expect(expectedKey, 'coves_session_production'); 51 } else if (currentEnv == 'local') { 52 expect(expectedKey, 'coves_session_local'); 53 } 54 }); 55 56 test('should isolate sessions between environments', () async { 57 // This test verifies that sessions stored in different environments 58 // are accessed via different storage keys, preventing cross-contamination 59 60 // Get the current environment's storage key 61 final currentEnv = EnvironmentConfig.current.environment.name; 62 final storageKey = 'coves_session_$currentEnv'; 63 64 // Arrange - Mock session data 65 const session = CovesSession( 66 token: 'test-token-123', 67 did: 'did:plc:test123', 68 sessionId: 'session-123', 69 handle: 'alice.bsky.social', 70 ); 71 72 // Mock storage read for the environment-specific key 73 when( 74 mockStorage.read(key: storageKey), 75 ).thenAnswer((_) async => session.toJsonString()); 76 77 // Act - Restore session 78 final result = await authService.restoreSession(); 79 80 // Assert 81 expect(result, isNotNull); 82 expect(result!.token, 'test-token-123'); 83 84 // Verify the correct environment-specific key was used 85 verify(mockStorage.read(key: storageKey)).called(1); 86 87 // Verify no other keys were accessed 88 verifyNever(mockStorage.read(key: 'coves_session')); 89 }); 90 91 test('should save sessions with environment-specific keys', () async { 92 // Get the current environment's storage key 93 final currentEnv = EnvironmentConfig.current.environment.name; 94 final storageKey = 'coves_session_$currentEnv'; 95 96 // First restore a session to set up state 97 const session = CovesSession( 98 token: 'old-token', 99 did: 'did:plc:test123', 100 sessionId: 'session-123', 101 handle: 'alice.bsky.social', 102 ); 103 104 when( 105 mockStorage.read(key: storageKey), 106 ).thenAnswer((_) async => session.toJsonString()); 107 await authService.restoreSession(); 108 109 // Mock successful refresh 110 const newToken = 'new-refreshed-token'; 111 when( 112 mockDio.post<Map<String, dynamic>>( 113 '/oauth/refresh', 114 data: anyNamed('data'), 115 ), 116 ).thenAnswer( 117 (_) async => Response( 118 requestOptions: RequestOptions(path: '/oauth/refresh'), 119 statusCode: 200, 120 data: {'sealed_token': newToken, 'access_token': 'some-access-token'}, 121 ), 122 ); 123 124 when( 125 mockStorage.write(key: storageKey, value: anyNamed('value')), 126 ).thenAnswer((_) async => {}); 127 128 // Act - Refresh token (which saves the updated session) 129 await authService.refreshToken(); 130 131 // Assert - Verify environment-specific key was used for saving 132 verify( 133 mockStorage.write(key: storageKey, value: anyNamed('value')), 134 ).called(1); 135 136 // Verify the generic key was never used 137 verifyNever( 138 mockStorage.write(key: 'coves_session', value: anyNamed('value')), 139 ); 140 }); 141 142 test('should delete sessions using environment-specific keys', () async { 143 // Get the current environment's storage key 144 final currentEnv = EnvironmentConfig.current.environment.name; 145 final storageKey = 'coves_session_$currentEnv'; 146 147 // First restore a session 148 const session = CovesSession( 149 token: 'test-token', 150 did: 'did:plc:test123', 151 sessionId: 'session-123', 152 ); 153 154 when( 155 mockStorage.read(key: storageKey), 156 ).thenAnswer((_) async => session.toJsonString()); 157 await authService.restoreSession(); 158 159 // Mock logout 160 when( 161 mockDio.post<void>('/oauth/logout', options: anyNamed('options')), 162 ).thenAnswer( 163 (_) async => Response( 164 requestOptions: RequestOptions(path: '/oauth/logout'), 165 statusCode: 200, 166 ), 167 ); 168 169 when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {}); 170 171 // Act - Sign out 172 await authService.signOut(); 173 174 // Assert - Verify environment-specific key was used for deletion 175 verify(mockStorage.delete(key: storageKey)).called(1); 176 177 // Verify the generic key was never used 178 verifyNever(mockStorage.delete(key: 'coves_session')); 179 }); 180 181 test('should document storage key format for both environments', () { 182 // This test serves as documentation for the storage key format 183 // Production key 184 expect('coves_session_production', 'coves_session_production'); 185 186 // Local development key 187 expect('coves_session_local', 'coves_session_local'); 188 189 // This ensures: 190 // 1. Production tokens are stored in 'coves_session_production' 191 // 2. Local dev tokens are stored in 'coves_session_local' 192 // 3. Switching between prod/dev builds doesn't cause token conflicts 193 // 4. Each environment maintains its own session independently 194 }); 195 }); 196}