Browse Source

Added explicit json comparisions to poll api

Rafał Pitoń 7 years ago
parent
commit
7792602cd1

+ 7 - 8
misago/threads/api/pollvotecreateendpoint.py

@@ -13,21 +13,20 @@ def poll_vote_create(request, thread, poll):
     allow_vote_poll(request.user, poll)
     allow_vote_poll(request.user, poll)
 
 
     serializer = NewVoteSerializer(
     serializer = NewVoteSerializer(
-        data={
-            'choices': request.data,
-        },
+        # FIXME: lets use {'choices': []} JSON instead!
+        data={'choices': request.data},
         context={
         context={
             'allowed_choices': poll.allowed_choices,
             'allowed_choices': poll.allowed_choices,
             'choices': poll.choices,
             'choices': poll.choices,
         },
         },
     )
     )
 
 
-    if not serializer.is_valid():
-        return Response(
-            {'detail': serializer.errors}, status=400)
+    serializer.is_valid(raise_exception=True)
+
+    validated_choices = serializer.validated_data['choices']
 
 
-    remove_user_votes(request.user, poll, serializer.data['choices'])
-    set_new_votes(request, poll, serializer.data['choices'])
+    remove_user_votes(request.user, poll, validated_choices)
+    set_new_votes(request, poll, validated_choices)
 
 
     add_acl(request.user, poll)
     add_acl(request.user, poll)
     serialized_poll = PollSerializer(poll).data
     serialized_poll = PollSerializer(poll).data

+ 0 - 3
misago/threads/tests/test_thread_pollcreate_api.py

@@ -146,9 +146,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             'allowed_choices': ["This field is required."],
             'allowed_choices': ["This field is required."],
         })
         })
 
 
-        response_json = response.json()
-        self.assertEqual(len(response_json), 4)
-
     def test_length_validation(self):
     def test_length_validation(self):
         """api validates poll's length"""
         """api validates poll's length"""
         response = self.post(
         response = self.post(

+ 195 - 122
misago/threads/tests/test_thread_polledit_api.py

@@ -3,6 +3,9 @@ from datetime import timedelta
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
+from misago.acl import add_acl
+from misago.core.utils import serialize_datetime
+from misago.threads.models import Poll
 from misago.threads.serializers.poll import MAX_POLL_OPTIONS
 from misago.threads.serializers.poll import MAX_POLL_OPTIONS
 
 
 from .test_thread_poll_api import ThreadPollApiTestCase
 from .test_thread_poll_api import ThreadPollApiTestCase
@@ -20,6 +23,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This action is not available to guests.",
+        })
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
@@ -33,6 +39,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
@@ -46,6 +55,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "No Thread matches the given query.",
+        })
 
 
     def test_invalid_poll_id(self):
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
         """api validates that poll id is integer"""
@@ -59,6 +71,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_nonexistant_poll_id(self):
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
         """api validates that poll exists"""
@@ -72,13 +87,19 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates that user has permission to edit poll in thread"""
         """api validates that user has permission to edit poll in thread"""
         self.override_acl({'can_edit_polls': 0})
         self.override_acl({'can_edit_polls': 0})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "can't edit polls", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit polls.",
+        })
 
 
     def test_no_permission_timeout(self):
     def test_no_permission_timeout(self):
         """api validates that user's window to edit poll in thread has closed"""
         """api validates that user's window to edit poll in thread has closed"""
@@ -88,9 +109,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
         self.poll.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "can't edit polls that are older than 5 minutes", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit polls that are older than 5 minutes.",
+        })
 
 
     def test_no_permission_poll_closed(self):
     def test_no_permission_poll_closed(self):
         """api validates that user's window to edit poll in thread has closed"""
         """api validates that user's window to edit poll in thread has closed"""
@@ -101,7 +123,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
         self.poll.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "This poll is over", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This poll is over. You can't edit it.",
+        })
 
 
     def test_no_permission_other_user_poll(self):
     def test_no_permission_other_user_poll(self):
         """api validates that user has permission to edit other user poll in thread"""
         """api validates that user has permission to edit other user poll in thread"""
@@ -111,7 +136,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.save()
         self.poll.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "can't edit other users polls", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit other users polls in this category.",
