Files
andros 347369868d Add /profile/ endpoint returning feed followers
New GET /profile/?feed={url} endpoint that returns the list of feed URLs
that follow the given profile, using the existing Follow model.
Includes cache support, 400/404 error handling, and 8 unit tests.
2026-05-16 11:58:45 +02:00

177 lines
6.7 KiB
Python

from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from app.feeds.models import Follow, Profile
class ProfileViewTest(TestCase):
"""Test cases for the ProfileView API using Given/When/Then structure."""
def setUp(self):
self.client = APIClient()
self.url = "/profile/"
self.profile_main = Profile.objects.create(
feed="https://example.com/social.org",
title="Example Profile",
nick="example_user",
)
self.profile_alice = Profile.objects.create(
feed="https://alice.org/social.org",
title="Alice",
nick="alice",
)
self.profile_bob = Profile.objects.create(
feed="https://bob.org/social.org",
title="Bob",
nick="bob",
)
self.profile_carol = Profile.objects.create(
feed="https://carol.org/social.org",
title="Carol",
nick="carol",
)
Follow.objects.create(follower=self.profile_alice, followed=self.profile_main)
Follow.objects.create(follower=self.profile_bob, followed=self.profile_main)
def test_get_profile_with_followers(self):
"""Test GET /profile/?feed=<url> returns correct followers list."""
# Given: A profile followed by two users
feed_url = self.profile_main.feed
# When: We request the profile
response = self.client.get(self.url, {"feed": feed_url})
# Then: Response is successful
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["errors"], [])
# Then: Data contains feed and followers
data = response.data["data"]
self.assertEqual(data["feed"], feed_url)
self.assertIsInstance(data["followers"], list)
self.assertIn(self.profile_alice.feed, data["followers"])
self.assertIn(self.profile_bob.feed, data["followers"])
self.assertNotIn(self.profile_carol.feed, data["followers"])
# Then: Meta contains correct counts
meta = response.data["meta"]
self.assertEqual(meta["feed"], feed_url)
self.assertEqual(meta["total_followers"], 2)
def test_get_profile_no_followers(self):
"""Test GET /profile/ returns empty followers for profile with no followers."""
# Given: A profile with no followers
feed_url = self.profile_carol.feed
# When: We request the profile
response = self.client.get(self.url, {"feed": feed_url})
# Then: Response is successful with empty followers
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["type"], "Success")
self.assertEqual(response.data["data"]["followers"], [])
self.assertEqual(response.data["meta"]["total_followers"], 0)
def test_get_profile_not_found(self):
"""Test GET /profile/ returns 404 for non-existent feed."""
# Given: A feed URL not registered in the relay
nonexistent_feed = "https://nonexistent.com/social.org"
# When: We request the profile
response = self.client.get(self.url, {"feed": nonexistent_feed})
# Then: We get 404
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_get_profile_missing_feed_param(self):
"""Test GET /profile/ returns 400 when feed param is missing."""
# When: We request without the feed parameter
response = self.client.get(self.url)
# Then: We get 400
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_get_profile_response_format(self):
"""Test GET /profile/ response matches README specification."""
# Given: A registered profile
feed_url = self.profile_main.feed
# When: We request the profile
response = self.client.get(self.url, {"feed": feed_url})
# Then: Response keys are correct
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)
data = response.data["data"]
self.assertIn("feed", data)
self.assertIn("followers", data)
meta = response.data["meta"]
self.assertIn("feed", meta)
self.assertIn("total_followers", meta)
links = response.data["_links"]
self.assertIn("self", links)
self.assertIn("href", links["self"])
self.assertIn("method", links["self"])
self.assertEqual(links["self"]["method"], "GET")
def test_get_profile_self_link_encoded(self):
"""Test that the self link URL-encodes the feed parameter."""
# Given: A feed URL with special characters
feed_url = self.profile_main.feed
# When: We request the profile
response = self.client.get(self.url, {"feed": feed_url})
# Then: Self link is URL-encoded
self.assertEqual(response.status_code, status.HTTP_200_OK)
href = response.data["_links"]["self"]["href"]
self.assertIn("%3A", href) # : is encoded
self.assertIn("%2F", href) # / is encoded
def test_get_profile_http_methods(self):
"""Test that only GET is allowed on the profile endpoint."""
# Given: A valid feed URL
params = {"feed": self.profile_main.feed}
# When/Then: Non-GET methods return 405
self.assertEqual(
self.client.post(self.url, params).status_code,
status.HTTP_405_METHOD_NOT_ALLOWED,
)
self.assertEqual(
self.client.put(self.url, params).status_code,
status.HTTP_405_METHOD_NOT_ALLOWED,
)
self.assertEqual(
self.client.delete(self.url, params).status_code,
status.HTTP_405_METHOD_NOT_ALLOWED,
)
def test_get_profile_followers_are_feeds_only(self):
"""Test that followers list contains only feed URLs, not profile metadata."""
# Given: A profile with a follower
feed_url = self.profile_main.feed
# When: We request the profile
response = self.client.get(self.url, {"feed": feed_url})
# Then: Each follower entry is a plain URL string
for follower in response.data["data"]["followers"]:
self.assertIsInstance(follower, str)
self.assertTrue(follower.startswith("http"))