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%
175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
"""Tests for PublishPostUseCase.
|
|
|
|
Tests publishing and unpublishing posts with authorization.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.application.dtos import PostResponseDTO
|
|
from app.application.interfaces import TransactionManager
|
|
from app.application.use_cases.publish_post import PublishPostUseCase
|
|
from app.domain.entities import Post
|
|
from app.domain.exceptions import ForbiddenException, NotFoundException
|
|
from app.domain.repositories import PostRepository
|
|
from app.domain.value_objects import Content, Slug, Title
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_post_repository() -> MagicMock:
|
|
"""Create mock post repository."""
|
|
return MagicMock(spec=PostRepository)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_transaction_manager() -> MagicMock:
|
|
"""Create mock transaction manager."""
|
|
tx = MagicMock(spec=TransactionManager)
|
|
tx.commit = AsyncMock()
|
|
return tx
|
|
|
|
|
|
@pytest.fixture
|
|
def publish_use_case(
|
|
mock_post_repository: MagicMock,
|
|
mock_transaction_manager: MagicMock,
|
|
) -> PublishPostUseCase:
|
|
"""Create publish use case with mocked dependencies."""
|
|
return PublishPostUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_post() -> Post:
|
|
"""Create a sample unpublished post."""
|
|
return Post(
|
|
id=uuid4(),
|
|
title=Title("Test Post"),
|
|
content=Content("Test content"),
|
|
slug=Slug("test-post"),
|
|
author_id="author-123",
|
|
published=False,
|
|
tags=["test"],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def published_post() -> Post:
|
|
"""Create a sample published post."""
|
|
post = Post(
|
|
id=uuid4(),
|
|
title=Title("Published Post"),
|
|
content=Content("Published content"),
|
|
slug=Slug("published-post"),
|
|
author_id="author-123",
|
|
published=True,
|
|
tags=["published"],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
return post
|
|
|
|
|
|
class TestPublishPost:
|
|
"""Test suite for publish method."""
|
|
|
|
async def test_publish_success(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
mock_transaction_manager: MagicMock,
|
|
sample_post: Post,
|
|
) -> None:
|
|
"""Test successful post publishing."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=sample_post)
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
result = await publish_use_case.publish(sample_post.id, sample_post.author_id)
|
|
|
|
assert isinstance(result, PostResponseDTO)
|
|
assert result.id == sample_post.id
|
|
assert result.published is True
|
|
mock_post_repository.update.assert_called_once()
|
|
mock_transaction_manager.commit.assert_called_once()
|
|
|
|
async def test_publish_not_found(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
) -> None:
|
|
"""Test publishing non-existent post raises NotFoundException."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=None)
|
|
|
|
with pytest.raises(NotFoundException) as exc_info:
|
|
await publish_use_case.publish(uuid4(), "author-123")
|
|
|
|
assert "not found" in str(exc_info.value).lower()
|
|
|
|
async def test_publish_forbidden(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
sample_post: Post,
|
|
) -> None:
|
|
"""Test publishing other user's post raises ForbiddenException."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=sample_post)
|
|
|
|
with pytest.raises(ForbiddenException) as exc_info:
|
|
await publish_use_case.publish(sample_post.id, "different-author")
|
|
|
|
assert "own posts" in str(exc_info.value).lower()
|
|
|
|
|
|
class TestUnpublishPost:
|
|
"""Test suite for unpublish method."""
|
|
|
|
async def test_unpublish_success(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
mock_transaction_manager: MagicMock,
|
|
published_post: Post,
|
|
) -> None:
|
|
"""Test successful post unpublishing."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=published_post)
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
result = await publish_use_case.unpublish(published_post.id, published_post.author_id)
|
|
|
|
assert isinstance(result, PostResponseDTO)
|
|
assert result.id == published_post.id
|
|
assert result.published is False
|
|
mock_post_repository.update.assert_called_once()
|
|
mock_transaction_manager.commit.assert_called_once()
|
|
|
|
async def test_unpublish_not_found(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
) -> None:
|
|
"""Test unpublishing non-existent post raises NotFoundException."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=None)
|
|
|
|
with pytest.raises(NotFoundException) as exc_info:
|
|
await publish_use_case.unpublish(uuid4(), "author-123")
|
|
|
|
assert "not found" in str(exc_info.value).lower()
|
|
|
|
async def test_unpublish_forbidden(
|
|
self,
|
|
publish_use_case: PublishPostUseCase,
|
|
mock_post_repository: MagicMock,
|
|
published_post: Post,
|
|
) -> None:
|
|
"""Test unpublishing other user's post raises ForbiddenException."""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=published_post)
|
|
|
|
with pytest.raises(ForbiddenException) as exc_info:
|
|
await publish_use_case.unpublish(published_post.id, "different-author")
|
|
|
|
assert "own posts" in str(exc_info.value).lower()
|