+        })
 
 
     def test_no_permission_closed_thread(self):
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to edit poll in closed thread"""
         """api validates that user has permission to edit poll in closed thread"""
@@ -121,7 +149,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.thread.save()
         self.thread.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "thread is closed", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This thread is closed. You can't edit polls in it.",
+        })
 
 
         self.override_acl(category={'can_close_threads': 1})
         self.override_acl(category={'can_close_threads': 1})
 
 
@@ -136,7 +167,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.category.save()
         self.category.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "category is closed", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This category is closed. You can't edit polls in it.",
+        })
 
 
         self.override_acl(category={'can_close_threads': 1})
         self.override_acl(category={'can_close_threads': 1})
 
 
@@ -147,9 +181,12 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api handles empty request data"""
         """api handles empty request data"""
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json), 4)
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["This field is required."],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
     def test_length_validation(self):
     def test_length_validation(self):
         """api validates poll's length"""
         """api validates poll's length"""
@@ -159,29 +196,40 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
             }
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["This field is required."],
+            'length': ["Ensure this value is greater than or equal to 0."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
-        response_json = response.json()
-        self.assertEqual(
-            response_json['length'], ["Ensure this value is greater than or equal to 0."]
+        response = self.put(
+            self.api_link, data={
+                'length': 200,
+            }
         )
         )
-
-        response = self.put(self.api_link, data={'length': 200})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['length'], ["Ensure this value is less than or equal to 180."]
-        )
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["This field is required."],
+            'length': ["Ensure this value is less than or equal to 180."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
     def test_question_validation(self):
     def test_question_validation(self):
         """api validates question length"""
         """api validates question length"""
-        response = self.put(self.api_link, data={'question': 'abcd' * 255})
-        self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['question'], ["Ensure this field has no more than 255 characters."]
+        response = self.put(
+            self.api_link, data={
+                'question': 'abcd' * 255,
+            }
         )
         )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'question': ["Ensure this field has no more than 255 characters."],
+            'choices': ["This field is required."],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
     def test_validate_choice_length(self):
     def test_validate_choice_length(self):
         """api validates single choice length"""
         """api validates single choice length"""
@@ -196,9 +244,12 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
             }
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["One or more poll choices are invalid."],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
         response = self.put(
         response = self.put(
             self.api_link,
             self.api_link,
@@ -212,27 +263,23 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
             }
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
-
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["One or more poll choices are invalid."],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
+        
     def test_validate_two_choices(self):
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
         """api validates that there are at least two choices in poll"""
-        response = self.put(
-            self.api_link, data={
-                'choices': [
-                    {
-                        'label': 'Choice',
-                    },
-                ],
-            }
-        )
+        response = self.put(self.api_link, data={'choices': [{'label': 'Choice'}]})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['choices'], ["You need to add at least two choices to a poll."]
-        )
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["You need to add at least two choices to a poll."],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
     def test_validate_max_choices(self):
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
         """api validates that there are no more choices in poll than allowed number"""
@@ -248,22 +295,25 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['choices'],
-            ["You can't add more than %s options to a single poll (added %s)." % error_formats]
-        )
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': [
+                "You can't add more than %s options to a single poll (added %s)." % error_formats
+            ],
+            'length': ["This field is required."],
+            'allowed_choices': ["This field is required."],
+        })
 
 
     def test_allowed_choices_validation(self):
     def test_allowed_choices_validation(self):
         """api validates allowed choices number"""
         """api validates allowed choices number"""
         response = self.put(self.api_link, data={'allowed_choices': 0})
         response = self.put(self.api_link, data={'allowed_choices': 0})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['allowed_choices'], ["Ensure this value is greater than or equal to 1."]
-        )
+        self.assertEqual(response.json(), {
+            'question': ["This field is required."],
+            'choices': ["This field is required."],
+            'length': ["This field is required."],
+            'allowed_choices': ["Ensure this value is greater than or equal to 1."],
+        })
 
 
         response = self.put(
         response = self.put(
             self.api_link,
             self.api_link,
@@ -282,12 +332,11 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
             }
             }
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-
-        response_json = response.json()
-        self.assertEqual(
-            response_json['non_field_errors'],
-            ["Number of allowed choices can't be greater than number of all choices."]
-        )
+        self.assertEqual(response.json(), {
+            'non_field_errors': [
+                "Number of allowed choices can't be greater than number of all choices."
+            ],
+        })
 
 
     def test_poll_all_choices_replaced(self):
     def test_poll_all_choices_replaced(self):
         """api edits all poll choices out"""
         """api edits all poll choices out"""
