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(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}