All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
Implement full comments system: domain entities (Comment, CommentLike), value objects (CommentContent), use cases (CRUD, like toggle), SQLAlchemy repository, API v1 endpoints, web UI with comment form and nested replies, i18n translations (EN/RU/FR/DE), and E2E tests. Fix nested reply (reply-to-reply) not displaying — the flat reply_comments dict was only queried for top-level comment IDs, so deeply nested replies were saved to DB (incrementing comment count) but never rendered. Switch to a recursive Jinja2 macro that renders any nesting depth.
240 lines
7.0 KiB
Python
240 lines
7.0 KiB
Python
"""API tests for blog post comments.
|
|
|
|
This module tests comment CRUD operations, nested replies,
|
|
and comment like/unlike toggle via API endpoints.
|
|
"""
|
|
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from tests.api.conftest import API_PREFIX
|
|
|
|
COMMENT_CONTENT = "This is a test comment with enough length."
|
|
|
|
|
|
class TestCreateComment:
|
|
"""Tests for POST /api/v1/posts/{post_id}/comments — create a comment."""
|
|
|
|
def test_create_comment_authenticated(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test creating a comment as authenticated user.
|
|
|
|
TC-API-118: Positive — create top-level comment.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
response = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["post_id"] == post_id
|
|
assert data["author_id"] == "dev-user"
|
|
assert data["content"] == COMMENT_CONTENT
|
|
assert data["parent_id"] is None
|
|
assert data["like_count"] == 0
|
|
assert UUID(data["id"])
|
|
|
|
def test_create_comment_reply(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test creating a reply to an existing comment.
|
|
|
|
TC-API-119: Positive — reply to comment with parent_id.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
parent = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
assert parent.status_code == 201
|
|
parent_id = parent.json()["id"]
|
|
|
|
response = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": "Reply to comment.", "parent_id": parent_id},
|
|
headers=user_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["parent_id"] == parent_id
|
|
assert data["post_id"] == post_id
|
|
|
|
def test_create_comment_as_guest(
|
|
self,
|
|
client: TestClient,
|
|
guest_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test creating a comment as guest returns 401.
|
|
|
|
TC-API-120: Negative — guest cannot create comment.
|
|
"""
|
|
post_id = created_post["id"]
|
|
response = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=guest_headers,
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestListComments:
|
|
"""Tests for GET /api/v1/posts/{post_id}/comments — list comments."""
|
|
|
|
def test_list_comments_by_post(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test listing comments for a post.
|
|
|
|
TC-API-121: Positive — returns list of comments.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": "Second comment."},
|
|
headers=user_headers,
|
|
)
|
|
|
|
response = client.get(f"{API_PREFIX}/{post_id}/comments")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
assert data[0]["content"] == COMMENT_CONTENT
|
|
assert data[1]["content"] == "Second comment."
|
|
|
|
|
|
class TestDeleteComment:
|
|
"""Tests for DELETE /api/v1/comments/{comment_id} — delete a comment."""
|
|
|
|
def test_delete_own_comment(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test deleting own comment returns 204.
|
|
|
|
TC-API-122: Positive — delete own comment.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
comment_resp = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
comment_id = comment_resp.json()["id"]
|
|
|
|
response = client.delete(
|
|
f"/api/v1/comments/{comment_id}",
|
|
headers=user_headers,
|
|
)
|
|
assert response.status_code == 204
|
|
|
|
def test_delete_comment_not_owner(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
user2_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test deleting another user's comment returns 403.
|
|
|
|
TC-API-123: Negative — not owner.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
comment_resp = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
comment_id = comment_resp.json()["id"]
|
|
|
|
response = client.delete(
|
|
f"/api/v1/comments/{comment_id}",
|
|
headers=user2_headers,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
class TestLikeComment:
|
|
"""Tests for POST /api/v1/comments/{comment_id}/like — like a comment."""
|
|
|
|
def test_like_comment_authenticated(
|
|
self,
|
|
client: TestClient,
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test liking a comment as authenticated user.
|
|
|
|
TC-API-124: Positive — authenticated like on.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
comment_resp = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
comment_id = comment_resp.json()["id"]
|
|
|
|
response = client.post(
|
|
f"/api/v1/comments/{comment_id}/like",
|
|
headers=user_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["like_count"] == 1
|
|
assert data["id"] == comment_id
|
|
|
|
def test_like_comment_as_guest(
|
|
self,
|
|
client: TestClient,
|
|
guest_headers: dict[str, str],
|
|
user_headers: dict[str, str],
|
|
created_post: dict[str, Any],
|
|
) -> None:
|
|
"""Test liking a comment as guest returns 401.
|
|
|
|
TC-API-125: Negative — guest cannot like.
|
|
"""
|
|
post_id = created_post["id"]
|
|
|
|
comment_resp = client.post(
|
|
f"{API_PREFIX}/{post_id}/comments",
|
|
json={"content": COMMENT_CONTENT},
|
|
headers=user_headers,
|
|
)
|
|
comment_id = comment_resp.json()["id"]
|
|
|
|
response = client.post(
|
|
f"/api/v1/comments/{comment_id}/like",
|
|
headers=guest_headers,
|
|
)
|
|
assert response.status_code == 401
|