""" Unit tests for JWT authentication utilities Tests cover token creation, verification, expiration, and error handling. """ import pytest from jose import jwt, JWTError import pendulum from app.utils.auth import create_token, verify_token from app.config import get_settings class TestTokenCreation: """Tests for JWT token creation""" def test_create_token_basic(self): """Test creating a token with valid user data""" user_data = {"user_id": "123", "username": "testuser"} token = create_token(user_data) assert token is not None assert isinstance(token, str) assert len(token) > 0 def test_create_token_includes_user_data(self): """Test token contains all user data""" user_data = { "user_id": "123", "username": "testuser", "discord_id": "456789" } token = create_token(user_data) payload = verify_token(token) assert payload["user_id"] == "123" assert payload["username"] == "testuser" assert payload["discord_id"] == "456789" def test_create_token_includes_expiration(self): """Test token has expiration timestamp""" user_data = {"user_id": "123"} token = create_token(user_data) payload = verify_token(token) assert "exp" in payload assert isinstance(payload["exp"], int) # Verify expiration is ~7 days from now exp_time = pendulum.from_timestamp(payload["exp"]) now = pendulum.now('UTC') diff = exp_time - now assert diff.days >= 6 # Allow for timing variance assert diff.days <= 8 def test_create_token_with_empty_user_data(self): """Test creating token with empty user data""" user_data = {} token = create_token(user_data) assert token is not None payload = verify_token(token) assert "exp" in payload # Should still have expiration def test_create_token_with_complex_data(self): """Test creating token with nested/complex user data""" user_data = { "user_id": "123", "roles": ["player", "admin"], "metadata": {"league": "sba", "team_id": 5} } token = create_token(user_data) payload = verify_token(token) assert payload["user_id"] == "123" assert payload["roles"] == ["player", "admin"] assert payload["metadata"]["league"] == "sba" class TestTokenVerification: """Tests for JWT token verification""" def test_verify_valid_token(self): """Test verifying a valid token""" user_data = {"user_id": "123", "username": "testuser"} token = create_token(user_data) payload = verify_token(token) assert payload["user_id"] == "123" assert payload["username"] == "testuser" def test_verify_invalid_token_raises_error(self): """Test verifying an invalid token raises JWTError""" invalid_token = "invalid.token.here" with pytest.raises(JWTError): verify_token(invalid_token) def test_verify_malformed_token(self): """Test verifying malformed tokens""" malformed_tokens = [ "", "notatoken", "a.b", # Missing part "header.payload", # Missing signature "a.b.c.d", # Too many parts ] for token in malformed_tokens: with pytest.raises(JWTError): verify_token(token) def test_verify_token_wrong_signature(self): """Test verifying token with tampered signature""" user_data = {"user_id": "123"} token = create_token(user_data) # Tamper with signature (last part of JWT) parts = token.split('.') parts[2] = parts[2][:-5] + "WRONG" tampered_token = '.'.join(parts) with pytest.raises(JWTError): verify_token(tampered_token) def test_verify_token_wrong_algorithm(self): """Test token signed with different algorithm fails""" settings = get_settings() user_data = {"user_id": "123"} # Create token with different algorithm payload = { **user_data, "exp": pendulum.now('UTC').add(days=7).int_timestamp } # Try to decode HS512 token as HS256 (should fail) wrong_alg_token = jwt.encode(payload, settings.secret_key, algorithm="HS512") with pytest.raises(JWTError): verify_token(wrong_alg_token) def test_verify_token_wrong_secret_key(self): """Test token signed with different secret fails""" user_data = {"user_id": "123"} # Create token with different secret payload = { **user_data, "exp": pendulum.now('UTC').add(days=7).int_timestamp } wrong_secret_token = jwt.encode(payload, "wrong-secret-key", algorithm="HS256") with pytest.raises(JWTError): verify_token(wrong_secret_token) class TestTokenExpiration: """Tests for token expiration behavior""" def test_expired_token_raises_error(self): """Test that expired token raises JWTError""" settings = get_settings() user_data = {"user_id": "123"} # Create already-expired token (expired 1 day ago) payload = { **user_data, "exp": pendulum.now('UTC').subtract(days=1).int_timestamp } expired_token = jwt.encode(payload, settings.secret_key, algorithm="HS256") with pytest.raises(JWTError): verify_token(expired_token) def test_token_expiration_boundary(self): """Test token expiration at exact boundary""" settings = get_settings() user_data = {"user_id": "123"} # Create token that expires in 1 second payload = { **user_data, "exp": pendulum.now('UTC').add(seconds=1).int_timestamp } short_lived_token = jwt.encode(payload, settings.secret_key, algorithm="HS256") # Should work now result = verify_token(short_lived_token) assert result["user_id"] == "123" # After waiting 2 seconds, should fail import time time.sleep(2) with pytest.raises(JWTError): verify_token(short_lived_token) class TestEdgeCases: """Tests for edge cases and error conditions""" def test_create_token_with_none_value(self): """Test creating token with None as a value""" user_data = {"user_id": "123", "optional_field": None} token = create_token(user_data) payload = verify_token(token) assert payload["user_id"] == "123" assert payload["optional_field"] is None def test_create_token_with_numeric_values(self): """Test creating token with various numeric types""" user_data = { "user_id": 123, # int "team_id": 5, "rating": 3.14 # float } token = create_token(user_data) payload = verify_token(token) assert payload["user_id"] == 123 assert payload["team_id"] == 5 assert payload["rating"] == 3.14 def test_create_token_with_boolean(self): """Test creating token with boolean values""" user_data = {"user_id": "123", "is_admin": True, "is_banned": False} token = create_token(user_data) payload = verify_token(token) assert payload["is_admin"] is True assert payload["is_banned"] is False def test_token_roundtrip(self): """Test complete create -> verify -> create -> verify roundtrip""" original_data = {"user_id": "123", "username": "test"} # First token token1 = create_token(original_data) payload1 = verify_token(token1) # Create new token from payload (excluding exp) payload1.pop("exp") token2 = create_token(payload1) payload2 = verify_token(token2) # Should have same user data assert payload2["user_id"] == original_data["user_id"] assert payload2["username"] == original_data["username"] def test_verify_token_missing_exp(self): """Test token without exp field (invalid)""" settings = get_settings() user_data = {"user_id": "123"} # Create token without exp (manually) token_no_exp = jwt.encode(user_data, settings.secret_key, algorithm="HS256") # Jose will still decode it (exp is optional in JWT spec) # But our tokens should always have exp payload = verify_token(token_no_exp) assert "user_id" in payload