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