Files
org-social-relay/app/feeds/test_feeds.py
2025-12-05 09:13:52 +01:00

342 lines
14 KiB
Python

import json
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from app.feeds.models import Feed
class FeedsViewTest(TestCase):
"""Test cases for the FeedsView API using Given/When/Then structure."""
def setUp(self):
self.client = APIClient()
self.feeds_url = "/feeds/" # Adjust based on your URL configuration
def test_get_empty_feeds_list(self):
"""Test GET /feeds returns empty list when no feeds exist."""
# Given: No feeds in the database
Feed.objects.all().delete()
# When: We request the feeds list
response = self.client.get(self.feeds_url)
# Then: We should get an 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_get_feeds_list_with_data(self):
"""Test GET /feeds returns list of feeds when feeds exist."""
# Given: Some feeds exist in the database
Feed.objects.create(url="https://example.com/social.org")
Feed.objects.create(url="https://test.dev/social.org")
Feed.objects.create(url="https://demo.net/social.org")
# When: We request the feeds list
response = self.client.get(self.feeds_url)
# Then: We should get all feeds 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(len(response.data["data"]), 3)
# Then: All feed URLs should be in the response
feed_urls = response.data["data"]
self.assertIn("https://example.com/social.org", feed_urls)
self.assertIn("https://test.dev/social.org", feed_urls)
self.assertIn("https://demo.net/social.org", feed_urls)
def test_post_new_valid_feed_success(self):
"""Test POST /feeds creates a new valid feed successfully."""
# Given: A new feed URL that is valid and accessible
feed_url = "https://andros.dev/static/social.org"
# Clean existing feed if any
Feed.objects.filter(url=feed_url).delete()
# When: We POST the new feed
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url}),
content_type="application/json",
)
# Then: We should get success response with 201 status
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["errors"], [])
self.assertEqual(response.data["data"]["feed"], feed_url)
# Then: The feed should be created in the database
self.assertTrue(Feed.objects.filter(url=feed_url).exists())
def test_post_existing_feed_returns_200(self):
"""Test POST /feeds returns 200 for existing feed without creating duplicate."""
# Given: A feed that already exists
feed_url = "https://existing.com/social.org"
Feed.objects.create(url=feed_url)
initial_count = Feed.objects.count()
# When: We POST the same feed URL again
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url}),
content_type="application/json",
)
# Then: We should get success response with 200 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"]["feed"], feed_url)
# Then: No duplicate feed should be created
self.assertEqual(Feed.objects.count(), initial_count)
def test_post_feed_with_whitespace_handling(self):
"""Test POST /feeds handles whitespace in feed URLs correctly."""
# Given: A valid feed URL with leading/trailing whitespace
feed_url_with_spaces = " https://andros.dev/static/social.org "
clean_feed_url = "https://andros.dev/static/social.org"
# Clean existing feed if any
Feed.objects.filter(url=clean_feed_url).delete()
# When: We POST the feed with whitespace
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url_with_spaces}),
content_type="application/json",
)
# Then: We should get success response
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["data"]["feed"], clean_feed_url)
# Then: The feed should be stored without whitespace
feed = Feed.objects.get(url=clean_feed_url)
self.assertEqual(feed.url, clean_feed_url)
def test_post_feed_missing_url_parameter(self):
"""Test POST /feeds returns error when feed URL is missing."""
# Given: A request without feed parameter
request_data = {}
# When: We POST without feed parameter
response = self.client.post(
self.feeds_url,
data=json.dumps(request_data),
content_type="application/json",
)
# 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 is required"])
self.assertIsNone(response.data["data"])
def test_post_feed_empty_url_parameter(self):
"""Test POST /feeds returns error when feed URL is empty."""
# Given: A request with empty feed parameter
request_data = {"feed": ""}
# When: We POST with empty feed parameter
response = self.client.post(
self.feeds_url,
data=json.dumps(request_data),
content_type="application/json",
)
# 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 is required"])
self.assertIsNone(response.data["data"])
def test_post_feed_whitespace_only_url(self):
"""Test POST /feeds returns error when feed URL contains only whitespace."""
# Given: A request with whitespace-only feed parameter
request_data = {"feed": " "}
# When: We POST with whitespace-only feed parameter
response = self.client.post(
self.feeds_url,
data=json.dumps(request_data),
content_type="application/json",
)
# 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 is required"])
self.assertIsNone(response.data["data"])
def test_post_feed_invalid_json(self):
"""Test POST /feeds handles invalid JSON gracefully."""
# Given: Invalid JSON data
invalid_json = '{"feed": "https://test.com/social.org"' # Missing closing brace
# When: We POST with invalid JSON
response = self.client.post(
self.feeds_url, data=invalid_json, content_type="application/json"
)
# Then: We should get a client error response
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_get_feeds_response_format_compliance(self):
"""Test GET /feeds response format matches README specification."""
# Given: Some feeds in the database
Feed.objects.create(url="https://example.com/social.org")
Feed.objects.create(url="https://another-example.com/social.org")
# When: We request the feeds list
response = self.client.get(self.feeds_url)
# Then: Response should match exact README 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)
def test_post_feed_response_format_compliance(self):
"""Test POST /feeds response format matches README specification."""
# Given: A valid feed URL
feed_url = "https://rossabaker.com/social.org"
# Clean existing feed if any
Feed.objects.filter(url=feed_url).delete()
# When: We POST the new feed
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url}),
content_type="application/json",
)
# Then: Response should match exact README format
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
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"], dict)
self.assertIn("feed", response.data["data"])
def test_feeds_view_methods_allowed(self):
"""Test that only GET and POST methods are allowed on feeds endpoint."""
# Given: The feeds endpoint
# When: We try different HTTP methods
put_response = self.client.put(self.feeds_url)
delete_response = self.client.delete(self.feeds_url)
patch_response = self.client.patch(self.feeds_url)
# Then: Unsupported methods should return 405
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_post_invalid_feed_url_404(self):
"""Test POST /feeds returns error for invalid feed URL (404)."""
# Given: A feed URL that returns 404
feed_url = "https://example.com/nonexistent.org"
# When: We POST the invalid feed
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url}),
content_type="application/json",
)
# 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.assertIn("Invalid Org Social feed", response.data["errors"][0])
self.assertIsNone(response.data["data"])
def test_post_invalid_feed_content(self):
"""Test POST /feeds returns error for URL with invalid content."""
# Given: A URL that returns HTML instead of Org Social content
feed_url = "https://www.google.com"
# When: We POST the URL with invalid content
response = self.client.post(
self.feeds_url,
data=json.dumps({"feed": feed_url}),
content_type="application/json",
)
# 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.assertIn("Invalid Org Social feed", response.data["errors"][0])
# Accept either encoding errors or missing metadata errors
error_message = response.data["errors"][0]
self.assertTrue(
"missing basic metadata" in error_message
or "Validation error" in error_message,
f"Expected validation error, got: {error_message}",
)
self.assertIsNone(response.data["data"])
def test_get_feeds_has_caching_headers(self):
"""Test GET /feeds returns ETag and Last-Modified headers."""
# Given: Some feeds in the database
Feed.objects.create(url="https://example.com/social.org")
Feed.objects.create(url="https://test.com/social.org")
# When: We request the feeds list
response = self.client.get(self.feeds_url)
# Then: Should have ETag and Last-Modified headers
self.assertIn("ETag", response)
self.assertIn("Last-Modified", response)
# Then: ETag should be properly formatted (quoted)
etag = response["ETag"]
self.assertTrue(etag.startswith('"') and etag.endswith('"'))
# Then: Last-Modified should be in RFC 2822 format
last_modified = response["Last-Modified"]
self.assertIsNotNone(last_modified)
# Format example: "Wed, 05 Nov 2025 10:15:00 GMT"
self.assertIn("GMT", last_modified)
def test_get_feeds_etag_is_global_and_consistent(self):
"""Test GET /feeds ETag is global and doesn't change when feeds are added."""
# Given: Initial feeds
Feed.objects.create(url="https://example.com/social.org")
# When: We request the feeds list
response1 = self.client.get(self.feeds_url)
etag1 = response1["ETag"]
# When: We add a new feed
Feed.objects.create(url="https://new.com/social.org")
# When: We request the feeds list again
response2 = self.client.get(self.feeds_url)
etag2 = response2["ETag"]
# Then: ETag should be the same (global ETag, only changes with scan_feeds)
self.assertEqual(etag1, etag2)
def test_get_empty_feeds_has_caching_headers(self):
"""Test GET /feeds returns caching headers even when empty."""
# Given: No feeds in the database
Feed.objects.all().delete()
# When: We request the feeds list
response = self.client.get(self.feeds_url)
# Then: Should still have caching headers
self.assertIn("ETag", response)
self.assertIn("Last-Modified", response)