Files
musicseerr/backend/tests/test_error_leakage.py
T

96 lines
3.3 KiB
Python

"""Tests that exception handlers do not leak internal details."""
import pytest
from fastapi import FastAPI
import httpx
from core.exceptions import ExternalServiceError
from core.exception_handlers import (
external_service_error_handler,
circuit_open_error_handler,
general_exception_handler,
)
from infrastructure.resilience.retry import CircuitOpenError
def _build_app() -> FastAPI:
app = FastAPI()
@app.get("/raise-general")
async def raise_general():
raise RuntimeError("secret internal path /app/main.py")
@app.get("/raise-external")
async def raise_external():
raise ExternalServiceError("connection to 10.0.0.5:8096 refused")
@app.get("/raise-circuit")
async def raise_circuit():
raise CircuitOpenError(
"Circuit breaker 'jellyfin' is OPEN",
breaker_name="jellyfin",
)
app.add_exception_handler(ExternalServiceError, external_service_error_handler)
app.add_exception_handler(CircuitOpenError, circuit_open_error_handler)
app.add_exception_handler(Exception, general_exception_handler)
return app
@pytest.mark.asyncio
async def test_general_exception_handler_hides_details():
app = _build_app()
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/raise-general")
body = resp.json()
assert resp.status_code == 500
assert body["error"]["message"] == "Internal server error"
assert "/app/main.py" not in resp.text
@pytest.mark.asyncio
async def test_external_service_error_hides_details():
app = _build_app()
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/raise-external")
body = resp.json()
assert resp.status_code == 503
assert body["error"]["message"] == "External service unavailable"
assert "10.0.0.5" not in resp.text
@pytest.mark.asyncio
async def test_circuit_open_error_hides_details():
app = _build_app()
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/raise-circuit")
body = resp.json()
assert resp.status_code == 503
assert body["error"]["message"] == "Jellyfin is temporarily unavailable due to repeated connection failures. Check your settings or wait for the service to recover."
assert "circuit breaker" not in resp.text.lower() or "CIRCUIT_BREAKER_OPEN" in resp.text
@pytest.mark.asyncio
async def test_circuit_open_error_without_breaker_name_falls_back():
app = FastAPI()
@app.get("/raise-circuit-unnamed")
async def raise_circuit_unnamed():
raise CircuitOpenError("CB tripped")
app.add_exception_handler(CircuitOpenError, circuit_open_error_handler)
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get("/raise-circuit-unnamed")
body = resp.json()
assert resp.status_code == 503
assert body["error"]["message"].startswith("Service is temporarily unavailable")