Main coves client
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}