Skip to content

Commit f75d672

Browse files
committed
test: add AuthService tests
- Tests initiateEmailSignIn - Tests completeEmailSignIn - Tests anonymous sign-in - Covers error scenarios
1 parent aa8f04d commit f75d672

File tree

1 file changed

+351
-0
lines changed

1 file changed

+351
-0
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
import 'package:ht_api/src/services/auth_service.dart';
2+
import 'package:ht_api/src/services/auth_token_service.dart';
3+
import 'package:ht_api/src/services/verification_code_storage_service.dart';
4+
import 'package:ht_data_repository/ht_data_repository.dart';
5+
import 'package:ht_email_repository/ht_email_repository.dart';
6+
import 'package:ht_shared/ht_shared.dart';
7+
import 'package:mocktail/mocktail.dart';
8+
import 'package:test/test.dart';
9+
import 'package:uuid/uuid.dart';
10+
11+
import '../../helpers/mock_classes.dart'; // Import mock classes
12+
13+
void main() {
14+
group('AuthService', () {
15+
late AuthService service;
16+
late MockUserRepository mockUserRepository;
17+
late MockAuthTokenService mockAuthTokenService;
18+
late MockVerificationCodeStorageService mockVerificationCodeStorageService;
19+
late MockEmailRepository mockEmailRepository;
20+
late MockUuid mockUuid;
21+
22+
const testEmail = '[email protected]';
23+
const testCode = '123456';
24+
const testUserId = 'user-id-123';
25+
const testToken = 'auth-token-xyz';
26+
const testUuidValue = 'generated-uuid-v4';
27+
28+
final testUser = User(id: testUserId, email: testEmail, isAnonymous: false);
29+
final testAnonymousUser = User(id: testUserId, isAnonymous: true);
30+
final paginatedResponseSingleUser = PaginatedResponse<User>(
31+
items: [testUser],
32+
cursor: null,
33+
hasMore: false,
34+
);
35+
final paginatedResponseEmpty = PaginatedResponse<User>(
36+
items: [],
37+
cursor: null,
38+
hasMore: false,
39+
);
40+
41+
42+
setUpAll(() {
43+
// Register fallback values for argument matchers used in verify/when
44+
registerFallbackValue(User(id: 'fallback', isAnonymous: true));
45+
registerFallbackValue(<String, dynamic>{}); // For query map
46+
});
47+
48+
setUp(() {
49+
mockUserRepository = MockUserRepository();
50+
mockAuthTokenService = MockAuthTokenService();
51+
mockVerificationCodeStorageService = MockVerificationCodeStorageService();
52+
mockEmailRepository = MockEmailRepository();
53+
mockUuid = MockUuid();
54+
55+
service = AuthService(
56+
userRepository: mockUserRepository,
57+
authTokenService: mockAuthTokenService,
58+
verificationCodeStorageService: mockVerificationCodeStorageService,
59+
emailRepository: mockEmailRepository,
60+
uuidGenerator: mockUuid,
61+
);
62+
63+
// Common stubs
64+
when(() => mockUuid.v4()).thenReturn(testUuidValue);
65+
when(
66+
() => mockVerificationCodeStorageService.generateAndStoreCode(
67+
any(),
68+
expiry: any(named: 'expiry'),
69+
),
70+
).thenAnswer((_) async => testCode);
71+
when(
72+
() => mockEmailRepository.sendOtpEmail(
73+
recipientEmail: any(named: 'recipientEmail'),
74+
otpCode: any(named: 'otpCode'),
75+
),
76+
).thenAnswer((_) async {});
77+
when(() => mockAuthTokenService.generateToken(any()))
78+
.thenAnswer((_) async => testToken);
79+
when(() => mockUserRepository.create(any()))
80+
.thenAnswer((invocation) async => invocation.positionalArguments[0] as User);
81+
// Default stub for user lookup (found)
82+
when(() => mockUserRepository.readAllByQuery(any()))
83+
.thenAnswer((_) async => paginatedResponseSingleUser);
84+
// Default stub for code validation (valid)
85+
when(() => mockVerificationCodeStorageService.validateCode(any(), any()))
86+
.thenAnswer((_) async => true);
87+
});
88+
89+
group('initiateEmailSignIn', () {
90+
test('successfully generates, stores, and sends code', () async {
91+
// Act
92+
await service.initiateEmailSignIn(testEmail);
93+
94+
// Assert
95+
verify(() => mockVerificationCodeStorageService.generateAndStoreCode(
96+
testEmail,
97+
expiry: any(named: 'expiry'),
98+
)).called(1);
99+
verify(() => mockEmailRepository.sendOtpEmail(
100+
recipientEmail: testEmail,
101+
otpCode: testCode,
102+
)).called(1);
103+
});
104+
105+
test('throws OperationFailedException if code storage fails', () async {
106+
// Arrange
107+
final exception = OperationFailedException('Storage failed');
108+
when(() => mockVerificationCodeStorageService.generateAndStoreCode(any(), expiry: any(named: 'expiry')))
109+
.thenThrow(exception);
110+
111+
// Act & Assert
112+
await expectLater(
113+
() => service.initiateEmailSignIn(testEmail),
114+
throwsA(isA<OperationFailedException>().having(
115+
(e) => e.message, 'message', 'Failed to initiate email sign-in process.'
116+
)),
117+
);
118+
verifyNever(() => mockEmailRepository.sendOtpEmail(recipientEmail: any(named: 'recipientEmail'), otpCode: any(named: 'otpCode')));
119+
});
120+
121+
test('rethrows HtHttpException from email repository', () async {
122+
// Arrange
123+
final exception = ServerException('Email service unavailable');
124+
when(() => mockEmailRepository.sendOtpEmail(recipientEmail: any(named: 'recipientEmail'), otpCode: any(named: 'otpCode')))
125+
.thenThrow(exception);
126+
127+
// Act & Assert
128+
await expectLater(
129+
() => service.initiateEmailSignIn(testEmail),
130+
throwsA(isA<ServerException>()),
131+
);
132+
verify(() => mockVerificationCodeStorageService.generateAndStoreCode(testEmail, expiry: any(named: 'expiry'))).called(1);
133+
});
134+
135+
test('throws OperationFailedException if email sending fails unexpectedly', () async {
136+
// Arrange
137+
final exception = Exception('SMTP error');
138+
when(() => mockEmailRepository.sendOtpEmail(recipientEmail: any(named: 'recipientEmail'), otpCode: any(named: 'otpCode')))
139+
.thenThrow(exception);
140+
141+
// Act & Assert
142+
await expectLater(
143+
() => service.initiateEmailSignIn(testEmail),
144+
throwsA(isA<OperationFailedException>().having(
145+
(e) => e.message, 'message', 'Failed to initiate email sign-in process.'
146+
)),
147+
);
148+
});
149+
});
150+
151+
group('completeEmailSignIn', () {
152+
test('successfully verifies code, finds existing user, generates token', () async {
153+
// Arrange: User lookup returns existing user
154+
when(() => mockUserRepository.readAllByQuery({'email': testEmail}))
155+
.thenAnswer((_) async => paginatedResponseSingleUser);
156+
157+
// Act
158+
final result = await service.completeEmailSignIn(testEmail, testCode);
159+
160+
// Assert
161+
expect(result.user, equals(testUser));
162+
expect(result.token, equals(testToken));
163+
verify(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode)).called(1);
164+
verify(() => mockUserRepository.readAllByQuery({'email': testEmail})).called(1);
165+
verifyNever(() => mockUserRepository.create(any()));
166+
verify(() => mockAuthTokenService.generateToken(testUser)).called(1);
167+
});
168+
169+
test('successfully verifies code, creates new user, generates token', () async {
170+
// Arrange: User lookup returns empty
171+
when(() => mockUserRepository.readAllByQuery({'email': testEmail}))
172+
.thenAnswer((_) async => paginatedResponseEmpty);
173+
// Arrange: Mock user creation to return the created user with generated ID
174+
final newUser = User(id: testUuidValue, email: testEmail, isAnonymous: false);
175+
when(() => mockUserRepository.create(any(that: isA<User>())))
176+
.thenAnswer((inv) async => inv.positionalArguments[0] as User);
177+
// Arrange: Mock token generation for the new user
178+
when(() => mockAuthTokenService.generateToken(newUser))
179+
.thenAnswer((_) async => testToken);
180+
181+
182+
// Act
183+
final result = await service.completeEmailSignIn(testEmail, testCode);
184+
185+
// Assert
186+
expect(result.user.id, equals(testUuidValue));
187+
expect(result.user.email, equals(testEmail));
188+
expect(result.user.isAnonymous, isFalse);
189+
expect(result.token, equals(testToken));
190+
verify(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode)).called(1);
191+
verify(() => mockUserRepository.readAllByQuery({'email': testEmail})).called(1);
192+
// Verify create was called with correct details (except ID)
193+
verify(() => mockUserRepository.create(
194+
any(that: predicate<User>((u) => u.email == testEmail && !u.isAnonymous)),
195+
)).called(1);
196+
verify(() => mockAuthTokenService.generateToken(result.user)).called(1);
197+
});
198+
199+
test('throws InvalidInputException if code validation fails', () async {
200+
// Arrange
201+
when(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode))
202+
.thenAnswer((_) async => false);
203+
204+
// Act & Assert
205+
await expectLater(
206+
() => service.completeEmailSignIn(testEmail, testCode),
207+
throwsA(isA<InvalidInputException>().having(
208+
(e) => e.message, 'message', 'Invalid or expired verification code.'
209+
)),
210+
);
211+
verifyNever(() => mockUserRepository.readAllByQuery(any()));
212+
verifyNever(() => mockUserRepository.create(any()));
213+
verifyNever(() => mockAuthTokenService.generateToken(any()));
214+
});
215+
216+
test('throws OperationFailedException if user lookup fails', () async {
217+
// Arrange
218+
final exception = ServerException('DB error');
219+
when(() => mockUserRepository.readAllByQuery(any())).thenThrow(exception);
220+
221+
// Act & Assert
222+
await expectLater(
223+
() => service.completeEmailSignIn(testEmail, testCode),
224+
throwsA(isA<OperationFailedException>().having(
225+
(e) => e.message, 'message', 'Failed to find or create user account.'
226+
)),
227+
);
228+
verify(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode)).called(1);
229+
verifyNever(() => mockUserRepository.create(any()));
230+
verifyNever(() => mockAuthTokenService.generateToken(any()));
231+
});
232+
233+
test('throws OperationFailedException if user creation fails', () async {
234+
// Arrange: User lookup returns empty
235+
when(() => mockUserRepository.readAllByQuery({'email': testEmail}))
236+
.thenAnswer((_) async => paginatedResponseEmpty);
237+
// Arrange: Mock user creation to throw
238+
final exception = ServerException('DB constraint violation');
239+
when(() => mockUserRepository.create(any(that: isA<User>())))
240+
.thenThrow(exception);
241+
242+
// Act & Assert
243+
await expectLater(
244+
() => service.completeEmailSignIn(testEmail, testCode),
245+
throwsA(isA<OperationFailedException>().having(
246+
(e) => e.message, 'message', 'Failed to find or create user account.'
247+
)),
248+
);
249+
verify(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode)).called(1);
250+
verify(() => mockUserRepository.readAllByQuery({'email': testEmail})).called(1);
251+
verify(() => mockUserRepository.create(any(that: isA<User>()))).called(1);
252+
verifyNever(() => mockAuthTokenService.generateToken(any()));
253+
});
254+
255+
256+
test('throws OperationFailedException if token generation fails', () async {
257+
// Arrange: User lookup succeeds
258+
when(() => mockUserRepository.readAllByQuery({'email': testEmail}))
259+
.thenAnswer((_) async => paginatedResponseSingleUser);
260+
// Arrange: Token generation throws
261+
final exception = Exception('Token signing error');
262+
when(() => mockAuthTokenService.generateToken(testUser)).thenThrow(exception);
263+
264+
// Act & Assert
265+
await expectLater(
266+
() => service.completeEmailSignIn(testEmail, testCode),
267+
throwsA(isA<OperationFailedException>().having(
268+
(e) => e.message, 'message', 'Failed to generate authentication token.'
269+
)),
270+
);
271+
verify(() => mockVerificationCodeStorageService.validateCode(testEmail, testCode)).called(1);
272+
verify(() => mockUserRepository.readAllByQuery({'email': testEmail})).called(1);
273+
verifyNever(() => mockUserRepository.create(any()));
274+
verify(() => mockAuthTokenService.generateToken(testUser)).called(1);
275+
});
276+
});
277+
278+
group('performAnonymousSignIn', () {
279+
test('successfully creates anonymous user and generates token', () async {
280+
// Arrange: Mock user creation for anonymous user
281+
final anonymousUser = User(id: testUuidValue, isAnonymous: true);
282+
when(() => mockUserRepository.create(any(that: predicate<User>((u) => u.isAnonymous))))
283+
.thenAnswer((inv) async => inv.positionalArguments[0] as User);
284+
// Arrange: Mock token generation for anonymous user
285+
when(() => mockAuthTokenService.generateToken(anonymousUser))
286+
.thenAnswer((_) async => testToken);
287+
288+
// Act
289+
final result = await service.performAnonymousSignIn();
290+
291+
// Assert
292+
expect(result.user.id, equals(testUuidValue));
293+
expect(result.user.isAnonymous, isTrue);
294+
expect(result.user.email, isNull);
295+
expect(result.token, equals(testToken));
296+
verify(() => mockUserRepository.create(any(that: predicate<User>((u) => u.isAnonymous)))).called(1);
297+
verify(() => mockAuthTokenService.generateToken(result.user)).called(1);
298+
});
299+
300+
test('throws OperationFailedException if user creation fails', () async {
301+
// Arrange
302+
final exception = ServerException('DB error');
303+
when(() => mockUserRepository.create(any(that: predicate<User>((u) => u.isAnonymous))))
304+
.thenThrow(exception);
305+
306+
// Act & Assert
307+
await expectLater(
308+
() => service.performAnonymousSignIn(),
309+
throwsA(isA<OperationFailedException>().having(
310+
(e) => e.message, 'message', 'Failed to create anonymous user.'
311+
)),
312+
);
313+
verifyNever(() => mockAuthTokenService.generateToken(any()));
314+
});
315+
316+
test('throws OperationFailedException if token generation fails', () async {
317+
// Arrange: User creation succeeds
318+
final anonymousUser = User(id: testUuidValue, isAnonymous: true);
319+
when(() => mockUserRepository.create(any(that: predicate<User>((u) => u.isAnonymous))))
320+
.thenAnswer((_) async => anonymousUser);
321+
// Arrange: Token generation fails
322+
final exception = Exception('Token signing error');
323+
when(() => mockAuthTokenService.generateToken(anonymousUser)).thenThrow(exception);
324+
325+
326+
// Act & Assert
327+
await expectLater(
328+
() => service.performAnonymousSignIn(),
329+
throwsA(isA<OperationFailedException>().having(
330+
(e) => e.message, 'message', 'Failed to generate authentication token.'
331+
)),
332+
);
333+
verify(() => mockUserRepository.create(any(that: predicate<User>((u) => u.isAnonymous)))).called(1);
334+
verify(() => mockAuthTokenService.generateToken(anonymousUser)).called(1);
335+
});
336+
});
337+
338+
group('performSignOut', () {
339+
test('completes successfully (placeholder)', () async {
340+
// Act & Assert
341+
await expectLater(
342+
() => service.performSignOut(userId: testUserId),
343+
completes, // Expect no errors for the placeholder
344+
);
345+
// Verify no dependencies are called in the current placeholder impl
346+
verifyNever(() => mockAuthTokenService.validateToken(any()));
347+
verifyNever(() => mockUserRepository.read(any()));
348+
});
349+
});
350+
});
351+
}

0 commit comments

Comments
 (0)