1import 'package:coves_flutter/models/coves_session.dart'; 2import 'package:coves_flutter/providers/auth_provider.dart'; 3import 'package:coves_flutter/services/coves_auth_service.dart'; 4import 'package:flutter_test/flutter_test.dart'; 5import 'package:mockito/annotations.dart'; 6import 'package:mockito/mockito.dart'; 7 8import 'auth_provider_test.mocks.dart'; 9 10// Generate mocks for CovesAuthService 11@GenerateMocks([CovesAuthService]) 12void main() { 13 TestWidgetsFlutterBinding.ensureInitialized(); 14 15 group('AuthProvider', () { 16 late AuthProvider authProvider; 17 late MockCovesAuthService mockAuthService; 18 19 setUp(() { 20 // Create mock auth service 21 mockAuthService = MockCovesAuthService(); 22 23 // Create auth provider with injected mock service 24 authProvider = AuthProvider(authService: mockAuthService); 25 }); 26 27 group('initialize', () { 28 test('should initialize with no stored session', () async { 29 when(mockAuthService.initialize()).thenAnswer((_) async => {}); 30 when(mockAuthService.restoreSession()).thenAnswer((_) async => null); 31 32 await authProvider.initialize(); 33 34 expect(authProvider.isAuthenticated, false); 35 expect(authProvider.isLoading, false); 36 expect(authProvider.session, null); 37 expect(authProvider.error, null); 38 }); 39 40 test('should restore session if available', () async { 41 const mockSession = CovesSession( 42 token: 'mock_sealed_token', 43 did: 'did:plc:test123', 44 sessionId: 'session123', 45 handle: 'test.user', 46 ); 47 48 when(mockAuthService.initialize()).thenAnswer((_) async => {}); 49 when( 50 mockAuthService.restoreSession(), 51 ).thenAnswer((_) async => mockSession); 52 53 await authProvider.initialize(); 54 55 expect(authProvider.isAuthenticated, true); 56 expect(authProvider.did, 'did:plc:test123'); 57 expect(authProvider.handle, 'test.user'); 58 }); 59 60 test('should handle initialization errors gracefully', () async { 61 when(mockAuthService.initialize()).thenThrow(Exception('Init failed')); 62 63 await authProvider.initialize(); 64 65 expect(authProvider.isAuthenticated, false); 66 expect(authProvider.error, isNotNull); 67 expect(authProvider.isLoading, false); 68 }); 69 }); 70 71 group('signIn', () { 72 test('should sign in successfully with valid handle', () async { 73 const mockSession = CovesSession( 74 token: 'mock_sealed_token', 75 did: 'did:plc:test123', 76 sessionId: 'session123', 77 handle: 'alice.bsky.social', 78 ); 79 80 when( 81 mockAuthService.signIn('alice.bsky.social'), 82 ).thenAnswer((_) async => mockSession); 83 84 await authProvider.signIn('alice.bsky.social'); 85 86 expect(authProvider.isAuthenticated, true); 87 expect(authProvider.did, 'did:plc:test123'); 88 expect(authProvider.handle, 'alice.bsky.social'); 89 expect(authProvider.error, null); 90 }); 91 92 test('should reject empty handle', () async { 93 expect(() => authProvider.signIn(''), throwsA(isA<Exception>())); 94 95 expect(authProvider.isAuthenticated, false); 96 }); 97 98 test('should handle sign in errors', () async { 99 when( 100 mockAuthService.signIn('invalid.handle'), 101 ).thenThrow(Exception('Sign in failed')); 102 103 expect( 104 () => authProvider.signIn('invalid.handle'), 105 throwsA(isA<Exception>()), 106 ); 107 108 expect(authProvider.isAuthenticated, false); 109 expect(authProvider.error, isNotNull); 110 }); 111 }); 112 113 group('signOut', () { 114 test('should sign out and clear state', () async { 115 // First sign in 116 const mockSession = CovesSession( 117 token: 'mock_sealed_token', 118 did: 'did:plc:test123', 119 sessionId: 'session123', 120 handle: 'alice.bsky.social', 121 ); 122 when( 123 mockAuthService.signIn('alice.bsky.social'), 124 ).thenAnswer((_) async => mockSession); 125 126 await authProvider.signIn('alice.bsky.social'); 127 expect(authProvider.isAuthenticated, true); 128 129 // Then sign out 130 when(mockAuthService.signOut()).thenAnswer((_) async => {}); 131 132 await authProvider.signOut(); 133 134 expect(authProvider.isAuthenticated, false); 135 expect(authProvider.session, null); 136 expect(authProvider.did, null); 137 expect(authProvider.handle, null); 138 }); 139 140 test('should clear state even if server revocation fails', () async { 141 // Sign in first 142 const mockSession = CovesSession( 143 token: 'mock_sealed_token', 144 did: 'did:plc:test123', 145 sessionId: 'session123', 146 handle: 'alice.bsky.social', 147 ); 148 when( 149 mockAuthService.signIn('alice.bsky.social'), 150 ).thenAnswer((_) async => mockSession); 151 152 await authProvider.signIn('alice.bsky.social'); 153 154 // Sign out with error 155 when(mockAuthService.signOut()) 156 .thenThrow(Exception('Revocation failed')); 157 158 await authProvider.signOut(); 159 160 expect(authProvider.isAuthenticated, false); 161 expect(authProvider.session, null); 162 }); 163 }); 164 165 group('getAccessToken', () { 166 test('should return null when not authenticated', () async { 167 final token = await authProvider.getAccessToken(); 168 expect(token, null); 169 }); 170 171 test('should return sealed token when authenticated', () async { 172 const mockSession = CovesSession( 173 token: 'mock_sealed_token', 174 did: 'did:plc:test123', 175 sessionId: 'session123', 176 ); 177 178 when( 179 mockAuthService.signIn('alice.bsky.social'), 180 ).thenAnswer((_) async => mockSession); 181 182 await authProvider.signIn('alice.bsky.social'); 183 184 final token = await authProvider.getAccessToken(); 185 expect(token, 'mock_sealed_token'); 186 }); 187 }); 188 189 group('refreshToken', () { 190 test('should return false when not authenticated', () async { 191 final result = await authProvider.refreshToken(); 192 expect(result, false); 193 }); 194 195 test('should refresh token successfully', () async { 196 const mockSession = CovesSession( 197 token: 'mock_sealed_token', 198 did: 'did:plc:test123', 199 sessionId: 'session123', 200 ); 201 const refreshedSession = CovesSession( 202 token: 'new_sealed_token', 203 did: 'did:plc:test123', 204 sessionId: 'session123', 205 ); 206 207 when( 208 mockAuthService.signIn('alice.bsky.social'), 209 ).thenAnswer((_) async => mockSession); 210 when( 211 mockAuthService.refreshToken(), 212 ).thenAnswer((_) async => refreshedSession); 213 214 await authProvider.signIn('alice.bsky.social'); 215 final result = await authProvider.refreshToken(); 216 217 expect(result, true); 218 expect(authProvider.session?.token, 'new_sealed_token'); 219 }); 220 221 test('should sign out if refresh fails', () async { 222 const mockSession = CovesSession( 223 token: 'mock_sealed_token', 224 did: 'did:plc:test123', 225 sessionId: 'session123', 226 ); 227 228 when( 229 mockAuthService.signIn('alice.bsky.social'), 230 ).thenAnswer((_) async => mockSession); 231 when( 232 mockAuthService.refreshToken(), 233 ).thenThrow(Exception('Refresh failed')); 234 when(mockAuthService.signOut()).thenAnswer((_) async => {}); 235 236 await authProvider.signIn('alice.bsky.social'); 237 final result = await authProvider.refreshToken(); 238 239 expect(result, false); 240 expect(authProvider.isAuthenticated, false); 241 }); 242 }); 243 244 group('State Management', () { 245 test('should notify listeners on state change', () async { 246 var notificationCount = 0; 247 authProvider.addListener(() { 248 notificationCount++; 249 }); 250 251 const mockSession = CovesSession( 252 token: 'mock_sealed_token', 253 did: 'did:plc:test123', 254 sessionId: 'session123', 255 ); 256 when( 257 mockAuthService.signIn('alice.bsky.social'), 258 ).thenAnswer((_) async => mockSession); 259 260 await authProvider.signIn('alice.bsky.social'); 261 262 // Should notify during sign in process 263 expect(notificationCount, greaterThan(0)); 264 }); 265 266 test('should clear error when clearError is called', () { 267 // Trigger an error state 268 authProvider.clearError(); 269 expect(authProvider.error, null); 270 }); 271 }); 272 }); 273}