Files
2025-12-05 09:13:52 +01:00

422 lines
17 KiB
Python

from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from app.feeds.models import Profile, Post, Mention
class NotificationsViewTest(TestCase):
"""Test cases for the NotificationsView API using Given/When/Then structure."""
def setUp(self):
self.client = APIClient()
self.notifications_url = "/notifications/"
# Create test profiles
self.profile1 = Profile.objects.create(
feed="https://example.com/social.org",
title="Example Profile",
nick="example_user",
description="Test profile 1",
)
self.profile2 = Profile.objects.create(
feed="https://test.com/social.org",
title="Test Profile",
nick="test_user",
description="Test profile 2",
)
self.profile3 = Profile.objects.create(
feed="https://third.com/social.org",
title="Third Profile",
nick="third_user",
description="Test profile 3",
)
# Create posts from profile1
self.post1 = Post.objects.create(
profile=self.profile1,
post_id="2025-01-01T12:00:00+00:00",
content="Original post 1",
)
self.post2 = Post.objects.create(
profile=self.profile1,
post_id="2025-01-01T13:00:00+00:00",
content="Original post 2",
)
# Create a mention
self.mention_post = Post.objects.create(
profile=self.profile2,
post_id="2025-01-01T14:00:00+00:00",
content="This post mentions @example_user",
)
Mention.objects.create(
post=self.mention_post,
mentioned_profile=self.profile1,
nickname="example_user",
)
# Create a reaction
self.reaction_post = Post.objects.create(
profile=self.profile2,
post_id="2025-01-01T15:00:00+00:00",
content="",
mood="👍",
reply_to=f"{self.profile1.feed}#{self.post1.post_id}",
)
# Create a reply
self.reply_post = Post.objects.create(
profile=self.profile3,
post_id="2025-01-01T16:00:00+00:00",
content="This is a reply to post 2",
mood="",
reply_to=f"{self.profile1.feed}#{self.post2.post_id}",
)
def test_get_all_notifications_success(self):
"""Test GET /notifications/?feed=<feed_url> returns all notification types."""
# Given: A profile with mentions, reactions, and replies
feed_url = self.profile1.feed
# When: We request all notifications for the profile
response = self.client.get(self.notifications_url, {"feed": feed_url})
# Then: We should get notifications successfully
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["errors"], [])
# Then: Response should contain all notification types
data = response.data["data"]
self.assertIsInstance(data, list)
self.assertEqual(len(data), 3) # 1 mention + 1 reaction + 1 reply
# Then: Should have different types
types = {notif["type"] for notif in data}
self.assertEqual(types, {"mention", "reaction", "reply"})
# Then: Meta should include by_type breakdown
meta = response.data["meta"]
self.assertEqual(meta["feed"], feed_url)
self.assertEqual(meta["total"], 3)
self.assertIn("by_type", meta)
self.assertEqual(meta["by_type"]["mentions"], 1)
self.assertEqual(meta["by_type"]["reactions"], 1)
self.assertEqual(meta["by_type"]["replies"], 1)
# Then: Should have ETag and Last-Modified headers
self.assertIn("ETag", response)
self.assertIn("Last-Modified", response)
def test_get_notifications_filtered_by_mention(self):
"""Test GET /notifications/?feed=<feed_url>&type=mention returns only mentions."""
# Given: A profile with all notification types
feed_url = self.profile1.feed
# When: We request only mentions
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "mention"}
)
# Then: Should only return mentions
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["type"], "mention")
# Then: Meta should show filtered results
meta = response.data["meta"]
self.assertEqual(meta["total"], 1)
def test_get_notifications_filtered_by_reaction(self):
"""Test GET /notifications/?feed=<feed_url>&type=reaction returns only reactions."""
# Given: A profile with all notification types
feed_url = self.profile1.feed
# When: We request only reactions
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "reaction"}
)
# Then: Should only return reactions
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["type"], "reaction")
self.assertIn("emoji", data[0])
self.assertIn("parent", data[0])
def test_get_notifications_filtered_by_reply(self):
"""Test GET /notifications/?feed=<feed_url>&type=reply returns only replies."""
# Given: A profile with all notification types
feed_url = self.profile1.feed
# When: We request only replies
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "reply"}
)
# Then: Should only return replies
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["type"], "reply")
self.assertIn("parent", data[0])
def test_get_notifications_invalid_type(self):
"""Test GET /notifications/ with invalid type parameter returns 400."""
# Given: A valid feed URL but invalid type
feed_url = self.profile1.feed
# When: We request with invalid type
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "invalid"}
)
# Then: Should return 400 error
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["type"], "Error")
self.assertIn("Invalid type parameter", response.data["errors"][0])
def test_get_notifications_no_notifications(self):
"""Test GET /notifications/ returns empty array for profile with no notifications."""
# Given: A profile with no notifications
feed_url = self.profile2.feed
# When: We request notifications for the profile
response = self.client.get(self.notifications_url, {"feed": feed_url})
# Then: We should get empty array
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["errors"], [])
self.assertEqual(response.data["data"], [])
# Then: Meta should show zero counts
meta = response.data["meta"]
self.assertEqual(meta["feed"], feed_url)
self.assertEqual(meta["total"], 0)
self.assertEqual(meta["by_type"]["mentions"], 0)
self.assertEqual(meta["by_type"]["reactions"], 0)
self.assertEqual(meta["by_type"]["replies"], 0)
def test_get_notifications_nonexistent_profile(self):
"""Test GET /notifications/ returns 404 for nonexistent profile."""
# Given: A feed URL that doesn't exist
nonexistent_feed = "https://nonexistent.com/social.org"
# When: We request notifications for nonexistent profile
response = self.client.get(self.notifications_url, {"feed": nonexistent_feed})
# Then: We should get 404 error
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data["type"], "Error")
self.assertIn("Profile not found", response.data["errors"][0])
self.assertIsNone(response.data["data"])
def test_notifications_missing_parameters(self):
"""Test that missing required parameters return 400 error."""
# Given: Notifications endpoint
# When: We request without required parameters
response = self.client.get(self.notifications_url)
# Then: Should return 400 error
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["type"], "Error")
self.assertIn("required", response.data["errors"][0])
def test_notifications_response_format_compliance(self):
"""Test notifications response format compliance with README specification."""
# Given: A profile with notifications exists
feed_url = self.profile1.feed
# When: We request notifications
response = self.client.get(self.notifications_url, {"feed": feed_url})
# Then: Response should match expected format
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("type", response.data)
self.assertIn("errors", response.data)
self.assertIn("data", response.data)
self.assertIn("meta", response.data)
self.assertIn("_links", response.data)
self.assertEqual(response.data["type"], "Success")
self.assertIsInstance(response.data["errors"], list)
self.assertIsInstance(response.data["data"], list)
self.assertIsInstance(response.data["meta"], dict)
# Then: Each notification should have type field
for notification in response.data["data"]:
self.assertIn("type", notification)
self.assertIn("post", notification)
def test_notifications_view_methods_allowed(self):
"""Test that only GET method is allowed on notifications endpoint."""
# Given: A valid notifications URL
params = {"feed": self.profile1.feed}
# When: We try different HTTP methods
post_response = self.client.post(self.notifications_url, params)
put_response = self.client.put(self.notifications_url, params)
delete_response = self.client.delete(self.notifications_url, params)
patch_response = self.client.patch(self.notifications_url, params)
# Then: Unsupported methods should return 405
self.assertEqual(post_response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
self.assertEqual(put_response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
self.assertEqual(
delete_response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED
)
self.assertEqual(patch_response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_notifications_sorted_by_post_id(self):
"""Test that notifications are sorted by post_id in descending order."""
# Given: Multiple notifications with different post_ids
# (already set up in setUp with ascending timestamps)
# When: We request all notifications
response = self.client.get(self.notifications_url, {"feed": self.profile1.feed})
# Then: Should be sorted by post_id descending (most recent first)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
# Extract post_ids from the notification posts
post_ids = []
for notif in data:
post_url = notif["post"]
post_id = post_url.split("#")[1]
post_ids.append(post_id)
# Then: post_ids should be in descending order
self.assertEqual(post_ids, sorted(post_ids, reverse=True))
def test_notifications_mention_structure(self):
"""Test that mention notifications have correct structure."""
# Given: A profile with a mention
feed_url = self.profile1.feed
# When: We request mention notifications
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "mention"}
)
# Then: Mention should have type and post fields only
self.assertEqual(response.status_code, status.HTTP_200_OK)
mention = response.data["data"][0]
self.assertEqual(mention["type"], "mention")
self.assertIn("post", mention)
self.assertNotIn("emoji", mention)
self.assertNotIn("parent", mention)
def test_notifications_reaction_structure(self):
"""Test that reaction notifications have correct structure."""
# Given: A profile with a reaction
feed_url = self.profile1.feed
# When: We request reaction notifications
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "reaction"}
)
# Then: Reaction should have type, post, emoji, and parent fields
self.assertEqual(response.status_code, status.HTTP_200_OK)
reaction = response.data["data"][0]
self.assertEqual(reaction["type"], "reaction")
self.assertIn("post", reaction)
self.assertIn("emoji", reaction)
self.assertIn("parent", reaction)
def test_notifications_reply_structure(self):
"""Test that reply notifications have correct structure."""
# Given: A profile with a reply
feed_url = self.profile1.feed
# When: We request reply notifications
response = self.client.get(
self.notifications_url, {"feed": feed_url, "type": "reply"}
)
# Then: Reply should have type, post, and parent fields
self.assertEqual(response.status_code, status.HTTP_200_OK)
reply = response.data["data"][0]
self.assertEqual(reply["type"], "reply")
self.assertIn("post", reply)
self.assertIn("parent", reply)
self.assertNotIn("emoji", reply)
def test_get_notifications_with_boosts(self):
"""Test GET /notifications/?feed=<feed_url> includes boosts."""
# Given: A profile with posts that have been boosted
boost_post = Post.objects.create(
profile=self.profile2,
post_id="2025-01-01T17:00:00+00:00",
content="Boosting this amazing post!",
include=f"{self.profile1.feed}#{self.post1.post_id}",
)
# When: We request all notifications for the profile
response = self.client.get(self.notifications_url, {"feed": self.profile1.feed})
# Then: We should get notifications including the boost
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
# Find the boost notification
boost_notifications = [n for n in data if n["type"] == "boost"]
self.assertEqual(len(boost_notifications), 1)
boost_notif = boost_notifications[0]
self.assertEqual(boost_notif["type"], "boost")
self.assertEqual(
boost_notif["post"], f"{self.profile2.feed}#{boost_post.post_id}"
)
self.assertEqual(
boost_notif["boosted"], f"{self.profile1.feed}#{self.post1.post_id}"
)
# Then: Meta should include boost count
meta = response.data["meta"]
self.assertEqual(meta["by_type"]["boosts"], 1)
self.assertEqual(meta["total"], 4) # 1 mention + 1 reaction + 1 reply + 1 boost
def test_get_notifications_filtered_by_boost(self):
"""Test GET /notifications/?feed=<feed_url>&type=boost returns only boosts."""
# Given: A profile with all notification types including boosts
Post.objects.create(
profile=self.profile2,
post_id="2025-01-01T17:00:00+00:00",
content="Boosting!",
include=f"{self.profile1.feed}#{self.post1.post_id}",
)
Post.objects.create(
profile=self.profile3,
post_id="2025-01-01T18:00:00+00:00",
content="",
include=f"{self.profile1.feed}#{self.post2.post_id}",
)
# When: We request only boosts
response = self.client.get(
self.notifications_url, {"feed": self.profile1.feed, "type": "boost"}
)
# Then: Should only return boosts
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data["data"]
self.assertEqual(len(data), 2)
for boost in data:
self.assertEqual(boost["type"], "boost")
self.assertIn("post", boost)
self.assertIn("boosted", boost)
self.assertNotIn("emoji", boost)
self.assertNotIn("parent", boost)
# Then: Meta should show filtered results
meta = response.data["meta"]
self.assertEqual(meta["total"], 2)
self.assertEqual(meta["by_type"]["boosts"], 2)