@@ -314,6 +363,9 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
+        poll = Poll.objects.all()[0]
+        add_acl(self.user, poll)
+
         response_json = response.json()
         response_json = response.json()
 
 
         self.assertEqual(response_json['poster_name'], self.user.username)
         self.assertEqual(response_json['poster_name'], self.user.username)
@@ -372,22 +424,21 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = response.json()
-
-        self.assertEqual(response_json['poster_name'], self.user.username)
-        self.assertEqual(response_json['length'], 40)
-        self.assertEqual(response_json['question'], "Select two best colors")
-        self.assertEqual(response_json['allowed_choices'], 2)
-        self.assertTrue(response_json['allow_revotes'])
-
-        # you can't change poll's type after its posted
-        self.assertFalse(response_json['is_public'])
-
-        # choices were updated
-        self.assertEqual(len(response_json['choices']), 4)
-        self.assertEqual(
-            response_json['choices'],
-            [
+        poll = Poll.objects.all()[0]
+        add_acl(self.user, poll)
+
+        self.assertEqual(response.json(), {
+            'id': poll.id,
+            'poster_name': self.user.username,
+            'posted_on': serialize_datetime(poll.posted_on),
+            'length': 40,
+            'question': "Select two best colors",
+            'allowed_choices': 2,
+            'allow_revotes': True,
+            'votes': 4,
+            'is_public': False,
+            'acl': poll.acl,
+            'choices': [
                 {
                 {
                     'hash': 'aaaaaaaaaaaa',
                     'hash': 'aaaaaaaaaaaa',
                     'label': 'First',
                     'label': 'First',
@@ -413,10 +464,16 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     'selected': True,
                     'selected': True,
                 },
                 },
             ],
             ],
-        )
+            'api': {
+                'index': poll.get_api_url(),
+                'votes': poll.get_votes_api_url(),
+            },
+            'url': {
+                'poster': self.user.get_absolute_url(),
+            },
+        })
 
 
         # no votes were removed
         # no votes were removed
-        self.assertEqual(response_json['votes'], 4)
         self.assertEqual(self.poll.pollvote_set.count(), 4)
         self.assertEqual(self.poll.pollvote_set.count(), 4)
 
 
     def test_poll_some_choices_edited(self):
     def test_poll_some_choices_edited(self):
@@ -450,22 +507,21 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = response.json()
-
-        self.assertEqual(response_json['poster_name'], self.user.username)
-        self.assertEqual(response_json['length'], 40)
-        self.assertEqual(response_json['question'], "Select two best colors")
-        self.assertEqual(response_json['allowed_choices'], 2)
-        self.assertTrue(response_json['allow_revotes'])
-
-        # you can't change poll's type after its posted
-        self.assertFalse(response_json['is_public'])
-
-        # choices were updated
-        self.assertEqual(len(response_json['choices']), 3)
-        self.assertEqual(
-            response_json['choices'],
-            [
+        poll = Poll.objects.all()[0]
+        add_acl(self.user, poll)
+
+        self.assertEqual(response.json(), {
+            'id': poll.id,
+            'poster_name': self.user.username,
+            'posted_on': serialize_datetime(poll.posted_on),
+            'length': 40,
+            'question': "Select two best colors",
+            'allowed_choices': 2,
+            'allow_revotes': True,
+            'votes': 1,
+            'is_public': False,
+            'acl': poll.acl,
+            'choices': [
                 {
                 {
                     'hash': 'aaaaaaaaaaaa',
                     'hash': 'aaaaaaaaaaaa',
                     'label': 'First',
                     'label': 'First',
@@ -479,16 +535,22 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     'selected': False,
                     'selected': False,
                 },
                 },
                 {
                 {
-                    'hash': response_json['choices'][2]['hash'],
+                    'hash': poll.choices[2]['hash'],
                     'label': 'New Option',
                     'label': 'New Option',
                     'votes': 0,
                     'votes': 0,
                     'selected': False,
                     'selected': False,
                 },
                 },
             ],
             ],
-        )
+            'api': {
+                'index': poll.get_api_url(),
+                'votes': poll.get_votes_api_url(),
+            },
+            'url': {
+                'poster': self.user.get_absolute_url(),
+            },
+        })
 
 
         # no votes were removed
         # no votes were removed
