"""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