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(mockStorage.read(key: storageKey)) 74 .thenAnswer((_) async => session.toJsonString()); 75 76 // Act - Restore session 77 final result = await authService.restoreSession(); 78 79 // Assert 80 expect(result, isNotNull); 81 expect(result!.token, 'test-token-123'); 82 83 // Verify the correct environment-specific key was used 84 verify(mockStorage.read(key: storageKey)).called(1); 85 86 // Verify no other keys were accessed 87 verifyNever(mockStorage.read(key: 'coves_session')); 88 }); 89 90 test('should save sessions with environment-specific keys', () async { 91 // Get the current environment's storage key 92 final currentEnv = EnvironmentConfig.current.environment.name; 93 final storageKey = 'coves_session_$currentEnv'; 94 95 // First restore a session to set up state 96 const session = CovesSession( 97 token: 'old-token', 98 did: 'did:plc:test123', 99 sessionId: 'session-123', 100 handle: 'alice.bsky.social', 101 ); 102 103 when(mockStorage.read(key: storageKey)) 104 .thenAnswer((_) async => session.toJsonString()); 105 await authService.restoreSession(); 106 107 // Mock successful refresh 108 const newToken = 'new-refreshed-token'; 109 when(mockDio.post<Map<String, dynamic>>( 110 '/oauth/refresh', 111 data: anyNamed('data'), 112 )).thenAnswer((_) async => Response( 113 requestOptions: RequestOptions(path: '/oauth/refresh'), 114 statusCode: 200, 115 data: { 116 'sealed_token': newToken, 117 'access_token': 'some-access-token' 118 }, 119 )); 120 121 when(mockStorage.write(key: storageKey, value: anyNamed('value'))) 122 .thenAnswer((_) async => {}); 123 124 // Act - Refresh token (which saves the updated session) 125 await authService.refreshToken(); 126 127 // Assert - Verify environment-specific key was used for saving 128 verify(mockStorage.write(key: storageKey, value: anyNamed('value'))) 129 .called(1); 130 131 // Verify the generic key was never used 132 verifyNever(mockStorage.write(key: 'coves_session', value: anyNamed('value'))); 133 }); 134 135 test('should delete sessions using environment-specific keys', () async { 136 // Get the current environment's storage key 137 final currentEnv = EnvironmentConfig.current.environment.name; 138 final storageKey = 'coves_session_$currentEnv'; 139 140 // First restore a session 141 const session = CovesSession( 142 token: 'test-token', 143 did: 'did:plc:test123', 144 sessionId: 'session-123', 145 ); 146 147 when(mockStorage.read(key: storageKey)) 148 .thenAnswer((_) async => session.toJsonString()); 149 await authService.restoreSession(); 150 151 // Mock logout 152 when(mockDio.post<void>( 153 '/oauth/logout', 154 options: anyNamed('options'), 155 )).thenAnswer((_) async => Response( 156 requestOptions: RequestOptions(path: '/oauth/logout'), 157 statusCode: 200, 158 )); 159 160 when(mockStorage.delete(key: storageKey)).thenAnswer((_) async => {}); 161 162 // Act - Sign out 163 await authService.signOut(); 164 165 // Assert - Verify environment-specific key was used for deletion 166 verify(mockStorage.delete(key: storageKey)).called(1); 167 168 // Verify the generic key was never used 169 verifyNever(mockStorage.delete(key: 'coves_session')); 170 }); 171 172 test('should document storage key format for both environments', () { 173 // This test serves as documentation for the storage key format 174 // Production key 175 expect('coves_session_production', 'coves_session_production'); 176 177 // Local development key 178 expect('coves_session_local', 'coves_session_local'); 179 180 // This ensures: 181 // 1. Production tokens are stored in 'coves_session_production' 182 // 2. Local dev tokens are stored in 'coves_session_local' 183 // 3. Switching between prod/dev builds doesn't cause token conflicts 184 // 4. Each environment maintains its own session independently 185 }); 186 }); 187}