Files
org-social-relay/app/feeds/test_mentions.py
2025-11-05 15:03:20 +01:00

300 lines
12 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 MentionsViewTest(TestCase):
"""Test cases for the MentionsView API using Given/When/Then structure."""
def setUp(self):
self.client = APIClient()
self.mentions_url = "/mentions/"
# Create test profiles
self.profile1 = Profile.objects.create(
feed="https://alice.example.com/social.org",
title="Alice Smith",
nick="alice",
description="Alice's profile",
)
self.profile2 = Profile.objects.create(
feed="https://bob.example.com/social.org",
title="Bob Johnson",
nick="bob",
description="Bob's profile",
)
self.profile3 = Profile.objects.create(
feed="https://charlie.example.com/social.org",
title="Charlie Brown",
nick="charlie",
description="Charlie's profile",
)
# Create test posts
self.post1 = Post.objects.create(
profile=self.profile2,
post_id="2024-01-01T10:00:00Z",
content="Hello @alice, how are you doing?",
)
self.post2 = Post.objects.create(
profile=self.profile3,
post_id="2024-01-01T11:00:00Z",
content="Hey @alice, check this out!",
)
# Create test mentions
self.mention1 = Mention.objects.create(
post=self.post1,
mentioned_profile=self.profile1,
nickname="alice",
)
self.mention2 = Mention.objects.create(
post=self.post2,
mentioned_profile=self.profile1,
nickname="alice",
)
def test_get_mentions_success(self):
"""Test GET /mentions returns mentions for a specific feed."""
# Given: A profile with mentions
feed_url = "https://alice.example.com/social.org"
# When: We request mentions for this feed
response = self.client.get(self.mentions_url, {"feed": feed_url})
# Then: We should get all mentions for this profile
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["errors"], [])
self.assertEqual(len(response.data["data"]), 2)
# Then: Check mention data structure (should be URLs only)
mentions = response.data["data"]
self.assertIsInstance(mentions, list)
self.assertEqual(len(mentions), 2)
# Then: Check that data contains post URLs
for mention_url in mentions:
self.assertIsInstance(mention_url, str)
self.assertIn("#", mention_url) # Should contain post ID after #
# Then: Check meta field exists and has required structure
self.assertIn("meta", response.data)
meta = response.data["meta"]
self.assertIn("feed", meta)
self.assertIn("total", meta)
self.assertEqual(meta["feed"], "https://alice.example.com/social.org")
self.assertEqual(meta["total"], 2)
# Then: Should have ETag and Last-Modified headers
self.assertIn("ETag", response)
self.assertIn("Last-Modified", response)
def test_get_mentions_missing_feed_parameter(self):
"""Test GET /mentions returns error when feed parameter is missing."""
# Given: A request without feed parameter
# When: We request mentions without feed parameter
response = self.client.get(self.mentions_url)
# Then: We should get error response with 400 status
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["type"], "Error")
self.assertEqual(response.data["errors"], ["Feed URL parameter is required"])
self.assertIsNone(response.data["data"])
def test_get_mentions_empty_feed_parameter(self):
"""Test GET /mentions returns error when feed parameter is empty."""
# Given: A request with empty feed parameter
feed_url = ""
# When: We request mentions with empty feed parameter
response = self.client.get(self.mentions_url, {"feed": feed_url})
# Then: We should get error response with 400 status
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["type"], "Error")
self.assertEqual(response.data["errors"], ["Feed URL parameter is required"])
self.assertIsNone(response.data["data"])
def test_get_mentions_whitespace_feed_parameter(self):
"""Test GET /mentions returns error when feed parameter is only whitespace."""
# Given: A request with whitespace-only feed parameter
feed_url = " "
# When: We request mentions with whitespace-only feed parameter
response = self.client.get(self.mentions_url, {"feed": feed_url})
# Then: We should get error response with 400 status
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["type"], "Error")
self.assertEqual(response.data["errors"], ["Feed URL parameter is required"])
self.assertIsNone(response.data["data"])
def test_get_mentions_profile_not_found(self):
"""Test GET /mentions returns error when profile doesn't exist."""
# Given: A feed URL that doesn't exist in our database
feed_url = "https://nonexistent.example.com/social.org"
# When: We request mentions for this non-existent feed
response = self.client.get(self.mentions_url, {"feed": feed_url})
# Then: We should get error response with 404 status
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data["type"], "Error")
self.assertEqual(
response.data["errors"], ["Profile not found for the given feed URL"]
)
self.assertIsNone(response.data["data"])
def test_get_mentions_no_mentions(self):
"""Test GET /mentions returns empty list when profile has no mentions."""
# Given: A profile with no mentions
feed_url = "https://bob.example.com/social.org"
# When: We request mentions for this profile
response = self.client.get(self.mentions_url, {"feed": feed_url})
# Then: We should get empty list with success status
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"], [])
def test_mentions_response_format_compliance(self):
"""Test GET /mentions response format compliance."""
# Given: A profile with mentions
feed_url = "https://alice.example.com/social.org"
# When: We request mentions for this feed
response = self.client.get(self.mentions_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.assertEqual(response.data["type"], "Success")
self.assertIsInstance(response.data["errors"], list)
self.assertIsInstance(response.data["data"], list)
# Then: Each mention should be a URL string
for mention in response.data["data"]:
self.assertIsInstance(mention, str)
self.assertIn("#", mention) # Should contain post ID after #
# Then: Check meta field
self.assertIn("meta", response.data)
meta = response.data["meta"]
self.assertIn("feed", meta)
self.assertIn("total", meta)
# Then: Should have ETag and Last-Modified headers
self.assertIn("ETag", response)
self.assertIn("Last-Modified", response)
def test_mentions_view_only_get_allowed(self):
"""Test that only GET method is allowed on mentions endpoint."""
# Given: The mentions endpoint
feed_url = "https://alice.example.com/social.org"
# When: We try different HTTP methods
post_response = self.client.post(self.mentions_url, {"feed": feed_url})
put_response = self.client.put(self.mentions_url, {"feed": feed_url})
delete_response = self.client.delete(self.mentions_url, {"feed": feed_url})
patch_response = self.client.patch(self.mentions_url, {"feed": feed_url})
# 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_get_mentions_with_whitespace_handling(self):
"""Test GET /mentions handles whitespace in feed URLs correctly."""
# Given: A valid feed URL with leading/trailing whitespace
feed_url_with_spaces = " https://alice.example.com/social.org "
# When: We request mentions with whitespace
response = self.client.get(self.mentions_url, {"feed": feed_url_with_spaces})
# Then: We should get success response
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(len(response.data["data"]), 2)
def test_mentions_ordering_most_recent_first(self):
"""Test that mentions are ordered from most recent to oldest."""
# Given: A profile with multiple mentions created at different times
import time
# Create an additional profile and posts with specific timestamps
profile4 = Profile.objects.create(
feed="https://david.example.com/social.org",
title="David Wilson",
nick="david",
description="David's profile",
)
# Create posts with different timestamps
post3 = Post.objects.create(
profile=profile4,
post_id="2024-01-01T08:00:00Z",
content="Early mention of @alice",
)
post4 = Post.objects.create(
profile=profile4,
post_id="2024-01-01T12:00:00Z",
content="Later mention of @alice",
)
# Create mentions with specific creation times
# Note: Since we can't control created_at directly, we'll create them in order
# and rely on the database's auto-timestamp behavior
Mention.objects.create(
post=post3,
mentioned_profile=self.profile1,
nickname="alice",
)
# Add a small delay to ensure different timestamps
time.sleep(0.01)
Mention.objects.create(
post=post4,
mentioned_profile=self.profile1,
nickname="alice",
)
# When: We request mentions for alice
response = self.client.get(
self.mentions_url, {"feed": "https://alice.example.com/social.org"}
)
# Then: We should get mentions in reverse chronological order (newest first)
self.assertEqual(response.status_code, status.HTTP_200_OK)
mentions_urls = response.data["data"]
# Should have 4 mentions total (2 from setUp + 2 new ones)
self.assertEqual(len(mentions_urls), 4)
# The most recent post should be first (based on post timestamp, not mention creation time)
# post4 has timestamp 12:00:00 which is later than post3 (08:00:00)
first_mention_url = mentions_urls[0]
self.assertIn(
"2024-01-01T12:00:00Z", first_mention_url
) # post4 timestamp (later in day)
# Verify all URLs are properly formatted
for url in mentions_urls:
self.assertIn("https://", url)
self.assertIn("#", url)