Spaces:
Running
Running
| # backend/tests/test_jwt_auth.py | |
| # Tests JWT verification — the only security gate between the internet and the chat endpoint. | |
| # Every path through verify_jwt() is covered. | |
| import time | |
| import pytest | |
| from fastapi import HTTPException | |
| from fastapi.security import HTTPAuthorizationCredentials | |
| from jose import jwt | |
| from unittest.mock import patch | |
| from tests.conftest import TEST_JWT_SECRET, TEST_ALGORITHM, make_jwt | |
| def _make_credentials(token: str) -> HTTPAuthorizationCredentials: | |
| return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token) | |
| def _verify(token: str): | |
| """Call verify_jwt synchronously (it's not async).""" | |
| from app.security.jwt_auth import verify_jwt | |
| return verify_jwt(_make_credentials(token)) | |
| class TestVerifyJWT: | |
| def test_valid_token_passes(self, valid_token): | |
| payload = _verify(valid_token) | |
| assert payload["sub"] == "test-user" | |
| def test_expired_token_rejected(self, expired_token): | |
| with pytest.raises(HTTPException) as exc_info: | |
| _verify(expired_token) | |
| assert exc_info.value.status_code == 401 | |
| assert "expired" in exc_info.value.detail.lower() | |
| def test_wrong_secret_rejected(self, wrong_secret_token): | |
| with pytest.raises(HTTPException) as exc_info: | |
| _verify(wrong_secret_token) | |
| assert exc_info.value.status_code == 401 | |
| def test_malformed_token_rejected(self): | |
| with pytest.raises(HTTPException) as exc_info: | |
| _verify("not.a.jwt") | |
| assert exc_info.value.status_code == 401 | |
| def test_empty_token_rejected(self): | |
| with pytest.raises(HTTPException): | |
| _verify("") | |
| def test_algorithm_none_attack_rejected(self): | |
| # Attacker crafts a token with alg=none to bypass signature verification. | |
| # jose refuses to decode alg=none tokens by default — this test confirms it. | |
| payload = {"sub": "attacker", "exp": int(time.time()) + 3600} | |
| # We can't easily craft an alg=none token with jose, but we can verify | |
| # that a tampered token (modified signature) is rejected. | |
| valid = make_jwt() | |
| tampered = valid[: valid.rfind(".")] + ".invalidsignature" | |
| with pytest.raises(HTTPException) as exc_info: | |
| _verify(tampered) | |
| assert exc_info.value.status_code == 401 | |
| def test_missing_jwt_secret_raises_500(self): | |
| # If JWT_SECRET is not configured on the server, the endpoint returns 500 | |
| # rather than accidentally accepting all tokens. | |
| from app.core.config import get_settings, Settings | |
| settings = get_settings() | |
| original = settings.JWT_SECRET | |
| settings.JWT_SECRET = None | |
| try: | |
| with pytest.raises(HTTPException) as exc_info: | |
| _verify(make_jwt()) | |
| assert exc_info.value.status_code == 500 | |
| finally: | |
| settings.JWT_SECRET = original | |
| def test_token_payload_fields_preserved(self): | |
| token = make_jwt(role="guest") | |
| payload = _verify(token) | |
| assert payload.get("role") == "guest" | |