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%
319 lines
11 KiB
Python
319 lines
11 KiB
Python
"""API tests for posts endpoints.
|
|
|
|
Tests REST API endpoints - focusing on endpoints that don't require
|
|
complex Dishka dependency mocking.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.application.dtos import PostResponseDTO
|
|
from app.domain.exceptions import NotFoundException
|
|
from app.main import app_factory
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_post_dto() -> PostResponseDTO:
|
|
"""Create a sample post DTO for testing."""
|
|
return PostResponseDTO(
|
|
id=uuid4(),
|
|
title="Test Post",
|
|
content="This is test content for the blog post",
|
|
slug="test-post",
|
|
author_id="test-user-id",
|
|
published=True,
|
|
tags=["python", "testing"],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
|
|
|
|
class TestListPublishedPosts:
|
|
"""Test suite for GET /api/v1/posts/published endpoint."""
|
|
|
|
async def test_list_published_posts(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test listing published posts without authentication."""
|
|
with patch(
|
|
"app.application.use_cases.list_posts.ListPostsUseCase.published_posts",
|
|
return_value=[sample_post_dto],
|
|
):
|
|
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/published")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert data["total"] == 1
|
|
|
|
|
|
class TestSearchPosts:
|
|
"""Test suite for GET /api/v1/posts/search endpoint."""
|
|
|
|
async def test_search_posts(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test searching posts by query."""
|
|
with patch(
|
|
"app.application.use_cases.list_posts.ListPostsUseCase.search",
|
|
return_value=[sample_post_dto],
|
|
):
|
|
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/search?query=test")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert data["total"] == 1
|
|
|
|
async def test_search_posts_empty_query(self) -> None:
|
|
"""Test search with empty query returns empty results."""
|
|
with patch(
|
|
"app.application.use_cases.list_posts.ListPostsUseCase.search",
|
|
return_value=[],
|
|
):
|
|
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/search?query=")
|
|
|
|
# Empty query returns 200 with empty results (not 422)
|
|
# as query param accepts empty strings
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["items"] == []
|
|
assert data["total"] == 0
|
|
|
|
|
|
class TestGetPostsByTag:
|
|
"""Test suite for GET /api/v1/posts/by-tag/{tag} endpoint."""
|
|
|
|
async def test_get_posts_by_tag(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test getting posts by tag."""
|
|
with patch(
|
|
"app.application.use_cases.list_posts.ListPostsUseCase.by_tag",
|
|
return_value=[sample_post_dto],
|
|
):
|
|
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/by-tag/python")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert data["total"] == 1
|
|
|
|
|
|
class TestGetPostsByAuthor:
|
|
"""Test suite for GET /api/v1/posts/by-author/{author_id} endpoint."""
|
|
|
|
async def test_get_posts_by_author(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test getting posts by author."""
|
|
with patch(
|
|
"app.application.use_cases.list_posts.ListPostsUseCase.by_author",
|
|
return_value=[sample_post_dto],
|
|
):
|
|
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/by-author/test-user-id")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert data["total"] == 1
|
|
|
|
|
|
class TestGetPostById:
|
|
"""Test suite for GET /api/v1/posts/{post_id} endpoint."""
|
|
|
|
async def test_get_post_by_id_success(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test getting a post by ID."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_id",
|
|
return_value=sample_post_dto,
|
|
):
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(f"/api/v1/posts/{sample_post_dto.id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == str(sample_post_dto.id)
|
|
assert data["title"] == sample_post_dto.title
|
|
|
|
async def test_get_post_by_id_not_found(self) -> None:
|
|
"""Test getting a non-existing post 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(f"/api/v1/posts/{uuid4()}")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestGetPostBySlug:
|
|
"""Test suite for GET /api/v1/posts/slug/{slug} endpoint."""
|
|
|
|
async def test_get_post_by_slug_success(
|
|
self,
|
|
sample_post_dto: PostResponseDTO,
|
|
) -> None:
|
|
"""Test getting a post by slug."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_slug",
|
|
return_value=sample_post_dto,
|
|
):
|
|
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/slug/test-post")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["slug"] == "test-post"
|
|
|
|
async def test_get_post_by_slug_not_found(self) -> None:
|
|
"""Test getting a non-existing post by slug returns 404."""
|
|
with patch(
|
|
"app.application.use_cases.get_post.GetPostUseCase.by_slug",
|
|
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/slug/non-existing-slug")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestCreatePostAuth:
|
|
"""Test suite for POST /api/v1/posts authentication."""
|
|
|
|
async def test_create_post_unauthorized(self) -> None:
|
|
"""Test post creation without authentication returns 401."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.post(
|
|
"/api/v1/posts",
|
|
json={
|
|
"title": "Test Post",
|
|
"content": "This is test content for the blog post",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestUpdatePostAuth:
|
|
"""Test suite for PATCH /api/v1/posts/{post_id} authentication."""
|
|
|
|
async def test_update_post_unauthorized(self) -> None:
|
|
"""Test updating post without authentication returns 401."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.patch(
|
|
f"/api/v1/posts/{uuid4()}",
|
|
json={"title": "Updated Title"},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestDeletePostAuth:
|
|
"""Test suite for DELETE /api/v1/posts/{post_id} authentication."""
|
|
|
|
async def test_delete_post_unauthorized(self) -> None:
|
|
"""Test deleting post without authentication returns 401."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.delete(f"/api/v1/posts/{uuid4()}")
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestPublishPostAuth:
|
|
"""Test suite for POST /api/v1/posts/{post_id}/publish authentication."""
|
|
|
|
async def test_publish_post_unauthorized(self) -> None:
|
|
"""Test publishing post without authentication returns 401."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.post(f"/api/v1/posts/{uuid4()}/publish")
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestUnpublishPostAuth:
|
|
"""Test suite for POST /api/v1/posts/{post_id}/unpublish authentication."""
|
|
|
|
async def test_unpublish_post_unauthorized(self) -> None:
|
|
"""Test unpublishing post without authentication returns 401."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.post(f"/api/v1/posts/{uuid4()}/unpublish")
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestHealthEndpoint:
|
|
"""Test suite for health check endpoint."""
|
|
|
|
async def test_health_check(self) -> None:
|
|
"""Test health check endpoint returns ok status."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
assert "app" in data
|
|
assert "env" in data
|
|
|
|
|
|
class TestRootRedirect:
|
|
"""Test suite for root redirect."""
|
|
|
|
async def test_root_redirect(self) -> None:
|
|
"""Test root URL redirects to web UI."""
|
|
app = app_factory()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/")
|
|
|
|
assert response.status_code == 200
|
|
assert "web/" in response.text
|