Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import asyncio | |
| from datetime import datetime, timedelta, timezone | |
| import jwt | |
| import pytest | |
| from fastapi import HTTPException | |
| from src.auth.jwt import decode_and_verify_jwt, require_jwt | |
| from src.config import settings | |
| def _build_token( | |
| *, | |
| secret: str, | |
| iss: str, | |
| aud: str, | |
| sub: str = "client:test-client", | |
| expires_in_seconds: int = 300, | |
| scope: str = "material:write lkpd:write lkpd:read", | |
| ) -> str: | |
| now = datetime.now(timezone.utc) | |
| payload = { | |
| "iss": iss, | |
| "aud": aud, | |
| "sub": sub, | |
| "iat": int(now.timestamp()), | |
| "exp": int((now + timedelta(seconds=expires_in_seconds)).timestamp()), | |
| "scope": scope, | |
| } | |
| return jwt.encode(payload, secret, algorithm="HS256") | |
| def _set_jwt_settings(monkeypatch: pytest.MonkeyPatch) -> None: | |
| monkeypatch.setattr(settings, "jwt_enabled", True) | |
| monkeypatch.setattr(settings, "jwt_secret", "x" * 32) | |
| monkeypatch.setattr(settings, "jwt_issuer", "my-backend") | |
| monkeypatch.setattr(settings, "jwt_audience", "rtm-class-ai") | |
| monkeypatch.setattr(settings, "jwt_clock_skew_seconds", 0) | |
| monkeypatch.setattr(settings, "jwt_denylist_enabled", False) | |
| def test_decode_and_verify_jwt_valid_token(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss=settings.jwt_issuer, | |
| aud=settings.jwt_audience, | |
| ) | |
| payload = decode_and_verify_jwt(token) | |
| assert payload["iss"] == settings.jwt_issuer | |
| assert payload["aud"] == settings.jwt_audience | |
| assert payload["sub"] == "client:test-client" | |
| def test_decode_and_verify_jwt_wrong_issuer(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss="not-backend", | |
| aud=settings.jwt_audience, | |
| ) | |
| with pytest.raises(HTTPException) as exc_info: | |
| decode_and_verify_jwt(token) | |
| assert exc_info.value.status_code == 401 | |
| assert exc_info.value.detail == "Invalid token issuer." | |
| def test_decode_and_verify_jwt_wrong_audience(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss=settings.jwt_issuer, | |
| aud="other-service", | |
| ) | |
| with pytest.raises(HTTPException) as exc_info: | |
| decode_and_verify_jwt(token) | |
| assert exc_info.value.status_code == 401 | |
| assert exc_info.value.detail == "Invalid token audience." | |
| def test_decode_and_verify_jwt_expired_token(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss=settings.jwt_issuer, | |
| aud=settings.jwt_audience, | |
| expires_in_seconds=-1, | |
| ) | |
| with pytest.raises(HTTPException) as exc_info: | |
| decode_and_verify_jwt(token) | |
| assert exc_info.value.status_code == 401 | |
| assert exc_info.value.detail == "Invalid or expired token." | |
| def test_require_jwt_missing_scope(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss=settings.jwt_issuer, | |
| aud=settings.jwt_audience, | |
| scope="lkpd:read", | |
| ) | |
| dependency = require_jwt(["material:write"]) | |
| with pytest.raises(HTTPException) as exc_info: | |
| asyncio.run(dependency(authorization=f"Bearer {token}")) | |
| assert exc_info.value.status_code == 403 | |
| assert exc_info.value.detail == "Insufficient scope." | |
| def test_require_jwt_invalid_subject(monkeypatch: pytest.MonkeyPatch) -> None: | |
| _set_jwt_settings(monkeypatch) | |
| token = _build_token( | |
| secret=settings.jwt_secret, | |
| iss=settings.jwt_issuer, | |
| aud=settings.jwt_audience, | |
| sub="service:backend", | |
| ) | |
| dependency = require_jwt(["material:write"]) | |
| with pytest.raises(HTTPException) as exc_info: | |
| asyncio.run(dependency(authorization=f"Bearer {token}")) | |
| assert exc_info.value.status_code == 401 | |
| assert exc_info.value.detail == "Invalid token subject." | |