# 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"