Files
blog.pyaqa.ru/tests/api/test_comments.py
Sergey Vanyushkin 7ff3fa0992
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
feat: add comments feature with nested replies and recursive rendering
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.
2026-05-11 15:34:20 +03:00

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