from __future__ import annotations import time from typing import Any from uuid import UUID import jwt import pytest from fastapi import HTTPException from core.auth.models import CurrentUser from v1.profile.dependencies import get_current_user class TestGetCurrentUser: """Tests for JWT validation in get_current_user dependency.""" @pytest.fixture def jwt_secret(self) -> str: return "super-secret-jwt-token-with-at-least-32-characters" @pytest.fixture def valid_user_id(self) -> str: return "00000000-0000-0000-0000-000000000123" @pytest.fixture def valid_payload(self, valid_user_id: str) -> dict[str, Any]: """Valid JWT payload with all required claims.""" now = int(time.time()) return { "sub": valid_user_id, "aud": "authenticated", "iss": "http://localhost:8001/auth/v1", "exp": now + 3600, # 1 hour from now "iat": now, } def _create_token(self, payload: dict[str, Any], secret: str) -> str: return jwt.encode(payload, secret, algorithm="HS256") def test_valid_token_returns_current_user( self, jwt_secret: str, valid_payload: dict[str, Any], valid_user_id: str, monkeypatch: pytest.MonkeyPatch, ) -> None: """Valid JWT with correct aud/iss/exp should return CurrentUser.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) token = self._create_token(valid_payload, jwt_secret) authorization = f"Bearer {token}" result = get_current_user(authorization=authorization) assert isinstance(result, CurrentUser) assert result.id == UUID(valid_user_id) def test_missing_authorization_raises_401(self) -> None: """Missing Authorization header should raise 401.""" with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=None) assert exc_info.value.status_code == 401 assert exc_info.value.detail == "Unauthorized" def test_invalid_scheme_raises_401(self) -> None: """Non-Bearer scheme should raise 401.""" with pytest.raises(HTTPException) as exc_info: get_current_user(authorization="Basic dXNlcjpwYXNz") assert exc_info.value.status_code == 401 def test_expired_token_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """Expired JWT should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) valid_payload["exp"] = int(time.time()) - 3600 # 1 hour ago token = self._create_token(valid_payload, jwt_secret) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 def test_invalid_audience_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """JWT with wrong audience should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) valid_payload["aud"] = "wrong-audience" token = self._create_token(valid_payload, jwt_secret) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 def test_invalid_issuer_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """JWT with wrong issuer should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) valid_payload["iss"] = "http://malicious-site.com/auth/v1" token = self._create_token(valid_payload, jwt_secret) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 def test_missing_subject_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """JWT without 'sub' claim should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) del valid_payload["sub"] token = self._create_token(valid_payload, jwt_secret) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 def test_wrong_secret_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """JWT signed with wrong secret should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) token = self._create_token( valid_payload, "wrong-secret-key-that-is-long-enough" ) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401 def test_jwt_secret_not_configured_raises_503( self, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch ) -> None: """Missing JWT secret in config should raise 503.""" monkeypatch.setattr("v1.profile.dependencies.config.supabase.jwt_secret", None) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization="Bearer some-token") assert exc_info.value.status_code == 503 assert exc_info.value.detail == "JWT secret not configured" def test_invalid_uuid_in_subject_raises_401( self, jwt_secret: str, valid_payload: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: """JWT with non-UUID 'sub' claim should raise 401.""" monkeypatch.setattr( "v1.profile.dependencies.config.supabase.jwt_secret", jwt_secret ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_scheme", "http", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.public_host", "localhost", ) monkeypatch.setattr( "v1.profile.dependencies.config.supabase.kong_http_port", 8001, ) valid_payload["sub"] = "not-a-valid-uuid" token = self._create_token(valid_payload, jwt_secret) with pytest.raises(HTTPException) as exc_info: get_current_user(authorization=f"Bearer {token}") assert exc_info.value.status_code == 401