mirror of
https://github.com/tanrax/org-social-relay
synced 2026-01-10 15:03:33 +01:00
342 lines
14 KiB
Python
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)
|