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(
156 mockAuthService.signOut(),
157 ).thenThrow(Exception('Revocation failed'));
158
159 await authProvider.signOut();
160
161 expect(authProvider.isAuthenticated, false);
162 expect(authProvider.session, null);
163 });
164 });
165
166 group('getAccessToken', () {
167 test('should return null when not authenticated', () async {
168 final token = await authProvider.getAccessToken();
169 expect(token, null);
170 });
171
172 test('should return sealed token when authenticated', () async {
173 const mockSession = CovesSession(
174 token: 'mock_sealed_token',
175 did: 'did:plc:test123',
176 sessionId: 'session123',
177 );
178
179 when(
180 mockAuthService.signIn('alice.bsky.social'),
181 ).thenAnswer((_) async => mockSession);
182
183 await authProvider.signIn('alice.bsky.social');
184
185 final token = await authProvider.getAccessToken();
186 expect(token, 'mock_sealed_token');
187 });
188 });
189
190 group('refreshToken', () {
191 test('should return false when not authenticated', () async {
192 final result = await authProvider.refreshToken();
193 expect(result, false);
194 });
195
196 test('should refresh token successfully', () async {
197 const mockSession = CovesSession(
198 token: 'mock_sealed_token',
199 did: 'did:plc:test123',
200 sessionId: 'session123',
201 );
202 const refreshedSession = CovesSession(
203 token: 'new_sealed_token',
204 did: 'did:plc:test123',
205 sessionId: 'session123',
206 );
207
208 when(
209 mockAuthService.signIn('alice.bsky.social'),
210 ).thenAnswer((_) async => mockSession);
211 when(
212 mockAuthService.refreshToken(),
213 ).thenAnswer((_) async => refreshedSession);
214
215 await authProvider.signIn('alice.bsky.social');
216 final result = await authProvider.refreshToken();
217
218 expect(result, true);
219 expect(authProvider.session?.token, 'new_sealed_token');
220 });
221
222 test('should sign out if refresh fails', () async {
223 const mockSession = CovesSession(
224 token: 'mock_sealed_token',
225 did: 'did:plc:test123',
226 sessionId: 'session123',
227 );
228
229 when(
230 mockAuthService.signIn('alice.bsky.social'),
231 ).thenAnswer((_) async => mockSession);
232 when(
233 mockAuthService.refreshToken(),
234 ).thenThrow(Exception('Refresh failed'));
235 when(mockAuthService.signOut()).thenAnswer((_) async => {});
236
237 await authProvider.signIn('alice.bsky.social');
238 final result = await authProvider.refreshToken();
239
240 expect(result, false);
241 expect(authProvider.isAuthenticated, false);
242 });
243 });
244
245 group('State Management', () {
246 test('should notify listeners on state change', () async {
247 var notificationCount = 0;
248 authProvider.addListener(() {
249 notificationCount++;
250 });
251
252 const mockSession = CovesSession(
253 token: 'mock_sealed_token',
254 did: 'did:plc:test123',
255 sessionId: 'session123',
256 );
257 when(
258 mockAuthService.signIn('alice.bsky.social'),
259 ).thenAnswer((_) async => mockSession);
260
261 await authProvider.signIn('alice.bsky.social');
262
263 // Should notify during sign in process
264 expect(notificationCount, greaterThan(0));
265 });
266
267 test('should clear error when clearError is called', () {
268 // Trigger an error state
269 authProvider.clearError();
270 expect(authProvider.error, null);
271 });
272 });
273 });
274}