|
| 1 | +"""Tests for auth hardening -- domain exceptions + null safety (OPE-76, OPE-77).""" |
| 2 | +import pytest |
| 3 | +from unittest.mock import patch, MagicMock |
| 4 | + |
| 5 | + |
| 6 | +class TestNullUserIdSafety: |
| 7 | + """API key users (no user_id) get 401, not confusing 404.""" |
| 8 | + |
| 9 | + def test_search_with_null_user_id_returns_401(self, client, valid_headers): |
| 10 | + """Search should reject None user_id, not pretend repo doesn't exist.""" |
| 11 | + with patch("routes.search.require_auth") as mock_auth, \ |
| 12 | + patch("routes.search.verify_repo_access") as mock_verify: |
| 13 | + from middleware.auth import AuthContext |
| 14 | + mock_auth.return_value = AuthContext(api_key_name="test-key", user_id=None) |
| 15 | + # verify_repo_access should raise 401 when user_id is None |
| 16 | + from fastapi import HTTPException |
| 17 | + mock_verify.side_effect = HTTPException(status_code=401, detail="User ID required") |
| 18 | + resp = client.post( |
| 19 | + "/api/v1/search", |
| 20 | + json={"query": "auth", "repo_id": "test"}, |
| 21 | + headers=valid_headers, |
| 22 | + ) |
| 23 | + assert resp.status_code == 401 |
| 24 | + |
| 25 | + def test_get_repo_or_404_rejects_none_user_id(self): |
| 26 | + """get_repo_or_404 should raise 401 when user_id is None.""" |
| 27 | + from dependencies import get_repo_or_404 |
| 28 | + from fastapi import HTTPException |
| 29 | + |
| 30 | + with pytest.raises(HTTPException) as exc: |
| 31 | + get_repo_or_404("some-repo", None) |
| 32 | + assert exc.value.status_code == 401 |
| 33 | + |
| 34 | + def test_verify_repo_access_rejects_none_user_id(self): |
| 35 | + """verify_repo_access should raise 401 when user_id is None.""" |
| 36 | + from dependencies import verify_repo_access |
| 37 | + from fastapi import HTTPException |
| 38 | + |
| 39 | + with pytest.raises(HTTPException) as exc: |
| 40 | + verify_repo_access("some-repo", None) |
| 41 | + assert exc.value.status_code == 401 |
| 42 | + |
| 43 | + |
| 44 | +class TestDomainExceptions: |
| 45 | + """Auth service raises domain exceptions, not HTTPException.""" |
| 46 | + |
| 47 | + def test_expired_token_raises_domain_exception(self): |
| 48 | + """Auth service should raise TokenExpiredError, not HTTPException.""" |
| 49 | + from services.exceptions import TokenExpiredError |
| 50 | + assert issubclass(TokenExpiredError, Exception) |
| 51 | + assert not issubclass(TokenExpiredError, __import__('fastapi').HTTPException) |
| 52 | + |
| 53 | + def test_exception_hierarchy(self): |
| 54 | + """All auth exceptions inherit from AuthenticationError.""" |
| 55 | + from services.exceptions import ( |
| 56 | + AuthenticationError, |
| 57 | + TokenExpiredError, |
| 58 | + InvalidTokenError, |
| 59 | + TokenMissingClaimError, |
| 60 | + InvalidCredentialsError, |
| 61 | + SignupError, |
| 62 | + SessionError, |
| 63 | + UserIdRequiredError, |
| 64 | + ) |
| 65 | + for exc_class in [ |
| 66 | + TokenExpiredError, InvalidTokenError, TokenMissingClaimError, |
| 67 | + InvalidCredentialsError, SignupError, SessionError, UserIdRequiredError, |
| 68 | + ]: |
| 69 | + assert issubclass(exc_class, AuthenticationError) |
0 commit comments