first commit
Some checks failed
Deploy / lint (push) Failing after 7s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-20 17:31:01 +01:00
commit 61ab24490d
160 changed files with 17034 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
"""Tests for the exception hierarchy and global exception handlers."""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.exceptions import (
AppError,
AuthenticationError,
AuthorizationError,
DuplicateError,
NotFoundError,
ProviderError,
RateLimitError,
ValidationError,
)
from app.middleware import register_exception_handlers
from app.schemas.common import APIEnvelope
# ── Exception hierarchy tests ──
def test_app_error_defaults():
err = AppError()
assert err.status_code == 500
assert err.message == "Internal server error"
assert str(err) == "Internal server error"
def test_app_error_custom_message():
err = AppError("something broke")
assert err.message == "something broke"
assert str(err) == "something broke"
@pytest.mark.parametrize(
"cls,code,default_msg",
[
(ValidationError, 400, "Validation error"),
(NotFoundError, 404, "Resource not found"),
(DuplicateError, 409, "Resource already exists"),
(AuthenticationError, 401, "Authentication required"),
(AuthorizationError, 403, "Insufficient permissions"),
(ProviderError, 502, "Market data provider unavailable"),
(RateLimitError, 429, "Rate limited"),
],
)
def test_subclass_defaults(cls, code, default_msg):
err = cls()
assert err.status_code == code
assert err.message == default_msg
def test_subclass_custom_message():
err = NotFoundError("Ticker not found: AAPL")
assert err.status_code == 404
assert err.message == "Ticker not found: AAPL"
def test_all_subclasses_are_app_errors():
for cls in (
ValidationError,
NotFoundError,
DuplicateError,
AuthenticationError,
AuthorizationError,
ProviderError,
RateLimitError,
):
assert issubclass(cls, AppError)
# ── APIEnvelope schema tests ──
def test_envelope_success():
env = APIEnvelope(status="success", data={"id": 1})
assert env.status == "success"
assert env.data == {"id": 1}
assert env.error is None
def test_envelope_error():
env = APIEnvelope(status="error", error="bad request")
assert env.status == "error"
assert env.data is None
assert env.error == "bad request"
# ── Middleware integration tests ──
def _make_app() -> FastAPI:
"""Create a minimal FastAPI app with exception handlers and test routes."""
app = FastAPI()
register_exception_handlers(app)
@app.get("/raise-not-found")
async def _raise_not_found():
raise NotFoundError("Ticker not found: XYZ")
@app.get("/raise-validation")
async def _raise_validation():
raise ValidationError("high < low")
@app.get("/raise-duplicate")
async def _raise_duplicate():
raise DuplicateError("Ticker already exists: AAPL")
@app.get("/raise-auth")
async def _raise_auth():
raise AuthenticationError()
@app.get("/raise-authz")
async def _raise_authz():
raise AuthorizationError()
@app.get("/raise-provider")
async def _raise_provider():
raise ProviderError()
@app.get("/raise-rate-limit")
async def _raise_rate_limit():
raise RateLimitError("Rate limited. Ingested 42 records. Resume available.")
@app.get("/raise-unhandled")
async def _raise_unhandled():
raise RuntimeError("unexpected")
return app
@pytest.fixture
def client():
return TestClient(_make_app())
def test_middleware_not_found(client):
resp = client.get("/raise-not-found")
assert resp.status_code == 404
body = resp.json()
assert body["status"] == "error"
assert body["data"] is None
assert body["error"] == "Ticker not found: XYZ"
def test_middleware_validation(client):
resp = client.get("/raise-validation")
assert resp.status_code == 400
body = resp.json()
assert body["status"] == "error"
assert body["error"] == "high < low"
def test_middleware_duplicate(client):
resp = client.get("/raise-duplicate")
assert resp.status_code == 409
body = resp.json()
assert body["status"] == "error"
assert "already exists" in body["error"]
def test_middleware_authentication(client):
resp = client.get("/raise-auth")
assert resp.status_code == 401
body = resp.json()
assert body["status"] == "error"
def test_middleware_authorization(client):
resp = client.get("/raise-authz")
assert resp.status_code == 403
body = resp.json()
assert body["status"] == "error"
def test_middleware_provider_error(client):
resp = client.get("/raise-provider")
assert resp.status_code == 502
body = resp.json()
assert body["status"] == "error"
def test_middleware_rate_limit(client):
resp = client.get("/raise-rate-limit")
assert resp.status_code == 429
body = resp.json()
assert body["status"] == "error"
assert "42 records" in body["error"]
def test_middleware_unhandled_exception():
app = _make_app()
with TestClient(app, raise_server_exceptions=False) as c:
resp = c.get("/raise-unhandled")
assert resp.status_code == 500
body = resp.json()
assert body["status"] == "error"
assert body["data"] is None
assert body["error"] == "Internal server error"