mirror of
https://github.com/tanrax/org-social-relay
synced 2026-01-10 15:03:33 +01:00
495 lines
20 KiB
Python
495 lines
20 KiB
Python
from django.test import TestCase
|
|
from rest_framework.test import APIClient
|
|
from rest_framework import status
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
from app.feeds.models import Profile, Post, PollOption, PollVote
|
|
|
|
|
|
class PollsViewTest(TestCase):
|
|
"""Test cases for the PollsView API using Given/When/Then structure."""
|
|
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.polls_url = "/polls/"
|
|
|
|
# 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",
|
|
)
|
|
|
|
# Create a test poll (active)
|
|
self.active_poll = Post.objects.create(
|
|
profile=self.profile1,
|
|
post_id="2025-01-01T12:00:00+00:00",
|
|
content="What's your favorite programming language?\n\n- [ ] Python\n- [ ] JavaScript\n- [ ] PHP\n- [ ] Emacs Lisp",
|
|
poll_end=timezone.now() + timedelta(hours=1),
|
|
)
|
|
|
|
# Create poll options
|
|
PollOption.objects.create(post=self.active_poll, option_text="Python", order=1)
|
|
PollOption.objects.create(
|
|
post=self.active_poll, option_text="JavaScript", order=2
|
|
)
|
|
PollOption.objects.create(post=self.active_poll, option_text="PHP", order=3)
|
|
PollOption.objects.create(
|
|
post=self.active_poll, option_text="Emacs Lisp", order=4
|
|
)
|
|
|
|
# Create an expired poll
|
|
self.expired_poll = Post.objects.create(
|
|
profile=self.profile1,
|
|
post_id="2024-12-01T12:00:00+00:00",
|
|
content="Old poll - What do you think?\n\n- [ ] Yes\n- [ ] No",
|
|
poll_end=timezone.now() - timedelta(hours=1),
|
|
)
|
|
|
|
PollOption.objects.create(post=self.expired_poll, option_text="Yes", order=1)
|
|
PollOption.objects.create(post=self.expired_poll, option_text="No", order=2)
|
|
|
|
# Create a vote post
|
|
self.vote_post = Post.objects.create(
|
|
profile=self.profile2,
|
|
post_id="2025-01-01T13:00:00+00:00",
|
|
content="I choose Python!",
|
|
reply_to=f"{self.profile1.feed}#{self.active_poll.post_id}",
|
|
)
|
|
|
|
# Create a poll vote
|
|
PollVote.objects.create(
|
|
post=self.vote_post,
|
|
poll_post=self.active_poll,
|
|
poll_option="Python",
|
|
)
|
|
|
|
def test_get_all_polls(self):
|
|
"""Test GET /polls returns all polls URLs (active and expired)."""
|
|
# Given: Active and expired polls exist
|
|
# (Setup already creates these)
|
|
|
|
# When: We request all polls
|
|
response = self.client.get(self.polls_url)
|
|
|
|
# Then: We should get both active and expired polls as URLs
|
|
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) # Both active and expired polls
|
|
|
|
# Then: Response should contain poll URLs in the correct format
|
|
expected_active_url = f"{self.profile1.feed}#{self.active_poll.post_id}"
|
|
expected_expired_url = f"{self.profile1.feed}#{self.expired_poll.post_id}"
|
|
|
|
self.assertIn(expected_active_url, response.data["data"])
|
|
self.assertIn(expected_expired_url, response.data["data"])
|
|
|
|
# Then: All data items should be strings (URLs)
|
|
for poll_url in response.data["data"]:
|
|
self.assertIsInstance(poll_url, str)
|
|
self.assertIn("#", poll_url) # Should contain the # separator
|
|
|
|
# Then: Meta should contain total
|
|
self.assertIn("meta", response.data)
|
|
self.assertEqual(response.data["meta"]["total"], 2)
|
|
|
|
# Then: Should have ETag and Last-Modified headers
|
|
self.assertIn("ETag", response)
|
|
self.assertIn("Last-Modified", response)
|
|
|
|
def test_get_polls_for_specific_feed(self):
|
|
"""Test GET /polls?feed=<url> returns polls for specific feed."""
|
|
# Given: Polls from different profiles exist
|
|
# (Setup already creates these)
|
|
|
|
# When: We request polls for a specific feed
|
|
response = self.client.get(self.polls_url, {"feed": self.profile1.feed})
|
|
|
|
# Then: We should get polls from that feed only
|
|
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) # Both active and expired
|
|
|
|
# Then: Response should contain feed metadata
|
|
self.assertEqual(response.data["meta"]["feed"], self.profile1.feed)
|
|
self.assertEqual(response.data["meta"]["total"], 2)
|
|
|
|
# Then: Should have ETag and Last-Modified headers
|
|
self.assertIn("ETag", response)
|
|
self.assertIn("Last-Modified", response)
|
|
|
|
def test_get_polls_for_nonexistent_feed(self):
|
|
"""Test GET /polls?feed=<nonexistent> returns 404."""
|
|
# Given: A feed URL that doesn't exist
|
|
nonexistent_feed = "https://nonexistent.com/social.org"
|
|
|
|
# When: We request polls for nonexistent feed
|
|
response = self.client.get(self.polls_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_get_voter_polls(self):
|
|
"""Test GET /polls?voter=<url> returns votes cast by specific voter."""
|
|
# Given: Voter has cast votes
|
|
# (Setup already creates a vote)
|
|
|
|
# When: We request votes for a specific voter
|
|
response = self.client.get(self.polls_url, {"voter": self.profile2.feed})
|
|
|
|
# Then: We should get votes cast by that voter
|
|
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"]), 1)
|
|
|
|
# Then: Vote data should be complete
|
|
vote_data = response.data["data"][0]
|
|
self.assertEqual(
|
|
vote_data["vote_post_id"], f"{self.profile2.feed}#{self.vote_post.post_id}"
|
|
)
|
|
self.assertEqual(
|
|
vote_data["poll_id"], f"{self.profile1.feed}#{self.active_poll.post_id}"
|
|
)
|
|
self.assertEqual(vote_data["poll_author"], self.profile1.nick)
|
|
self.assertEqual(vote_data["selected_option"], "Python")
|
|
|
|
# Then: Response should contain voter metadata
|
|
self.assertEqual(response.data["meta"]["voter"], self.profile2.feed)
|
|
self.assertEqual(response.data["meta"]["total"], 1)
|
|
|
|
def test_get_voter_polls_nonexistent_voter(self):
|
|
"""Test GET /polls?voter=<nonexistent> returns 404."""
|
|
# Given: A voter feed URL that doesn't exist
|
|
nonexistent_voter = "https://nonexistent.com/social.org"
|
|
|
|
# When: We request votes for nonexistent voter
|
|
response = self.client.get(self.polls_url, {"voter": nonexistent_voter})
|
|
|
|
# Then: We should get 404 error
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
self.assertEqual(response.data["type"], "Error")
|
|
self.assertIn("Voter profile not found", response.data["errors"][0])
|
|
self.assertIsNone(response.data["data"])
|
|
|
|
def test_polls_response_format_compliance(self):
|
|
"""Test GET /polls response format compliance."""
|
|
# Given: Polls exist in the database
|
|
# (Setup already creates these)
|
|
|
|
# When: We request polls
|
|
response = self.client.get(self.polls_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.assertEqual(response.data["type"], "Success")
|
|
self.assertIsInstance(response.data["errors"], list)
|
|
self.assertIsInstance(response.data["data"], list)
|
|
self.assertIsInstance(response.data["meta"], dict)
|
|
|
|
def test_polls_view_methods_allowed(self):
|
|
"""Test that only GET method is allowed on polls endpoint."""
|
|
# Given: The polls endpoint
|
|
|
|
# When: We try different HTTP methods
|
|
post_response = self.client.post(self.polls_url)
|
|
put_response = self.client.put(self.polls_url)
|
|
delete_response = self.client.delete(self.polls_url)
|
|
patch_response = self.client.patch(self.polls_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)
|
|
|
|
|
|
class PollVotesViewTest(TestCase):
|
|
"""Test cases for the PollVotesView API using Given/When/Then structure."""
|
|
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
|
|
# 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://voter.com/social.org",
|
|
title="Voter Profile",
|
|
nick="voter_user",
|
|
description="Test profile 3",
|
|
)
|
|
|
|
# Create a test poll
|
|
self.poll = Post.objects.create(
|
|
profile=self.profile1,
|
|
post_id="2025-01-01T12:00:00+00:00",
|
|
content="What's your favorite framework?\n\n- [ ] Django\n- [ ] Flask\n- [ ] FastAPI",
|
|
poll_end=timezone.now() + timedelta(hours=1),
|
|
)
|
|
|
|
# Create poll options
|
|
PollOption.objects.create(post=self.poll, option_text="Django", order=1)
|
|
PollOption.objects.create(post=self.poll, option_text="Flask", order=2)
|
|
PollOption.objects.create(post=self.poll, option_text="FastAPI", order=3)
|
|
|
|
# Create vote posts and poll votes
|
|
self.vote_post1 = Post.objects.create(
|
|
profile=self.profile2,
|
|
post_id="2025-01-01T13:00:00+00:00",
|
|
content="Django is great!",
|
|
reply_to=f"{self.profile1.feed}#{self.poll.post_id}",
|
|
)
|
|
|
|
self.vote_post2 = Post.objects.create(
|
|
profile=self.profile3,
|
|
post_id="2025-01-01T14:00:00+00:00",
|
|
content="I prefer Flask",
|
|
reply_to=f"{self.profile1.feed}#{self.poll.post_id}",
|
|
)
|
|
|
|
PollVote.objects.create(
|
|
post=self.vote_post1,
|
|
poll_post=self.poll,
|
|
poll_option="Django",
|
|
)
|
|
|
|
PollVote.objects.create(
|
|
post=self.vote_post2,
|
|
poll_post=self.poll,
|
|
poll_option="Flask",
|
|
)
|
|
|
|
# Create a non-poll post for testing
|
|
self.non_poll = Post.objects.create(
|
|
profile=self.profile1,
|
|
post_id="2025-01-01T11:00:00+00:00",
|
|
content="This is just a regular post",
|
|
)
|
|
|
|
def test_get_poll_votes_success(self):
|
|
"""Test GET /polls/votes/?post=<post_url> returns poll votes."""
|
|
# Given: A poll with votes exists
|
|
poll_votes_url = "/polls/votes/"
|
|
post_url = f"{self.profile1.feed}#{self.poll.post_id}"
|
|
|
|
# When: We request votes for the poll
|
|
response = self.client.get(poll_votes_url, {"post": post_url})
|
|
|
|
# Then: We should get poll votes 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 vote data in README format
|
|
data = response.data["data"]
|
|
self.assertIsInstance(data, list)
|
|
self.assertEqual(len(data), 3) # 3 options
|
|
|
|
# Then: Each option should have the correct structure
|
|
option_names = [item["option"] for item in data]
|
|
self.assertIn("Django", option_names)
|
|
self.assertIn("Flask", option_names)
|
|
self.assertIn("FastAPI", option_names)
|
|
|
|
# Then: Check vote counts
|
|
django_votes = next(item for item in data if item["option"] == "Django")[
|
|
"votes"
|
|
]
|
|
flask_votes = next(item for item in data if item["option"] == "Flask")["votes"]
|
|
fastapi_votes = next(item for item in data if item["option"] == "FastAPI")[
|
|
"votes"
|
|
]
|
|
|
|
self.assertEqual(len(django_votes), 1)
|
|
self.assertEqual(len(flask_votes), 1)
|
|
self.assertEqual(len(fastapi_votes), 0)
|
|
|
|
# Then: Votes should be URLs in correct format
|
|
self.assertIn(f"{self.profile2.feed}#{self.vote_post1.post_id}", django_votes)
|
|
self.assertIn(f"{self.profile3.feed}#{self.vote_post2.post_id}", flask_votes)
|
|
|
|
# Then: Meta should contain correct information
|
|
meta = response.data["meta"]
|
|
self.assertEqual(meta["poll"], post_url)
|
|
self.assertEqual(meta["total_votes"], 2)
|
|
|
|
# Then: Should have ETag and Last-Modified headers
|
|
self.assertIn("ETag", response)
|
|
self.assertIn("Last-Modified", response)
|
|
|
|
def test_get_poll_votes_nonexistent_poll(self):
|
|
"""Test GET /polls/votes/ returns 404 for nonexistent poll."""
|
|
# Given: A poll ID that doesn't exist
|
|
nonexistent_post_url = f"{self.profile1.feed}#2025-01-01T00:00:00+00:00"
|
|
poll_votes_url = "/polls/votes/"
|
|
|
|
# When: We request votes for nonexistent poll
|
|
response = self.client.get(poll_votes_url, {"post": nonexistent_post_url})
|
|
|
|
# Then: We should get 404 error
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
self.assertEqual(response.json()["type"], "Error")
|
|
self.assertIn("Poll not found", response.json()["errors"][0])
|
|
self.assertIsNone(response.json()["data"])
|
|
|
|
def test_get_poll_votes_nonexistent_profile(self):
|
|
"""Test GET /polls/votes/ returns 404 for nonexistent profile."""
|
|
# Given: A profile feed that doesn't exist
|
|
nonexistent_post_url = f"https://nonexistent.com/social.org#{self.poll.post_id}"
|
|
poll_votes_url = "/polls/votes/"
|
|
|
|
# When: We request votes for poll from nonexistent profile
|
|
response = self.client.get(poll_votes_url, {"post": nonexistent_post_url})
|
|
|
|
# Then: We should get 404 error
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
self.assertEqual(response.json()["type"], "Error")
|
|
self.assertIn("Poll not found", response.json()["errors"][0])
|
|
self.assertIsNone(response.json()["data"])
|
|
|
|
def test_get_votes_for_non_poll_post(self):
|
|
"""Test GET /polls/votes/ returns 400 for non-poll post."""
|
|
# Given: A regular post (not a poll)
|
|
poll_votes_url = "/polls/votes/"
|
|
non_poll_url = f"{self.profile1.feed}#{self.non_poll.post_id}"
|
|
|
|
# When: We request votes for non-poll post
|
|
response = self.client.get(poll_votes_url, {"post": non_poll_url})
|
|
|
|
# Then: We should get 400 error
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
self.assertEqual(response.data["type"], "Error")
|
|
self.assertIn("Post is not a poll", response.data["errors"][0])
|
|
self.assertIsNone(response.data["data"])
|
|
|
|
def test_poll_votes_response_format_compliance(self):
|
|
"""Test poll votes response format compliance."""
|
|
# Given: A poll with votes exists
|
|
poll_votes_url = "/polls/votes/"
|
|
post_url = f"{self.profile1.feed}#{self.poll.post_id}"
|
|
|
|
# When: We request poll votes
|
|
response = self.client.get(poll_votes_url, {"post": post_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.assertEqual(response.data["type"], "Success")
|
|
self.assertIsInstance(response.data["errors"], list)
|
|
self.assertIsInstance(response.data["data"], list)
|
|
self.assertIsInstance(response.data["meta"], dict)
|
|
|
|
# Then: Data should be array of options with votes
|
|
data = response.data["data"]
|
|
for option in data:
|
|
self.assertIn("option", option)
|
|
self.assertIn("votes", option)
|
|
self.assertIsInstance(option["votes"], list)
|
|
|
|
def test_poll_votes_view_methods_allowed(self):
|
|
"""Test that only GET method is allowed on poll votes endpoint."""
|
|
# Given: A valid poll votes URL
|
|
poll_votes_url = "/polls/votes/"
|
|
params = {"post": f"{self.profile1.feed}#{self.poll.post_id}"}
|
|
|
|
# When: We try different HTTP methods
|
|
post_response = self.client.post(poll_votes_url, params)
|
|
put_response = self.client.put(poll_votes_url, params)
|
|
delete_response = self.client.delete(poll_votes_url, params)
|
|
patch_response = self.client.patch(poll_votes_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_poll_votes_url_encoding_handling(self):
|
|
"""Test that special characters in post parameter are handled correctly."""
|
|
# Given: A feed URL that has special characters
|
|
special_profile = Profile.objects.create(
|
|
feed="https://example.com/user name/social.org",
|
|
title="Special Profile",
|
|
nick="special_user",
|
|
description="Profile with special characters",
|
|
)
|
|
|
|
special_poll = Post.objects.create(
|
|
profile=special_profile,
|
|
post_id="2025-01-01T15:00:00+00:00",
|
|
content="Test poll with special feed URL",
|
|
poll_end=timezone.now() + timedelta(hours=1),
|
|
)
|
|
|
|
# When: We request votes using post parameter
|
|
poll_votes_url = "/polls/votes/"
|
|
post_url = f"{special_profile.feed}#{special_poll.post_id}"
|
|
response = self.client.get(poll_votes_url, {"post": post_url})
|
|
|
|
# Then: Request should be handled correctly
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data["type"], "Success")
|
|
meta = response.data["meta"]
|
|
self.assertEqual(meta["poll"], post_url)
|
|
|
|
def test_poll_votes_missing_parameters(self):
|
|
"""Test that missing required parameters return 400 error."""
|
|
# Given: Poll votes endpoint
|
|
poll_votes_url = "/polls/votes/"
|
|
|
|
# When: We request without required parameters
|
|
response_no_params = self.client.get(poll_votes_url)
|
|
|
|
# Then: Should return 400 error
|
|
self.assertEqual(response_no_params.status_code, status.HTTP_400_BAD_REQUEST)
|
|
self.assertEqual(response_no_params.data["type"], "Error")
|
|
self.assertIn("required", response_no_params.data["errors"][0])
|
|
|
|
def test_poll_votes_invalid_post_format(self):
|
|
"""Test that invalid post URL format returns 400 error."""
|
|
# Given: Poll votes endpoint
|
|
poll_votes_url = "/polls/votes/"
|
|
|
|
# When: We request with invalid post format (no # separator)
|
|
response = self.client.get(poll_votes_url, {"post": "invalid_url_format"})
|
|
|
|
# Then: Should return 400 error
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
self.assertEqual(response.data["type"], "Error")
|
|
self.assertIn("Invalid post URL format", response.data["errors"][0])
|