-        self.assertEqual(response_json['votes'], 1)
         self.assertEqual(self.poll.pollvote_set.count(), 1)
         self.assertEqual(self.poll.pollvote_set.count(), 1)
 
 
     def test_moderate_user_poll(self):
     def test_moderate_user_poll(self):
@@ -499,7 +561,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         self.poll.length = 5
         self.poll.save()
         self.poll.save()
-
+        
         response = self.put(
         response = self.put(
             self.api_link,
             self.api_link,
             data={
             data={
@@ -523,24 +585,35 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = response.json()
-
-        self.assertEqual(response_json['poster_name'], self.user.username)
-        self.assertEqual(response_json['length'], 40)
-        self.assertEqual(response_json['question'], "Select two best colors")
-        self.assertEqual(response_json['allowed_choices'], 2)
-        self.assertTrue(response_json['allow_revotes'])
-
-        # you can't change poll's type after its posted
-        self.assertFalse(response_json['is_public'])
-
-        # choices were updated
-        self.assertEqual(len(response_json['choices']), 3)
-        self.assertEqual(len(set([c['hash'] for c in response_json['choices']])), 3)
-        self.assertEqual([c['label'] for c in response_json['choices']], ['Red', 'Green', 'Blue'])
-        self.assertEqual([c['votes'] for c in response_json['choices']], [0, 0, 0])
-        self.assertEqual([c['selected'] for c in response_json['choices']], [False, False, False])
-
+        poll = Poll.objects.all()[0]
+        add_acl(self.user, poll)
+
+        expected_choices = []
+        for choice in poll.choices:
+            self.assertIn(choice['label'], ["Red", "Green", "Blue"])
+            expected_choices.append(choice.copy())
+            expected_choices[-1]['selected'] = False
+
+        self.assertEqual(response.json(), {
+            'id': poll.id,
+            'poster_name': self.user.username,
+            'posted_on': serialize_datetime(poll.posted_on),
+            'length': 40,
+            'question': "Select two best colors",
+            'allowed_choices': 2,
+            'allow_revotes': True,
+            'votes': 0,
+            'is_public': False,
+            'acl': poll.acl,
+            'choices': expected_choices,
+            'api': {
+                'index': poll.get_api_url(),
+                'votes': poll.get_votes_api_url(),
+            },
+            'url': {
+                'poster': None,
+            },
+        })
+        
         # votes were removed
         # votes were removed
-        self.assertEqual(response_json['votes'], 0)
         self.assertEqual(self.poll.pollvote_set.count(), 0)
         self.assertEqual(self.poll.pollvote_set.count(), 0)

+ 335 - 72
misago/threads/tests/test_thread_pollvotes_api.py

@@ -4,6 +4,8 @@ from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
+from misago.acl import add_acl
+from misago.core.utils import serialize_datetime
 from misago.threads.models import Poll
 from misago.threads.models import Poll
 
 
 from .test_thread_poll_api import ThreadPollApiTestCase
 from .test_thread_poll_api import ThreadPollApiTestCase
@@ -29,12 +31,61 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
             }
             }
         )
         )
 
 
+    def get_votes_json(self):
+        choices_votes = {choice['hash']: [] for choice in self.poll.choices}
+        queryset = self.poll.pollvote_set.order_by('-id').select_related()
+        for vote in queryset:
+            if vote.voter:
+                url = vote.voter.get_absolute_url()
+            else:
+                url = None
+
+            choices_votes[vote.choice_hash].append({
+                'username': vote.voter_name,
+                'voted_on': serialize_datetime(vote.voted_on),
+                'url': url
+            })
+        return choices_votes
+
     def test_anonymous(self):
     def test_anonymous(self):
         """api allows guests to get poll votes"""
         """api allows guests to get poll votes"""
         self.logout_user()
         self.logout_user()
 
 
