feat(tests): increase test coverage from 68% to 78%
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%
This commit is contained in:
207
tests/api/test_error_handlers.py
Normal file
207
tests/api/test_error_handlers.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user