Add comprehensive integration and API tests: - Integration tests for SQLAlchemyPostRepository (34 tests) - API tests for posts endpoints and error handlers (22 tests) - Unit tests for PublishPostUseCase and ListPostsUseCase - Unit tests for SessionTransactionManager Also register generic exception handler in error_handler.py All 167 tests pass, coverage now meets CI threshold of 70%
208 lines
7.9 KiB
Python
208 lines
7.9 KiB
Python
"""Tests for error handler middleware.
|
|
|
|
Tests exception handling and error responses.
|
|
"""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.domain.exceptions import (
|
|
AlreadyExistsException,
|
|
DomainException,
|
|
ForbiddenException,
|
|
NotFoundException,
|
|
ValidationException,
|
|
)
|
|
from app.main import app_factory
|
|
|
|
|
|
class TestDomainExceptionHandlers:
|
|
"""Test suite for domain exception handlers."""
|
|
|
|
async def test_validation_exception(self) -> None:
|
|
"""Test ValidationException returns 400."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
side_effect=ValidationException("Invalid input"),
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/posts/12345678-1234-1234-1234-123456789abc")
|
|
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["error"] == "ValidationException"
|
|
assert data["message"] == "Invalid input"
|
|
assert "timestamp" in data
|
|
assert "path" in data
|
|
|
|
async def test_forbidden_exception(self) -> None:
|
|
"""Test ForbiddenException returns 403."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
side_effect=ForbiddenException("Access denied"),
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/posts/12345678-1234-1234-1234-123456789abc")
|
|
|
|
assert response.status_code == 403
|
|
data = response.json()
|
|
assert data["error"] == "ForbiddenException"
|
|
assert data["message"] == "Access denied"
|
|
|
|
async def test_not_found_exception(self) -> None:
|
|
"""Test NotFoundException returns 404."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
side_effect=NotFoundException("Post not found"),
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/posts/12345678-1234-1234-1234-123456789abc")
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["error"] == "NotFoundException"
|
|
assert data["message"] == "Post not found"
|
|
|
|
async def test_already_exists_exception(self) -> None:
|
|
"""Test AlreadyExistsException returns 409."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
side_effect=AlreadyExistsException("Post already exists"),
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/posts/12345678-1234-1234-1234-123456789abc")
|
|
|
|
assert response.status_code == 409
|
|
data = response.json()
|
|
assert data["error"] == "AlreadyExistsException"
|
|
assert data["message"] == "Post already exists"
|
|
|
|
async def test_generic_domain_exception(self) -> None:
|
|
"""Test generic DomainException returns 500."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
side_effect=DomainException("Generic error"),
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/posts/12345678-1234-1234-1234-123456789abc")
|
|
|
|
assert response.status_code == 500
|
|
data = response.json()
|
|
assert data["error"] == "DomainException"
|
|
assert data["message"] == "Generic error"
|
|
|
|
|
|
class TestHTTPExceptionHandler:
|
|
"""Test suite for HTTP exception handling."""
|
|
|
|
async def test_http_exception_structure(self) -> None:
|
|
"""Test HTTP exception response structure."""
|
|
# Test that exception handler is registered and produces correct format
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
|
|
from starlette.exceptions import HTTPException
|
|
|
|
from app.infrastructure.middleware.error_handler import http_exception_handler
|
|
|
|
# Create mock request
|
|
@dataclass
|
|
class MockURL:
|
|
path: str = "/test"
|
|
|
|
@dataclass
|
|
class MockRequest:
|
|
url: MockURL = field(default_factory=MockURL)
|
|
|
|
exc = HTTPException(status_code=404, detail="Not found")
|
|
response = await http_exception_handler(MockRequest(), exc) # type: ignore[arg-type]
|
|
|
|
assert response.status_code == 404
|
|
body_bytes: bytes = response.body # type: ignore[assignment]
|
|
data: dict[str, object] = json.loads(body_bytes.decode("utf-8"))
|
|
assert data["error"] == "HTTPException"
|
|
assert "message" in data
|
|
|
|
|
|
class TestGenericExceptionHandler:
|
|
"""Test suite for generic exception handling."""
|
|
|
|
async def test_generic_exception_handler_function(self) -> None:
|
|
"""Test generic exception handler function directly."""
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
|
|
from app.infrastructure.middleware.error_handler import (
|
|
generic_exception_handler,
|
|
)
|
|
|
|
# Create mock request
|
|
@dataclass
|
|
class MockURL:
|
|
path: str = "/test"
|
|
|
|
@dataclass
|
|
class MockRequest:
|
|
url: MockURL = field(default_factory=MockURL)
|
|
|
|
exc = RuntimeError("Internal error")
|
|
response = await generic_exception_handler(MockRequest(), exc) # type: ignore[arg-type]
|
|
|
|
assert response.status_code == 500
|
|
body_bytes: bytes = response.body # type: ignore[assignment]
|
|
data: dict[str, object] = json.loads(body_bytes.decode("utf-8"))
|
|
assert data["error"] == "InternalServerError"
|
|
assert data["message"] == "An unexpected error occurred"
|
|
assert "timestamp" in data
|
|
assert "path" in data
|
|
|
|
|
|
class TestGetStatusCode:
|
|
"""Test suite for get_status_code function."""
|
|
|
|
def test_validation_exception_status(self) -> None:
|
|
"""Test ValidationException maps to 400."""
|
|
from app.infrastructure.middleware.error_handler import get_status_code
|
|
|
|
exc = ValidationException("Invalid")
|
|
assert get_status_code(exc) == 400
|
|
|
|
def test_forbidden_exception_status(self) -> None:
|
|
"""Test ForbiddenException maps to 403."""
|
|
from app.infrastructure.middleware.error_handler import get_status_code
|
|
|
|
exc = ForbiddenException("Forbidden")
|
|
assert get_status_code(exc) == 403
|
|
|
|
def test_not_found_exception_status(self) -> None:
|
|
"""Test NotFoundException maps to 404."""
|
|
from app.infrastructure.middleware.error_handler import get_status_code
|
|
|
|
exc = NotFoundException("Not found")
|
|
assert get_status_code(exc) == 404
|
|
|
|
def test_already_exists_exception_status(self) -> None:
|
|
"""Test AlreadyExistsException maps to 409."""
|
|
from app.infrastructure.middleware.error_handler import get_status_code
|
|
|
|
exc = AlreadyExistsException("Already exists")
|
|
assert get_status_code(exc) == 409
|
|
|
|
def test_generic_exception_status(self) -> None:
|
|
"""Test generic DomainException maps to 500."""
|
|
from app.infrastructure.middleware.error_handler import get_status_code
|
|
|
|
exc = DomainException("Generic")
|
|
assert get_status_code(exc) == 500
|