import pytest from django.core.files.uploadedfile import SimpleUploadedFile from apps.blog.models import Category, Comment, Like, Post, PostAsset, SavedPost, Tag pytestmark = [pytest.mark.integration, pytest.mark.django_db] def test_public_blog_lists_empty_content(client): response = client.get("/api/blog/posts") assert response.status_code == 200 assert response.json() == [] def test_admin_blog_requires_authentication(client): assert client.get("/api/blog/admin/posts").status_code == 401 @pytest.fixture def blog_content(user_factory): author = user_factory() category = Category.objects.create(name="Events") child = Category.objects.create(name="News", parent=category) tag = Tag.objects.create(name="Featured") first = Post.objects.create(title="First story", content="**First** story", author=author, category=child, status=Post.StatusChoices.PUBLISHED, is_featured=True) first.tags.add(tag) second = Post.objects.create(title="Second story", content="Second content", author=author, category=category, status=Post.StatusChoices.PUBLISHED) draft = Post.objects.create(title="Private draft", content="Draft", author=author) return author, category, child, tag, first, second, draft def test_public_filters_posts_and_recommendations(client, blog_content): author, category, child, tag, first, second, draft = blog_content assert {post["slug"] for post in client.get("/api/blog/posts").json()} == {first.slug, second.slug} assert {post["slug"] for post in client.get("/api/blog/posts", {"category": category.slug}).json()} == {first.slug, second.slug} assert [post["slug"] for post in client.get("/api/blog/posts", {"tag": tag.slug}).json()] == [first.slug] assert [post["slug"] for post in client.get("/api/blog/posts", {"search": "Second"}).json()] == [second.slug] assert [post["slug"] for post in client.get("/api/blog/posts", {"featured": "true"}).json()] == [first.slug] assert {post["slug"] for post in client.get("/api/blog/posts", {"author": author.username}).json()} == {first.slug, second.slug} assert client.get("/api/blog/posts", {"category": "unknown"}).json() == [] assert client.get(f"/api/blog/posts/{draft.slug}").status_code == 404 assert client.get(f"/api/blog/posts/{first.slug}").json()["title"] == first.title assert [post["slug"] for post in client.get(f"/api/blog/posts/{first.slug}/recommended").json()] == [second.slug] filters = client.get("/api/blog/filters").json() assert filters["categories"][0]["children"][0]["slug"] == child.slug assert filters["tags"][0]["post_count"] == 1 assert filters["authors"][0]["post_count"] == 2 def test_public_taxonomy_and_comment_visibility(client, blog_content, user_factory): author, category, child, tag, post, _, _ = blog_content member = user_factory() approved = Comment.objects.create(post=post, author=member, content="Visible") hidden = Comment.objects.create(post=post, author=member, content="Hidden", is_hidden=True) assert [row["slug"] for row in client.get("/api/blog/categories").json()] == [category.slug, child.slug] assert client.get(f"/api/blog/categories/{category.slug}").json()["name"] == category.name assert client.get(f"/api/blog/tags/{tag.slug}").json()["name"] == tag.name assert [row["id"] for row in client.get(f"/api/blog/posts/{post.slug}/comments").json()] == [approved.id] assert hidden.id != approved.id def test_blog_interactions_and_activity(client, blog_content, user_factory, auth_headers, mocker): _, _, _, _, post, _, _ = blog_content member = user_factory() headers = auth_headers(member) mocker.patch("apps.blog.api.views.notify_user") assert client.post(f"/api/blog/posts/{post.slug}/like", **headers).json()["liked"] is True assert Like.objects.filter(post=post, user=member).exists() assert client.post(f"/api/blog/posts/{post.slug}/save", **headers).json()["saved"] is True assert SavedPost.objects.filter(post=post, user=member).exists() response = client.post(f"/api/blog/posts/{post.slug}/comments", data={"content": "A useful comment"}, content_type="application/json", **headers) assert response.status_code == 201, response.content comment = Comment.objects.get(pk=response.json()["id"]) assert comment.content == "A useful comment" assert client.get(f"/api/blog/posts/{post.slug}/interaction", **headers).json()["comments_count"] == 1 activity = client.get("/api/blog/me/activity", **headers).json() assert activity["liked_posts"][0]["id"] == post.id assert activity["saved_posts"][0]["id"] == post.id assert activity["comments"][0]["id"] == comment.id assert client.put(f"/api/blog/comments/{comment.id}", data={"content": "Edited"}, content_type="application/json", **headers).status_code == 200 comment.refresh_from_db() assert comment.content == "Edited" assert client.post(f"/api/blog/posts/{post.slug}/like", **headers).json()["liked"] is False assert not Like.objects.filter(post=post, user=member).exists() def test_my_blog_activity_paginates_each_category_independently(client, blog_content, user_factory, auth_headers): _, _, _, _, _, _, _ = blog_content member = user_factory() headers = auth_headers(member) posts = [ Post.objects.create(title=f"Saved and liked {index}", content="Content", author=member, status=Post.StatusChoices.PUBLISHED) for index in range(12) ] for post in posts: Like.objects.create(post=post, user=member) SavedPost.objects.create(post=post, user=member) Comment.objects.create(post=post, author=member, content=f"Comment {post.id}") liked_page = client.get("/api/blog/me/activity/liked", {"page": 1}, **headers) saved_page = client.get("/api/blog/me/activity/saved", {"page": 2}, **headers) comments_page = client.get("/api/blog/me/activity/comments", {"page": 2}, **headers) assert liked_page.status_code == 200, liked_page.content assert liked_page.json()["count"] == 12 assert liked_page.json()["page_size"] == 10 assert len(liked_page.json()["results"]) == 10 assert len(saved_page.json()["results"]) == 2 assert len(comments_page.json()["results"]) == 2 assert comments_page.json()["total_pages"] == 2 def test_blog_editor_review_and_moderation(client, blog_content, user_factory, auth_headers, mocker): author, _, _, _, _, _, _ = blog_content editor = user_factory(is_staff=True) member = user_factory() editor_headers = auth_headers(editor) member_headers = auth_headers(member) mocker.patch("apps.blog.api.views.notify_user") assert client.get("/api/blog/admin/posts", **member_headers).status_code == 403 created = client.post("/api/blog/admin/posts", data={"title": "Reviewed story", "content": "Body"}, content_type="application/json", **editor_headers) assert created.status_code == 201, created.content post = Post.objects.get(pk=created.json()["id"]) assert post.author == editor and post.status == Post.StatusChoices.DRAFT submitted = client.post(f"/api/blog/admin/posts/{post.id}/submit", **editor_headers) assert submitted.status_code == 200 post.refresh_from_db() assert post.status == Post.StatusChoices.SUBMITTED invalid = client.post(f"/api/blog/admin/posts/{post.id}/review", data={"action": "nope"}, content_type="application/json", **editor_headers) assert invalid.status_code == 400 reviewed = client.post(f"/api/blog/admin/posts/{post.id}/review", data={"action": "publish"}, content_type="application/json", **editor_headers) assert reviewed.status_code == 200, reviewed.content post.refresh_from_db() assert post.status == Post.StatusChoices.PUBLISHED and post.published_by == editor assert client.get(f"/api/blog/posts/{post.slug}").status_code == 200 response = client.post(f"/api/blog/posts/{post.slug}/comments", data={"content": "Review me"}, content_type="application/json", **member_headers) comment = Comment.objects.get(pk=response.json()["id"]) assert client.post(f"/api/blog/comments/{comment.id}/hide", data={"note": "spam"}, content_type="application/json", **member_headers).status_code == 403 assert client.post(f"/api/blog/comments/{comment.id}/hide", data={"note": "spam"}, content_type="application/json", **editor_headers).status_code == 200 comment.refresh_from_db() assert comment.is_hidden assert client.get(f"/api/blog/posts/{post.slug}/comments").json() == [] assert client.post(f"/api/blog/comments/{comment.id}/unhide", **editor_headers).status_code == 200 comment.refresh_from_db() assert not comment.is_hidden assert client.post(f"/api/blog/comments/{comment.id}/delete", data={"note": "remove"}, content_type="application/json", **editor_headers).status_code == 200 assert Comment.deleted_objects.filter(pk=comment.pk).exists() assert client.post(f"/api/blog/deleted/comments/{comment.id}/restore", **editor_headers).status_code == 200 assert Comment.objects.filter(pk=comment.pk).exists() def test_blog_taxonomy_admin_crud_and_restore(client, user_factory, auth_headers): admin = user_factory(is_superuser=True, is_staff=True) member = user_factory() admin_headers = auth_headers(admin) assert client.get("/api/blog/admin/categories", **auth_headers(member)).status_code == 403 root = client.post("/api/blog/admin/categories", data={"name": "Research"}, content_type="application/json", **admin_headers) assert root.status_code == 201, root.content category_id = root.json()["id"] assert Category.objects.filter(pk=category_id).exists() child = client.post("/api/blog/admin/categories", data={"name": "Reports", "parent_id": category_id}, content_type="application/json", **admin_headers) assert child.status_code == 201, child.content invalid = client.put(f"/api/blog/admin/categories/{category_id}", data={"name": "Research", "parent_id": child.json()["id"]}, content_type="application/json", **admin_headers) assert invalid.status_code == 400 assert Category.objects.get(pk=category_id).parent_id is None tag = client.post("/api/blog/admin/tags", data={"name": "Python"}, content_type="application/json", **admin_headers) assert tag.status_code == 201, tag.content tag_id = tag.json()["id"] assert client.put(f"/api/blog/admin/tags/{tag_id}", data={"name": "Django"}, content_type="application/json", **admin_headers).status_code == 200 assert Tag.objects.get(pk=tag_id).name == "Django" assert client.delete(f"/api/blog/admin/tags/{tag_id}", **admin_headers).status_code == 200 assert Tag.deleted_objects.filter(pk=tag_id).exists() assert client.post(f"/api/blog/deleted/tags/{tag_id}/restore", **admin_headers).status_code == 200 assert Tag.objects.filter(pk=tag_id).exists() def test_blog_post_admin_listing_update_assets_and_soft_delete(client, blog_content, user_factory, auth_headers, tmp_path, settings): settings.MEDIA_ROOT = tmp_path author, _, _, _, post, _, draft = blog_content admin = user_factory(is_superuser=True, is_staff=True) member = user_factory() headers = auth_headers(admin) assert client.get("/api/blog/admin/posts", **headers).status_code == 200 assert client.get(f"/api/blog/admin/posts/{post.pk}", **auth_headers(member)).status_code == 403 assert client.get(f"/api/blog/admin/posts/{post.pk}", **headers).json()["id"] == post.pk updated = client.put(f"/api/blog/admin/posts/{draft.pk}", data='{"title":"New title","content":"Changed"}', content_type="application/json", **headers) assert updated.status_code == 200, updated.content draft.refresh_from_db() assert draft.title == "New title" and draft.content == "Changed" assert client.get(f"/api/blog/admin/posts/{post.pk}/assets", **headers).json() == [] invalid = client.post(f"/api/blog/admin/posts/{post.pk}/assets", {"file": SimpleUploadedFile("unsafe.exe", b"data", content_type="application/octet-stream")}, **headers) assert invalid.status_code == 400 asset = client.post(f"/api/blog/admin/posts/{post.pk}/assets", {"file": SimpleUploadedFile("notes.txt", b"notes", content_type="text/plain"), "title": "Notes"}, **headers) assert asset.status_code == 201, asset.content asset_id = asset.json()["id"] assert PostAsset.objects.filter(pk=asset_id, post=post).exists() assert len(client.get(f"/api/blog/admin/posts/{post.pk}/assets", **headers).json()) == 1 assert client.delete(f"/api/blog/admin/posts/{post.pk}/assets/{asset_id}", **headers).status_code == 200 assert not PostAsset.objects.filter(pk=asset_id).exists() assert client.delete(f"/api/blog/posts/{post.slug}", **auth_headers(author)).status_code == 403 assert client.delete(f"/api/blog/posts/{post.slug}", **headers).status_code == 200 assert Post.deleted_objects.filter(pk=post.pk).exists() assert client.get("/api/blog/deleted/posts", **headers).json()[0]["id"] == post.pk assert client.post(f"/api/blog/deleted/posts/{post.pk}/restore", **headers).status_code == 200 assert Post.objects.filter(pk=post.pk).exists() def test_blog_comment_reply_and_edit_authorization(client, blog_content, user_factory, auth_headers, mocker): _, _, _, _, post, _, _ = blog_content first_user = user_factory() second_user = user_factory() notify = mocker.patch("apps.blog.api.views.notify_user") root = client.post(f"/api/blog/posts/{post.slug}/comments", data='{"content":"Root"}', content_type="application/json", **auth_headers(first_user)) assert root.status_code == 201, root.content root_id = root.json()["id"] reply = client.post(f"/api/blog/posts/{post.slug}/comments", data=f'{{"content":"Reply","parent_id":{root_id}}}', content_type="application/json", **auth_headers(second_user)) assert reply.status_code == 201, reply.content assert Comment.objects.get(pk=reply.json()["id"]).parent_id == root_id assert notify.call_count >= 2 assert client.put(f"/api/blog/comments/{root_id}", data='{"content":"Changed"}', content_type="application/json", **auth_headers(second_user)).status_code == 403 comments = client.get(f"/api/blog/posts/{post.slug}/comments").json() assert comments[0]["replies"][0]["id"] == reply.json()["id"] def test_blog_taxonomy_deletion_and_validation(client, auth_headers, user_factory): admin = user_factory(is_superuser=True, is_staff=True) headers = auth_headers(admin) category = Category.objects.create(name="Delete category") tag = Tag.objects.create(name="Delete tag") assert client.post("/api/blog/admin/categories", data='{"name":" "}', content_type="application/json", **headers).status_code == 400 assert client.post("/api/blog/admin/tags", data='{"name":" "}', content_type="application/json", **headers).status_code == 400 assert client.delete(f"/api/blog/admin/categories/{category.pk}", **headers).status_code == 200 assert client.get("/api/blog/deleted/categories", **headers).json()[0]["id"] == category.pk assert client.post(f"/api/blog/deleted/categories/{category.pk}/restore", **headers).status_code == 200 assert Category.objects.filter(pk=category.pk).exists() assert client.post("/api/blog/deleted/categories/999999/restore", **headers).status_code == 400 assert client.get(f"/api/blog/tags/{tag.slug}").json()["id"] == tag.pk def test_blog_featured_image_and_compatibility_routes(client, blog_content, user_factory, auth_headers, tmp_path, settings): settings.MEDIA_ROOT = tmp_path _, _, _, _, post, _, _ = blog_content admin = user_factory(is_staff=True, is_superuser=True) headers = auth_headers(admin) assert client.get("/api/blog/admin/writers", **headers).status_code == 200 assert client.get("/api/blog/admin/writers", **auth_headers(user_factory())).status_code == 403 invalid = client.post(f"/api/blog/admin/posts/{post.pk}/featured-image", {"file": SimpleUploadedFile("notes.txt", b"text", content_type="text/plain")}, **headers) assert invalid.status_code == 400 uploaded = client.post(f"/api/blog/admin/posts/{post.pk}/featured-image", {"file": SimpleUploadedFile("cover.png", b"image", content_type="image/png")}, **headers) assert uploaded.status_code == 200, uploaded.content post.refresh_from_db() assert post.featured_image assert client.delete(f"/api/blog/admin/posts/{post.pk}/featured-image", **headers).status_code == 200 post.refresh_from_db() assert not post.featured_image compatible = client.post("/api/blog/posts", data='{"title":"Compat","content":"Body"}', content_type="application/json", **headers) assert compatible.status_code == 201, compatible.content compat_post = Post.objects.get(pk=compatible.json()["id"]) update = client.put(f"/api/blog/posts/{compat_post.slug}", data='{"title":"Compat edited","content":"New body"}', content_type="application/json", **headers) assert update.status_code == 200, update.content compat_post.refresh_from_db() assert compat_post.title == "Compat edited" def test_blog_review_changes_and_taxonomy_parent_validation(client, blog_content, user_factory, auth_headers): _, _, _, _, _, _, draft = blog_content admin = user_factory(is_staff=True) headers = auth_headers(admin) changed = client.post(f"/api/blog/admin/posts/{draft.pk}/review", data='{"action":"request_changes","note":"Revise details"}', content_type="application/json", **headers) assert changed.status_code == 200, changed.content draft.refresh_from_db() assert draft.status == Post.StatusChoices.CHANGES_REQUESTED and draft.review_note == "Revise details" archived = client.post(f"/api/blog/admin/posts/{draft.pk}/review", data='{"action":"archive"}', content_type="application/json", **headers) assert archived.status_code == 200 draft.refresh_from_db() assert draft.status == Post.StatusChoices.ARCHIVED root = Category.objects.create(name="Root for validation") child = Category.objects.create(name="Child for validation", parent=root) invalid = client.put(f"/api/blog/admin/categories/{child.pk}", data=f'{{"name":"Child for validation","parent_id":{child.pk}}}', content_type="application/json", **headers) assert invalid.status_code == 400 child.refresh_from_db() assert child.parent_id == root.pk def test_blog_filters_accept_multiple_tag_values_and_page_results(client, blog_content): _, _, _, tag, first, second, _ = blog_content tagged = client.get("/api/blog/posts", {"tag": f"{tag.slug},unknown,{tag.slug}"}) assert tagged.status_code == 200 assert [row["id"] for row in tagged.json()] == [first.pk] first_page = client.get("/api/blog/posts", {"page": 1, "limit": 1}).json() second_page = client.get("/api/blog/posts", {"page": 2, "limit": 1}).json() assert len(first_page) == len(second_page) == 1 assert {first_page[0]["id"], second_page[0]["id"]} == {first.pk, second.pk} assert client.get("/api/blog/posts", {"page": 3, "limit": 1}).json() == [] assert [row["id"] for row in client.get(f"/api/blog/posts/{second.slug}/recommended").json()] == [first.pk]