from __future__ import annotations import secrets from datetime import timedelta import pytest from services.security import ( JWTSettings, TokenDecodeError, TokenExpiredError, TokenTypeMismatchError, create_access_token, create_refresh_token, decode_access_token, hash_password, verify_password, ) @pytest.fixture() def jwt_settings() -> JWTSettings: """Provide a unique JWTSettings instance per test with random secret.""" return JWTSettings(secret_key=secrets.token_urlsafe(32)) def test_hash_password_round_trip() -> None: password = secrets.token_urlsafe(16) hashed = hash_password(password) assert hashed != password assert verify_password(password, hashed) assert not verify_password("incorrect", hashed) def test_verify_password_handles_malformed_hash() -> None: assert not verify_password("secret", "not-a-valid-hash") def test_access_token_roundtrip(jwt_settings: JWTSettings) -> None: token = create_access_token( "user-id-123", jwt_settings, scopes=("read", "write"), extra_claims={"custom": "value"}, ) payload = decode_access_token(token, jwt_settings) assert payload.sub == "user-id-123" assert payload.type == "access" assert payload.scopes == ["read", "write"] def test_refresh_token_type_mismatch(jwt_settings: JWTSettings) -> None: token = create_refresh_token("user-id-456", jwt_settings) with pytest.raises(TokenTypeMismatchError): decode_access_token(token, jwt_settings) def test_decode_expired_token(jwt_settings: JWTSettings) -> None: expired_token = create_access_token( "user-id-789", jwt_settings, expires_delta=timedelta(seconds=-5), ) with pytest.raises(TokenExpiredError): decode_access_token(expired_token, jwt_settings) def test_decode_tampered_token(jwt_settings: JWTSettings) -> None: token = create_access_token("user-id-321", jwt_settings) tampered = token[:-1] + ("a" if token[-1] != "a" else "b") with pytest.raises(TokenDecodeError): decode_access_token(tampered, jwt_settings)