jebin2 commited on
Commit
d9bae79
·
1 Parent(s): c7703f9

feat: add comprehensive auth service test suite

Browse files

- Created test_auth_service.py with 31 test cases
- Tests cover JWT token creation, verification, expiry
- Tests cover Google OAuth integration (mocked)
- Tests cover error handling and edge cases
- All 31 tests passing

Files changed (1) hide show
  1. tests/test_auth_service.py +539 -0
tests/test_auth_service.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test Suite for Auth Service
3
+
4
+ Comprehensive tests for the authentication service including:
5
+ - JWT token creation and verification
6
+ - Token expiry validation
7
+ - Token version checking (logout/invalidation)
8
+ - Google OAuth token verification (mocked)
9
+ - Error handling
10
+ """
11
+
12
+ import pytest
13
+ import os
14
+ from datetime import datetime, timedelta
15
+ from unittest.mock import patch, MagicMock
16
+
17
+ from services.auth_service.jwt_provider import (
18
+ JWTService,
19
+ TokenPayload,
20
+ create_access_token,
21
+ create_refresh_token,
22
+ verify_access_token,
23
+ TokenExpiredError,
24
+ InvalidTokenError,
25
+ ConfigurationError,
26
+ get_jwt_service
27
+ )
28
+ from services.auth_service.google_provider import (
29
+ GoogleAuthService,
30
+ GoogleUserInfo,
31
+ InvalidTokenError as GoogleInvalidTokenError,
32
+ ConfigurationError as GoogleConfigError,
33
+ get_google_auth_service
34
+ )
35
+
36
+
37
+ # ============================================================================
38
+ # Fixtures
39
+ # ============================================================================
40
+
41
+ @pytest.fixture
42
+ def jwt_secret():
43
+ """Provide a test JWT secret."""
44
+ return "test-secret-key-for-testing-only-do-not-use-in-production"
45
+
46
+
47
+ @pytest.fixture
48
+ def jwt_service(jwt_secret):
49
+ """Create a JWTService instance for testing."""
50
+ return JWTService(
51
+ secret_key=jwt_secret,
52
+ algorithm="HS256",
53
+ access_expiry_minutes=15,
54
+ refresh_expiry_days=7
55
+ )
56
+
57
+
58
+ @pytest.fixture
59
+ def google_client_id():
60
+ """Provide a test Google client ID."""
61
+ return "test-google-client-id.apps.googleusercontent.com"
62
+
63
+
64
+ @pytest.fixture
65
+ def mock_google_user_info():
66
+ """Provide mock Google user info."""
67
+ return GoogleUserInfo(
68
+ google_id="12345678901234567890",
69
+ email="test@example.com",
70
+ name="Test User",
71
+ picture="https://example.com/photo.jpg"
72
+ )
73
+
74
+
75
+ # ============================================================================
76
+ # JWT Service Tests
77
+ # ============================================================================
78
+
79
+ class TestJWTService:
80
+ """Test JWT token creation and verification."""
81
+
82
+ def test_service_initialization(self, jwt_secret):
83
+ """Test that JWT service initializes correctly."""
84
+ service = JWTService(
85
+ secret_key=jwt_secret,
86
+ algorithm="HS256",
87
+ access_expiry_minutes=15,
88
+ refresh_expiry_days=7
89
+ )
90
+
91
+ assert service.secret_key == jwt_secret
92
+ assert service.algorithm == "HS256"
93
+ assert service.access_expiry_minutes == 15
94
+ assert service.refresh_expiry_days == 7
95
+
96
+ def test_service_requires_secret(self, monkeypatch):
97
+ """Test that service requires a secret key."""
98
+ # Clear environment variable so it can't fall back to env
99
+ monkeypatch.delenv("JWT_SECRET", raising=False)
100
+
101
+ with pytest.raises(ConfigurationError) as exc_info:
102
+ JWTService(secret_key=None) # None and no env var
103
+
104
+ assert "secret" in str(exc_info.value).lower()
105
+
106
+ def test_service_warns_short_secret(self, caplog):
107
+ """Test that service warns about short secret keys."""
108
+ short_secret = "short"
109
+ service = JWTService(secret_key=short_secret)
110
+
111
+ assert "short" in caplog.text.lower() or "32 chars" in caplog.text.lower()
112
+
113
+ def test_service_from_env(self, monkeypatch, jwt_secret):
114
+ """Test that service reads config from environment."""
115
+ monkeypatch.setenv("JWT_SECRET", jwt_secret)
116
+ monkeypatch.setenv("JWT_ALGORITHM", "HS512")
117
+ monkeypatch.setenv("JWT_ACCESS_EXPIRY_MINUTES", "30")
118
+ monkeypatch.setenv("JWT_REFRESH_EXPIRY_DAYS", "14")
119
+
120
+ service = JWTService()
121
+
122
+ assert service.secret_key == jwt_secret
123
+ assert service.algorithm == "HS512"
124
+ assert service.access_expiry_minutes == 30
125
+ assert service.refresh_expiry_days == 14
126
+
127
+
128
+ class TestAccessTokenCreation:
129
+ """Test access token creation."""
130
+
131
+ def test_create_access_token(self, jwt_service):
132
+ """Test creating an access token."""
133
+ token = jwt_service.create_access_token(
134
+ user_id="usr_123",
135
+ email="test@example.com",
136
+ token_version=1
137
+ )
138
+
139
+ assert isinstance(token, str)
140
+ assert len(token) > 0
141
+ assert token.count('.') == 2 # JWT format: header.payload.signature
142
+
143
+ def test_access_token_payload(self, jwt_service):
144
+ """Test that access token has correct payload."""
145
+ token = jwt_service.create_access_token(
146
+ user_id="usr_123",
147
+ email="test@example.com",
148
+ token_version=1
149
+ )
150
+
151
+ payload = jwt_service.verify_token(token)
152
+
153
+ assert payload.user_id == "usr_123"
154
+ assert payload.email == "test@example.com"
155
+ assert payload.token_version == 1
156
+ assert payload.token_type == "access"
157
+
158
+ def test_access_token_expiry(self, jwt_service):
159
+ """Test that access token has correct expiry time."""
160
+ before = datetime.utcnow()
161
+ token = jwt_service.create_access_token(
162
+ user_id="usr_123",
163
+ email="test@example.com"
164
+ )
165
+ after = datetime.utcnow()
166
+
167
+ payload = jwt_service.verify_token(token)
168
+
169
+ # Should expire 15 minutes from creation (with some tolerance for execution time)
170
+ expected_min = before + timedelta(minutes=15) - timedelta(seconds=1)
171
+ expected_max = after + timedelta(minutes=15) + timedelta(seconds=1)
172
+
173
+ assert expected_min <= payload.expires_at <= expected_max
174
+
175
+ def test_access_token_custom_expiry(self, jwt_service):
176
+ """Test creating token with custom expiry."""
177
+ custom_delta = timedelta(hours=1)
178
+ token = jwt_service.create_token(
179
+ user_id="usr_123",
180
+ email="test@example.com",
181
+ token_type="access",
182
+ expiry_delta=custom_delta
183
+ )
184
+
185
+ payload = jwt_service.verify_token(token)
186
+ time_diff = payload.expires_at - payload.issued_at
187
+
188
+ # Should be approximately 1 hour
189
+ assert 3590 <= time_diff.total_seconds() <= 3610
190
+
191
+ def test_access_token_extra_claims(self, jwt_service):
192
+ """Test creating token with extra claims."""
193
+ token = jwt_service.create_token(
194
+ user_id="usr_123",
195
+ email="test@example.com",
196
+ token_type="access",
197
+ extra_claims={"role": "admin", "org": "test_org"}
198
+ )
199
+
200
+ payload = jwt_service.verify_token(token)
201
+
202
+ assert payload.extra.get("role") == "admin"
203
+ assert payload.extra.get("org") == "test_org"
204
+
205
+
206
+ class TestRefreshTokenCreation:
207
+ """Test refresh token creation."""
208
+
209
+ def test_create_refresh_token(self, jwt_service):
210
+ """Test creating a refresh token."""
211
+ token = jwt_service.create_refresh_token(
212
+ user_id="usr_123",
213
+ email="test@example.com",
214
+ token_version=1
215
+ )
216
+
217
+ assert isinstance(token, str)
218
+ assert len(token) > 0
219
+
220
+ def test_refresh_token_type(self, jwt_service):
221
+ """Test that refresh token has correct type."""
222
+ token = jwt_service.create_refresh_token(
223
+ user_id="usr_123",
224
+ email="test@example.com"
225
+ )
226
+
227
+ payload = jwt_service.verify_token(token)
228
+
229
+ assert payload.token_type == "refresh"
230
+
231
+ def test_refresh_token_longer_expiry(self, jwt_service):
232
+ """Test that refresh token expires in 7 days."""
233
+ before = datetime.utcnow()
234
+ token = jwt_service.create_refresh_token(
235
+ user_id="usr_123",
236
+ email="test@example.com"
237
+ )
238
+
239
+ payload = jwt_service.verify_token(token)
240
+ time_diff = payload.expires_at - before
241
+
242
+ # Should be approximately 7 days
243
+ expected_seconds = 7 * 24 * 60 * 60
244
+ assert abs(time_diff.total_seconds() - expected_seconds) < 10
245
+
246
+
247
+ class TestTokenVerification:
248
+ """Test token verification."""
249
+
250
+ def test_verify_valid_token(self, jwt_service):
251
+ """Test verifying a valid token."""
252
+ token = jwt_service.create_access_token(
253
+ user_id="usr_123",
254
+ email="test@example.com"
255
+ )
256
+
257
+ payload = jwt_service.verify_token(token)
258
+
259
+ assert payload.user_id == "usr_123"
260
+ assert payload.email == "test@example.com"
261
+
262
+ def test_verify_empty_token(self, jwt_service):
263
+ """Test that empty token raises error."""
264
+ with pytest.raises(InvalidTokenError) as exc_info:
265
+ jwt_service.verify_token("")
266
+
267
+ assert "empty" in str(exc_info.value).lower()
268
+
269
+ def test_verify_malformed_token(self, jwt_service):
270
+ """Test that malformed token raises error."""
271
+ with pytest.raises(InvalidTokenError):
272
+ jwt_service.verify_token("not.a.valid.jwt.token")
273
+
274
+ def test_verify_tampered_token(self, jwt_service):
275
+ """Test that tampered token raises error."""
276
+ token = jwt_service.create_access_token(
277
+ user_id="usr_123",
278
+ email="test@example.com"
279
+ )
280
+
281
+ # Tamper with the token
282
+ parts = token.split('.')
283
+ parts[1] = parts[1][:-5] + "AAAAA" # Change payload
284
+ tampered = '.'.join(parts)
285
+
286
+ with pytest.raises(InvalidTokenError):
287
+ jwt_service.verify_token(tampered)
288
+
289
+ def test_verify_token_wrong_secret(self, jwt_service):
290
+ """Test that token with wrong secret fails."""
291
+ # Create token with one secret
292
+ token = jwt_service.create_access_token(
293
+ user_id="usr_123",
294
+ email="test@example.com"
295
+ )
296
+
297
+ # Try to verify with different secret
298
+ wrong_service = JWTService(secret_key="different-secret")
299
+
300
+ with pytest.raises(InvalidTokenError):
301
+ wrong_service.verify_token(token)
302
+
303
+
304
+ class TestTokenExpiry:
305
+ """Test token expiry behavior."""
306
+
307
+ def test_expired_token_raises_error(self, jwt_service):
308
+ """Test that expired token raises TokenExpiredError."""
309
+ # Create token that expires immediately
310
+ token = jwt_service.create_token(
311
+ user_id="usr_123",
312
+ email="test@example.com",
313
+ token_type="access",
314
+ expiry_delta=timedelta(seconds=-1) # Already expired
315
+ )
316
+
317
+ with pytest.raises(TokenExpiredError) as exc_info:
318
+ jwt_service.verify_token(token)
319
+
320
+ assert "expired" in str(exc_info.value).lower()
321
+
322
+ def test_token_not_expired_yet(self, jwt_service):
323
+ """Test that non-expired token verifies successfully."""
324
+ token = jwt_service.create_token(
325
+ user_id="usr_123",
326
+ email="test@example.com",
327
+ token_type="access",
328
+ expiry_delta=timedelta(hours=1)
329
+ )
330
+
331
+ # Should not raise
332
+ payload = jwt_service.verify_token(token)
333
+ assert payload.user_id == "usr_123"
334
+ assert not payload.is_expired
335
+
336
+ def test_token_expiry_property(self, jwt_service):
337
+ """Test TokenPayload.is_expired property."""
338
+ token = jwt_service.create_token(
339
+ user_id="usr_123",
340
+ email="test@example.com",
341
+ expiry_delta=timedelta(seconds=-1)
342
+ )
343
+
344
+ # Decode without verifying expiry
345
+ import jwt as pyjwt
346
+ payload_dict = pyjwt.decode(
347
+ token,
348
+ jwt_service.secret_key,
349
+ algorithms=[jwt_service.algorithm],
350
+ options={"verify_exp": False}
351
+ )
352
+
353
+ payload = TokenPayload(
354
+ user_id=payload_dict["sub"],
355
+ email=payload_dict["email"],
356
+ issued_at=datetime.utcfromtimestamp(payload_dict["iat"]),
357
+ expires_at=datetime.utcfromtimestamp(payload_dict["exp"]),
358
+ token_version=payload_dict.get("tv", 1),
359
+ token_type=payload_dict.get("type", "access")
360
+ )
361
+
362
+ assert payload.is_expired is True
363
+
364
+
365
+ class TestTokenVersion:
366
+ """Test token version functionality."""
367
+
368
+ def test_token_version_in_payload(self, jwt_service):
369
+ """Test that token version is included in payload."""
370
+ token = jwt_service.create_access_token(
371
+ user_id="usr_123",
372
+ email="test@example.com",
373
+ token_version=5
374
+ )
375
+
376
+ payload = jwt_service.verify_token(token)
377
+
378
+ assert payload.token_version == 5
379
+
380
+ def test_default_token_version(self, jwt_service):
381
+ """Test that default token version is 1."""
382
+ token = jwt_service.create_access_token(
383
+ user_id="usr_123",
384
+ email="test@example.com"
385
+ )
386
+
387
+ payload = jwt_service.verify_token(token)
388
+
389
+ assert payload.token_version == 1
390
+
391
+
392
+ class TestConvenienceFunctions:
393
+ """Test module-level convenience functions."""
394
+
395
+ def test_create_access_token_function(self, monkeypatch, jwt_secret):
396
+ """Test create_access_token convenience function."""
397
+ monkeypatch.setenv("JWT_SECRET", jwt_secret)
398
+
399
+ # Reset singleton
400
+ import services.auth_service.jwt_provider as jwt_module
401
+ jwt_module._default_service = None
402
+
403
+ token = create_access_token(
404
+ user_id="usr_123",
405
+ email="test@example.com"
406
+ )
407
+
408
+ assert isinstance(token, str)
409
+ assert len(token) > 0
410
+
411
+ def test_create_refresh_token_function(self, monkeypatch, jwt_secret):
412
+ """Test create_refresh_token convenience function."""
413
+ monkeypatch.setenv("JWT_SECRET", jwt_secret)
414
+
415
+ # Reset singleton
416
+ import services.auth_service.jwt_provider as jwt_module
417
+ jwt_module._default_service = None
418
+
419
+ token = create_refresh_token(
420
+ user_id="usr_123",
421
+ email="test@example.com"
422
+ )
423
+
424
+ assert isinstance(token, str)
425
+ payload_dict = jwt_module.get_jwt_service().verify_token(token)
426
+ assert payload_dict.token_type == "refresh"
427
+
428
+ def test_verify_access_token_function(self, monkeypatch, jwt_secret):
429
+ """Test verify_access_token convenience function."""
430
+ monkeypatch.setenv("JWT_SECRET", jwt_secret)
431
+
432
+ # Reset singleton
433
+ import services.auth_service.jwt_provider as jwt_module
434
+ jwt_module._default_service = None
435
+
436
+ token = create_access_token(
437
+ user_id="usr_123",
438
+ email="test@example.com"
439
+ )
440
+
441
+ payload = verify_access_token(token)
442
+
443
+ assert payload.user_id == "usr_123"
444
+
445
+ def test_get_jwt_service_singleton(self, monkeypatch, jwt_secret):
446
+ """Test that get_jwt_service returns singleton."""
447
+ monkeypatch.setenv("JWT_SECRET", jwt_secret)
448
+
449
+ # Reset singleton
450
+ import services.auth_service.jwt_provider as jwt_module
451
+ jwt_module._default_service = None
452
+
453
+ service1 = get_jwt_service()
454
+ service2 = get_jwt_service()
455
+
456
+ assert service1 is service2 # Same instance
457
+
458
+
459
+ # ============================================================================
460
+ # Google OAuth Tests
461
+ # ============================================================================
462
+
463
+ class TestGoogleAuthService:
464
+ """Test Google OAuth integration."""
465
+
466
+ def test_service_initialization(self, google_client_id):
467
+ """Test Google auth service initialization."""
468
+ service = GoogleAuthService(client_id=google_client_id)
469
+
470
+ assert service.client_id == google_client_id
471
+
472
+ def test_service_requires_client_id(self, monkeypatch):
473
+ """Test that service requires client ID."""
474
+ # Clear environment variable so it can't fall back to env
475
+ monkeypatch.delenv("AUTH_SIGN_IN_GOOGLE_CLIENT_ID", raising=False)
476
+ monkeypatch.delenv("GOOGLE_CLIENT_ID", raising=False)
477
+
478
+ with pytest.raises(GoogleConfigError) as exc_info:
479
+ GoogleAuthService(client_id=None) # None and no env var
480
+
481
+ assert "client id" in str(exc_info.value).lower()
482
+
483
+ @patch('google.oauth2.id_token.verify_oauth2_token')
484
+ def test_verify_valid_token(self, mock_verify, google_client_id, mock_google_user_info):
485
+ """Test verifying valid Google ID token."""
486
+ # Mock the Google verification
487
+ mock_verify.return_value = {
488
+ 'sub': mock_google_user_info.google_id,
489
+ 'email': mock_google_user_info.email,
490
+ 'name': mock_google_user_info.name,
491
+ 'picture': mock_google_user_info.picture,
492
+ 'iss': 'accounts.google.com',
493
+ 'aud': google_client_id
494
+ }
495
+
496
+ service = GoogleAuthService(client_id=google_client_id)
497
+ user_info = service.verify_token("fake-google-id-token")
498
+
499
+ assert user_info.google_id == mock_google_user_info.google_id
500
+ assert user_info.email == mock_google_user_info.email
501
+ assert user_info.name == mock_google_user_info.name
502
+ assert user_info.picture == mock_google_user_info.picture
503
+
504
+ @patch('google.oauth2.id_token.verify_oauth2_token')
505
+ def test_verify_invalid_token(self, mock_verify, google_client_id):
506
+ """Test that invalid token raises error."""
507
+ # Mock verification failure
508
+ mock_verify.side_effect = ValueError("Invalid token")
509
+
510
+ service = GoogleAuthService(client_id=google_client_id)
511
+
512
+ with pytest.raises(GoogleInvalidTokenError) as exc_info:
513
+ service.verify_token("invalid-token")
514
+
515
+ assert "invalid" in str(exc_info.value).lower()
516
+
517
+ @patch('google.oauth2.id_token.verify_oauth2_token')
518
+ def test_verify_wrong_audience(self, mock_verify, google_client_id):
519
+ """Test that token with wrong audience fails."""
520
+ # Mock token with wrong audience
521
+ mock_verify.return_value = {
522
+ 'sub': '12345',
523
+ 'email': 'test@example.com',
524
+ 'iss': 'accounts.google.com',
525
+ 'aud': 'wrong-client-id'
526
+ }
527
+
528
+ service = GoogleAuthService(client_id=google_client_id)
529
+
530
+ with pytest.raises(GoogleInvalidTokenError):
531
+ service.verify_token("token-for-wrong-app")
532
+
533
+
534
+ # ============================================================================
535
+ # Run Tests
536
+ # ============================================================================
537
+
538
+ if __name__ == "__main__":
539
+ pytest.main([__file__, "-v", "--tb=short"])