+        votes_json = self.get_votes_json()
+
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), [
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 1,
+                'voters': votes_json['aaaaaaaaaaaa'],
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 0,
+                'voters': [],
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 2,
+                'voters': votes_json['gggggggggggg'],
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 1,
+                'voters': votes_json['dddddddddddd'],
+            },
+        ])
+
+        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
+        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
+        self.assertEqual(len(votes_json['gggggggggggg']), 2)
+        self.assertEqual(len(votes_json['dddddddddddd']), 1)
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
@@ -48,6 +99,9 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
@@ -61,6 +115,9 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "No Thread matches the given query.",
+        })
 
 
     def test_invalid_poll_id(self):
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
         """api validates that poll id is integer"""
@@ -74,6 +131,9 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_nonexistant_poll_id(self):
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
         """api validates that poll exists"""
@@ -87,6 +147,9 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.json(), {
+            'detail': "NOT FOUND",
+        })
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api chcecks permission to see poll voters"""
         """api chcecks permission to see poll voters"""
@@ -97,6 +160,9 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You dont have permission to this poll's voters.",
+        })
 
 
     def test_nonpublic_poll(self):
     def test_nonpublic_poll(self):
         """api validates that poll is public"""
         """api validates that poll is public"""
@@ -107,26 +173,47 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You dont have permission to this poll's voters.",
+        })
 
 
     def test_get_votes(self):
     def test_get_votes(self):
         """api returns list of voters"""
         """api returns list of voters"""
+        votes_json = self.get_votes_json()
+
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json), 4)
-
-        self.assertEqual([c['label'] for c in response_json], ['Alpha', 'Beta', 'Gamma', 'Delta'])
-        self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
-        self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
-
-        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
-                         'bob')
-
-        user = UserModel.objects.get(slug='bob')
-
-        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
-                         user.get_absolute_url())
+        self.assertEqual(response.json(), [
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 1,
+                'voters': votes_json['aaaaaaaaaaaa'],
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 0,
+                'voters': [],
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 2,
+                'voters': votes_json['gggggggggggg'],
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 1,
+                'voters': votes_json['dddddddddddd'],
+            },
+        ])
+
+        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
+        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
+        self.assertEqual(len(votes_json['gggggggggggg']), 2)
+        self.assertEqual(len(votes_json['dddddddddddd']), 1)
 
 
     def test_get_votes_private_poll(self):
     def test_get_votes_private_poll(self):
         """api returns list of voters on private poll for user with permission"""
         """api returns list of voters on private poll for user with permission"""
@@ -135,23 +222,41 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         self.poll.is_public = False
         self.poll.is_public = False
         self.poll.save()
         self.poll.save()
 
 
+        votes_json = self.get_votes_json()
+
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json), 4)
-
-        self.assertEqual([c['label'] for c in response_json], ['Alpha', 'Beta', 'Gamma', 'Delta'])
-        self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
-        self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
-
-        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
-                         'bob')
-
-        user = UserModel.objects.get(slug='bob')
-
-        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
-                         user.get_absolute_url())
+        self.assertEqual(response.json(), [
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 1,
+                'voters': votes_json['aaaaaaaaaaaa'],
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 0,
+                'voters': [],
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 2,
+                'voters': votes_json['gggggggggggg'],
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 1,
+                'voters': votes_json['dddddddddddd'],
+            },
+        ])
+
+        self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
+        self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
+        self.assertEqual(len(votes_json['gggggggggggg']), 2)
+        self.assertEqual(len(votes_json['dddddddddddd']), 1)
 
 
 
 
 class ThreadPostVotesTests(ThreadPollApiTestCase):
 class ThreadPostVotesTests(ThreadPollApiTestCase):
@@ -182,6 +287,9 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This action is not available to guests.",
+        })
 
 
     def test_empty_vote_json(self):
     def test_empty_vote_json(self):
         """api validates if vote that user has made was empty"""
         """api validates if vote that user has made was empty"""
@@ -190,30 +298,48 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.client.post(
         response = self.client.post(
             self.api_link, '[]', content_type='application/json'
             self.api_link, '[]', content_type='application/json'
         )
         )
-        self.assertContains(response, "You have to make a choice.", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ["You have to make a choice."],
+        })
 
 
     def test_empty_vote_form(self):
     def test_empty_vote_form(self):
         """api validates if vote that user has made was empty"""
         """api validates if vote that user has made was empty"""
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You have to make a choice.", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ["You have to make a choice."],
+        })
 
 
     def test_malformed_vote(self):
     def test_malformed_vote(self):
         """api validates if vote that user has made was correctly structured"""
         """api validates if vote that user has made was correctly structured"""
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ['Expected a list of items but got type "dict".'],
+        })
 
 
         response = self.post(self.api_link, data={})
         response = self.post(self.api_link, data={})
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ['Expected a list of items but got type "dict".'],
+        })
 
 
         response = self.post(self.api_link, data='hello')
         response = self.post(self.api_link, data='hello')
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ['Expected a list of items but got type "str".'],
+        })
 
 
         response = self.post(self.api_link, data=123)
         response = self.post(self.api_link, data=123)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ['Expected a list of items but got type "int".'],
+        })
 
 
     def test_invalid_choices(self):
     def test_invalid_choices(self):
         """api validates if vote that user has made overlaps with allowed votes"""
         """api validates if vote that user has made overlaps with allowed votes"""
@@ -229,19 +355,23 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.poll.save()
         self.poll.save()
 
 
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
-        self.assertContains(
-            response, "This poll disallows voting for more than 1 choice.", status_code=400
-        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'choices': ["This poll disallows voting for more than 1 choice."],
+        })
 
 
     def test_revote(self):
     def test_revote(self):
         """api validates if user is trying to change vote in poll that disallows revoting"""
         """api validates if user is trying to change vote in poll that disallows revoting"""
         response = self.post(self.api_link, data=['lorem', 'ipsum'])
         response = self.post(self.api_link, data=['lorem', 'ipsum'])
-        self.assertContains(response, "You have already voted in this poll.", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have already voted in this poll.",
+        })
 
 
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
 
 
     def test_vote_in_closed_thread(self):
     def test_vote_in_closed_thread(self):
         """api validates is user has permission to vote poll in closed thread"""
         """api validates is user has permission to vote poll in closed thread"""
@@ -253,12 +383,15 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "thread is closed", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This thread is closed. You can't vote in it.",
+        })
 
 
         self.override_acl(category={'can_close_threads': 1})
         self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
 
 
     def test_vote_in_closed_category(self):
     def test_vote_in_closed_category(self):
         """api validates is user has permission to vote poll in closed category"""
         """api validates is user has permission to vote poll in closed category"""
@@ -270,12 +403,15 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "category is closed", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This category is closed. You can't vote in it.",
+        })
 
 
         self.override_acl(category={'can_close_threads': 1})
         self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
 
 
     def test_vote_in_finished_poll(self):
     def test_vote_in_finished_poll(self):
         """api valdiates if poll has finished before letting user to vote in it"""
         """api valdiates if poll has finished before letting user to vote in it"""
@@ -286,63 +422,190 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.delete_user_votes()
         self.delete_user_votes()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "This poll is over. You can't vote in it.", status_code=403)
-
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This poll is over. You can't vote in it.",
+        })
+        
         self.poll.length = 50
         self.poll.length = 50
         self.poll.save()
         self.poll.save()
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
-        self.assertContains(response, "Expected a list of items", status_code=400)
+        self.assertEqual(response.status_code, 400)
 
 
     def test_fresh_vote(self):
     def test_fresh_vote(self):
         """api handles first vote in poll"""
         """api handles first vote in poll"""
         self.delete_user_votes()
         self.delete_user_votes()
 
 
+        add_acl(self.user, self.poll)
+        self.poll.acl['can_vote'] = False
+
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'id': self.poll.id,
+            'poster_name': self.user.username,
+            'posted_on': serialize_datetime(self.poll.posted_on),
+            'length': 0,
+            'question': "Lorem ipsum dolor met?",
+            'allowed_choices': 2,
+            'allow_revotes': False,
+            'votes': 4,
+            'is_public': False,
+            'acl': self.poll.acl,
+            'choices': [
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'Alpha',
+                    'selected': True,
+                    'votes': 2
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Beta',
+                    'selected': True,
+                    'votes': 1
+                },
+                {
+                    'hash': 'gggggggggggg',
+                    'label': 'Gamma',
+                    'selected': False,
+                    'votes': 1
+                },
+                {
+                    'hash': 'dddddddddddd',
+                    'label': 'Delta',
+                    'selected': False,
+                    'votes': 0
+                },
+            ],
+            'api': {
+                'index': self.poll.get_api_url(),
+                'votes': self.poll.get_votes_api_url(),
+            },
+            'url': {
+                'poster': self.user.get_absolute_url(),
+            },
+        })
 
 
         # validate state change
         # validate state change
         poll = Poll.objects.get(pk=self.poll.pk)
         poll = Poll.objects.get(pk=self.poll.pk)
         self.assertEqual(poll.votes, 4)
         self.assertEqual(poll.votes, 4)
-        self.assertEqual([c['votes'] for c in poll.choices], [2, 1, 1, 0])
-
-        for choice in poll.choices:
-            self.assertNotIn('selected', choice)
+        self.assertEqual(poll.choices, [
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 2
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 1
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 1
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 0
+            },
+        ])
 
 
         self.assertEqual(poll.pollvote_set.count(), 4)
         self.assertEqual(poll.pollvote_set.count(), 4)
 
 
-        # validate response json
-        response_json = response.json()
-        self.assertEqual(response_json['votes'], 4)
-        self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
-        self.assertEqual([c['selected'] for c in response_json['choices']],
-                         [True, True, False, False])
-
-        self.assertFalse(response_json['acl']['can_vote'])
+        # validate poll disallows for revote
+        response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have already voted in this poll.",
+        })
 
 
     def test_vote_change(self):
     def test_vote_change(self):
         """api handles vote change"""
         """api handles vote change"""
         self.poll.allow_revotes = True
         self.poll.allow_revotes = True
         self.poll.save()
         self.poll.save()
 
 
+        add_acl(self.user, self.poll)
+
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'id': self.poll.id,
+            'poster_name': self.user.username,
+            'posted_on': serialize_datetime(self.poll.posted_on),
+            'length': 0,
+            'question': "Lorem ipsum dolor met?",
+            'allowed_choices': 2,
+            'allow_revotes': True,
+            'votes': 4,
+            'is_public': False,
+            'acl': self.poll.acl,
+            'choices': [
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'Alpha',
+                    'selected': True,
+                    'votes': 2
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Beta',
+                    'selected': True,
+                    'votes': 1
+                },
+                {
+                    'hash': 'gggggggggggg',
+                    'label': 'Gamma',
+                    'selected': False,
+                    'votes': 1
+                },
+                {
+                    'hash': 'dddddddddddd',
+                    'label': 'Delta',
+                    'selected': False,
+                    'votes': 0
+                },
+            ],
+            'api': {
+                'index': self.poll.get_api_url(),
+                'votes': self.poll.get_votes_api_url(),
+            },
+            'url': {
+                'poster': self.user.get_absolute_url(),
+            },
+        })
 
 
         # validate state change
         # validate state change
         poll = Poll.objects.get(pk=self.poll.pk)
         poll = Poll.objects.get(pk=self.poll.pk)
         self.assertEqual(poll.votes, 4)
         self.assertEqual(poll.votes, 4)
-        self.assertEqual([c['votes'] for c in poll.choices], [2, 1, 1, 0])
-
-        for choice in poll.choices:
-            self.assertNotIn('selected', choice)
+        self.assertEqual(poll.choices, [
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 2
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 1
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 1
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 0
+            },
+        ])
 
 
         self.assertEqual(poll.pollvote_set.count(), 4)
         self.assertEqual(poll.pollvote_set.count(), 4)
 
 
-        # validate response json
-        response_json = response.json()
-        self.assertEqual(response_json['votes'], 4)
-        self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
-        self.assertEqual([c['selected'] for c in response_json['choices']],
-                         [True, True, False, False])
-
-        self.assertTrue(response_json['acl']['can_vote'])
+        # validate poll allows for revote
+        response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
+        self.assertEqual(response.status_code, 200)