"""Tests for JWT utilities - T14. Tests for JWT token creation, decoding, and verification. """ from datetime import datetime, timedelta, timezone import pytest from jose import JWTError, jwt pytestmark = [pytest.mark.unit, pytest.mark.security] class TestJWTCreateAccessToken: """Test suite for create_access_token function.""" def test_create_access_token_returns_string(self, jwt_secret): """Test that create_access_token returns a string token.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token data = {"sub": "user123"} # Act token = create_access_token(data, secret_key=jwt_secret) # Assert assert isinstance(token, str) assert len(token) > 0 def test_create_access_token_contains_payload(self, jwt_secret): """Test that token contains the original payload data.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123", "email": "test@example.com"} # Act token = create_access_token(data, secret_key=jwt_secret) decoded = decode_access_token(token, secret_key=jwt_secret) # Assert assert decoded["sub"] == "user123" assert decoded["email"] == "test@example.com" def test_create_access_token_includes_exp(self, jwt_secret): """Test that token includes expiration claim.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} # Act token = create_access_token(data, secret_key=jwt_secret) decoded = decode_access_token(token, secret_key=jwt_secret) # Assert assert "exp" in decoded assert isinstance(decoded["exp"], int) def test_create_access_token_includes_iat(self, jwt_secret): """Test that token includes issued-at claim.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} # Act token = create_access_token(data, secret_key=jwt_secret) decoded = decode_access_token(token, secret_key=jwt_secret) # Assert assert "iat" in decoded assert isinstance(decoded["iat"], int) def test_create_access_token_with_custom_expiration(self, jwt_secret): """Test token creation with custom expiration delta.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} expires_delta = timedelta(hours=1) # Act token = create_access_token(data, expires_delta=expires_delta, secret_key=jwt_secret) decoded = decode_access_token(token, secret_key=jwt_secret) # Assert exp_timestamp = decoded["exp"] iat_timestamp = decoded["iat"] exp_duration = exp_timestamp - iat_timestamp assert exp_duration == 3600 # 1 hour in seconds def test_create_access_token_default_expiration(self, jwt_secret): """Test token creation with default expiration (24 hours).""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} # Act token = create_access_token(data, secret_key=jwt_secret) decoded = decode_access_token(token, secret_key=jwt_secret) # Assert exp_timestamp = decoded["exp"] iat_timestamp = decoded["iat"] exp_duration = exp_timestamp - iat_timestamp assert exp_duration == 86400 # 24 hours in seconds class TestJWTDecodeAccessToken: """Test suite for decode_access_token function.""" def test_decode_valid_token_returns_payload(self, jwt_secret): """Test decoding a valid token returns the payload.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123", "role": "admin"} token = create_access_token(data, secret_key=jwt_secret) # Act decoded = decode_access_token(token, secret_key=jwt_secret) # Assert assert decoded["sub"] == "user123" assert decoded["role"] == "admin" def test_decode_expired_token_raises_error(self, jwt_secret): """Test decoding an expired token raises JWTError.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} # Create token that expired 1 hour ago expires_delta = timedelta(hours=-1) token = create_access_token(data, expires_delta=expires_delta, secret_key=jwt_secret) # Act & Assert with pytest.raises(JWTError): decode_access_token(token, secret_key=jwt_secret) def test_decode_invalid_signature_raises_error(self, jwt_secret): """Test decoding token with wrong secret raises JWTError.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} token = create_access_token(data, secret_key=jwt_secret) # Act & Assert with pytest.raises(JWTError): decode_access_token(token, secret_key="wrong-secret-key-32-chars-long!") def test_decode_malformed_token_raises_error(self, jwt_secret): """Test decoding malformed token raises JWTError.""" # Arrange from src.openrouter_monitor.services.jwt import decode_access_token # Act & Assert with pytest.raises(JWTError): decode_access_token("invalid-token", secret_key=jwt_secret) class TestJWTVerifyToken: """Test suite for verify_token function.""" def test_verify_valid_token_returns_token_data(self, jwt_secret): """Test verifying valid token returns TokenData.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, verify_token data = {"sub": "user123"} token = create_access_token(data, secret_key=jwt_secret) # Act token_data = verify_token(token, secret_key=jwt_secret) # Assert assert token_data.user_id == "user123" assert token_data.exp is not None def test_verify_token_without_sub_raises_error(self, jwt_secret): """Test verifying token without sub claim raises error.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, verify_token data = {"email": "test@example.com"} # No sub token = create_access_token(data, secret_key=jwt_secret) # Act & Assert with pytest.raises(JWTError): verify_token(token, secret_key=jwt_secret) def test_verify_expired_token_raises_error(self, jwt_secret): """Test verifying expired token raises JWTError.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, verify_token data = {"sub": "user123"} expires_delta = timedelta(hours=-1) token = create_access_token(data, expires_delta=expires_delta, secret_key=jwt_secret) # Act & Assert with pytest.raises(JWTError): verify_token(token, secret_key=jwt_secret) class TestJWTAlgorithm: """Test suite for JWT algorithm configuration.""" def test_token_uses_hs256_algorithm(self, jwt_secret): """Test that token uses HS256 algorithm.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token data = {"sub": "user123"} # Act token = create_access_token(data, secret_key=jwt_secret) # Decode without verification to check header header = jwt.get_unverified_header(token) # Assert assert header["alg"] == "HS256" class TestJWTWithConfig: """Test suite for JWT functions using config settings.""" def test_create_access_token_uses_config_secret(self): """Test that create_access_token uses SECRET_KEY from config when not provided.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} # Act - Don't pass secret_key, should use config token = create_access_token(data) decoded = decode_access_token(token) # Assert assert decoded["sub"] == "user123" def test_decode_access_token_uses_config_secret(self): """Test that decode_access_token uses SECRET_KEY from config when not provided.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, decode_access_token data = {"sub": "user123"} token = create_access_token(data) # Uses config # Act - Don't pass secret_key, should use config decoded = decode_access_token(token) # Assert assert decoded["sub"] == "user123" def test_verify_token_uses_config_secret(self): """Test that verify_token uses SECRET_KEY from config when not provided.""" # Arrange from src.openrouter_monitor.services.jwt import create_access_token, verify_token data = {"sub": "user123"} token = create_access_token(data) # Uses config # Act - Don't pass secret_key, should use config token_data = verify_token(token) # Assert assert token_data.user_id == "user123" class TestJWTEdgeCases: """Test suite for JWT edge cases.""" def test_verify_token_without_exp_raises_error(self, jwt_secret): """Test verifying token without exp claim raises error.""" # Arrange - Create token manually without exp from jose import jwt as jose_jwt payload = {"sub": "user123", "iat": datetime.now(timezone.utc).timestamp()} token = jose_jwt.encode(payload, jwt_secret, algorithm="HS256") # Act & Assert from src.openrouter_monitor.services.jwt import verify_token with pytest.raises(Exception): # JWTError or similar verify_token(token, secret_key=jwt_secret) def test_verify_token_without_iat_raises_error(self, jwt_secret): """Test verifying token without iat claim raises error.""" # Arrange - Create token manually without iat from jose import jwt as jose_jwt from datetime import timedelta now = datetime.now(timezone.utc) payload = {"sub": "user123", "exp": (now + timedelta(hours=1)).timestamp()} token = jose_jwt.encode(payload, jwt_secret, algorithm="HS256") # Act & Assert from src.openrouter_monitor.services.jwt import verify_token with pytest.raises(Exception): # JWTError or similar verify_token(token, secret_key=jwt_secret) # Fixtures @pytest.fixture def jwt_secret(): """Provide a test JWT secret key.""" return "test-jwt-secret-key-32-chars-long!"