Просмотр исходного кода

WIP Improve API, Username Change

Rafał Pitoń 7 лет назад
Родитель
Сommit
4badbca034

+ 4 - 1
misago/search/tests/test_api.py

@@ -18,7 +18,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.test_link)
 
-        self.assertContains(response, "have permission to search site", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You don't have permission to search site."
+        })
 
     def test_no_phrase(self):
         """api handles no search query"""

+ 14 - 7
misago/threads/tests/test_privatethread_start_api.py

@@ -38,19 +38,24 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You can't use private threads.", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't use private threads.",
+        })
 
     def test_cant_start_private_thread(self):
         """permission to start private thread is validated"""
         override_acl(self.user, {'can_start_private_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You can't start private threads.", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't start private threads.",
+        })
 
     def test_empty_data(self):
         """no data sent handling has no showstoppers"""
         response = self.client.post(self.api_link, data={})
-
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
@@ -320,11 +325,13 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             }
         )
         self.assertEqual(response.status_code, 200)
-
+        
         thread = self.user.thread_set.all()[:1][0]
-
-        response_json = response.json()
-        self.assertEqual(response_json['url'], thread.get_absolute_url())
+        self.assertEqual(response.json(), {
+            'id': thread.pk,
+            'title': thread.title,
+            'url': thread.get_absolute_url(),
+        })
 
         response = self.client.get(thread.get_absolute_url())
         self.assertContains(response, self.category.name)

+ 23 - 15
misago/threads/tests/test_privatethreads_api.py

@@ -18,22 +18,26 @@ class PrivateThreadsListApiTests(PrivateThreadsTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertContains(response, "sign in to use private threads", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have to sign in to use private threads.",
+        })
 
     def test_no_permission(self):
         """api requires user to have permission to be able to access it"""
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertContains(response, "can't use private threads", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't use private threads.",
+        })
 
     def test_empty_list(self):
         """api has no showstoppers on returning empty list"""
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(response_json['count'], 0)
+        self.assertEqual(response.json()['results'], [])
 
     def test_thread_visibility(self):
         """only participated threads are returned by private threads api"""
@@ -79,14 +83,20 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
         self.logout_user()
 
         response = self.client.get(self.api_link)
-        self.assertContains(response, "sign in to use private threads", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You have to sign in to use private threads.",
+        })
 
     def test_no_permission(self):
         """user needs to have permission to see private thread"""
         override_acl(self.user, {'can_use_private_threads': 0})
 
         response = self.client.get(self.api_link)
-        self.assertContains(response, "t use private threads", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't use private threads.",
+        })
 
     def test_no_participant(self):
         """user cant see thread he isn't part of"""
@@ -180,19 +190,17 @@ class PrivateThreadDeleteApiTests(PrivateThreadsTestCase):
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
-
-        self.assertEqual(
-            response.json()['detail'], "You can't delete threads in this category."
-        )
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete threads in this category.",
+        })
 
         self.override_acl({'can_hide_threads': 1})
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
-
-        self.assertEqual(
-            response.json()['detail'], "You can't delete threads in this category."
-        )
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete threads in this category.",
+        })
 
     def test_delete_thread(self):
         """DELETE to API link with permission deletes thread"""

+ 46 - 26
misago/threads/tests/test_thread_editreply_api.py

@@ -75,7 +75,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.override_acl({'can_edit_posts': 0})
 
         response = self.put(self.api_link)
-        self.assertContains(response, "You can't edit posts in this category.", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit posts in this category.",
+        })
 
     def test_cant_edit_other_user_reply(self):
         """permission to edit reply by other users is validated"""
@@ -85,9 +88,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "You can't edit other users posts in this category.", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit other users posts in this category.",
+        })
 
     def test_edit_too_old(self):
         """permission to edit reply within timelimit is validated"""
@@ -100,9 +104,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "You can't edit posts that are older than 1 minute.", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't edit posts that are older than 1 minute.",
+        })
 
     def test_closed_category(self):
         """permssion to edit reply in closed category is validated"""
@@ -112,15 +117,19 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.category.save()
 
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "This category is closed. You can't edit posts in it.", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This category is closed. You can't edit posts in it.",
+        })
 
         # allow to post in closed category
         self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'post': ['You have to enter a message.'],
+        })
 
     def test_closed_thread(self):
         """permssion to edit reply in closed thread is validated"""
@@ -130,15 +139,19 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.thread.save()
 
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "This thread is closed. You can't edit posts in it.", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This thread is closed. You can't edit posts in it.",
+        })
 
         # allow to post in closed thread
         self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'post': ['You have to enter a message.'],
+        })
 
     def test_protected_post(self):
         """permssion to edit protected post is validated"""
@@ -148,35 +161,40 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link)
-        self.assertContains(
-            response, "This post is protected. You can't edit it.", status_code=403
-        )
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This post is protected. You can't edit it.",
+        })
 
         # allow to post in closed thread
         self.override_acl({'can_protect_posts': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'post': ['You have to enter a message.'],
+        })
 
     def test_empty_data(self):
         """no data sent handling has no showstoppers"""
         self.override_acl()
 
         response = self.put(self.api_link, data={})
-
-        self.assertContains(response, "You have to enter a message.", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'post': ['You have to enter a message.'],
+        })
 
     def test_invalid_data(self):
         """api errors for invalid request data"""
         self.override_acl()
 
-        response = self.client.put(
-            self.api_link,
-            'false',
-            content_type="application/json",
-        )
+        response = self.client.put(self.api_link,'false', content_type="application/json")
 
-        self.assertContains(response, "Invalid data.", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'non_field_errors': ['Invalid data. Expected a dictionary, but got bool.'],
+        })
 
     def test_edit_event(self):
         """events can't be edited"""
@@ -186,8 +204,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.post.save()
 
         response = self.put(self.api_link, data={})
-
-        self.assertContains(response, "Events can't be edited.", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "Events can't be edited.",
+        })
 
     def test_post_is_validated(self):
         """post is validated"""

+ 43 - 20
misago/threads/tests/test_thread_merge_api.py

@@ -67,7 +67,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.override_acl({'can_merge_threads': 0})
 
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You can't merge threads in this category.", status_code=403)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'detail': "You can't merge threads in this category.",
+        })
         
     def test_merge_no_url(self):
         """api validates if thread url was given"""
@@ -75,7 +78,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'other_thread': ["Enter link to new thread."]})
+        self.assertEqual(response.json(), {
+            'other_thread': ["Enter link to new thread."],
+        })
 
     def test_invalid_url(self):
         """api validates thread url"""
@@ -87,7 +92,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'other_thread': ["This is not a valid thread link."]})
+        self.assertEqual(response.json(), {
+            'other_thread': ["This is not a valid thread link."],
+        })
 
     def test_current_other_thread(self):
         """api validates if thread url given is to current thread"""
@@ -99,9 +106,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json(), {'other_thread': ["You can't merge thread with itself."]}
-        )
+        self.assertEqual(response.json(), {
+            'other_thread': ["You can't merge thread with itself."],
+        })
 
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
@@ -119,10 +126,12 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {'other_thread': [
-                "The thread you have entered link to doesn't exist or "
-                "you don't have permission to see it."
-            ]},
+            {
+                'other_thread': )
+                    "The thread you have entered link to doesn't exist or "
+                    "you don't have permission to see it."
+                ],
+            }
         )
 
     def test_other_thread_is_invisible(self):
@@ -141,10 +150,12 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {'other_thread': [
-                "The thread you have entered link to doesn't exist or "
-                "you don't have permission to see it."
-            ]},
+            {
+                'other_thread': [
+                    "The thread you have entered link to doesn't exist or "
+                    "you don't have permission to see it."
+                ],
+            },
         )
 
     def test_other_thread_isnt_mergeable(self):
@@ -162,7 +173,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'other_thread': ["Other thread can't be merged with."]}
+            response.json(), {
+                'other_thread': ["Other thread can't be merged with."],
+            }
         )
 
     def test_thread_category_is_closed(self):
@@ -187,7 +200,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(
-            response.json(), {'detail': "This category is closed. You can't merge it's threads."},
+            response.json(), {
+                'detail': "This category is closed. You can't merge it's threads.",
+            }
         )
 
     def test_thread_is_closed(self):
@@ -213,7 +228,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 403)
         self.assertEqual(
             response.json(),
-            {'detail': "This thread is closed. You can't merge it with other threads."},
+            {
+                'detail': "This thread is closed. You can't merge it with other threads.",
+            },
         )
 
     def test_other_thread_category_is_closed(self):
@@ -239,7 +256,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {'other_thread': ["Other thread's category is closed. You can't merge with it."]},
+            {
+                'other_thread': ["Other thread's category is closed. You can't merge with it."],
+            },
         )
 
     def test_other_thread_is_closed(self):
@@ -265,7 +284,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {'other_thread': ["Other thread is closed and can't be merged with."]},
+            {
+                'other_thread': ["Other thread is closed and can't be merged with."],
+            }
         )
 
     def test_other_thread_isnt_replyable(self):
@@ -287,7 +308,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {'other_thread': ["You can't merge this thread into thread you can't reply."]},
+            {
+                'other_thread': ["You can't merge this thread into thread you can't reply."],
+            }
         )
 
     def test_merge_threads(self):

+ 12 - 8
misago/users/api/userendpoints/username.py

@@ -19,25 +19,29 @@ def get_username_options(user):
     options = UsernameChanges(user)
     return {
         'changes_left': options.left,
-        'next_on': options.next_on,
+        'next_change_on': options.next_change_on,
         'length_min': settings.username_length_min,
         'length_max': settings.username_length_max,
     }
 
 
 def options_response(options):
-    if options['next_on']:
-        options['next_on'] = options['next_on'].isoformat()
+    if options['next_change_on']:
+        options['next_change_on'] = options['next_change_on'].isoformat()
     return Response(options)
 
 
 def change_username(request):
     options = get_username_options(request.user)
     if not options['changes_left']:
+        if options['next_change_on']:
+            next_change_on = options['next_change_on'].isoformat()
+        else:
+            next_change_on = None
         return Response(
             {
-                'detail': _("You can't change your username now."),
-                'options': options,
+                'username': [_("You can't change your username at this time.")],
+                'next_change_on': next_change_on,
             },
             status=400,
         )
@@ -60,7 +64,7 @@ def change_username(request):
     except IntegrityError:
         return Response(
             {
-                'detail': _("Error changing username. Please try again."),
+                'username': [_("Please try again.")],
             },
             status=400,
         )
@@ -71,7 +75,7 @@ def moderate_username_endpoint(request, profile):
         serializer = ChangeUsernameSerializer(data=request.data, context={'user': profile})
 
         if not serializer.is_valid():
-            return Response({'detail': serializer.errors}, status=400)
+            return Response(serializer.errors, status=400)
 
         try:
             serializer.change_username(changed_by=request.user)
@@ -82,7 +86,7 @@ def moderate_username_endpoint(request, profile):
         except IntegrityError:
             return Response(
                 {
-                    'detail': _("Error changing username. Please try again."),
+                    'username': [_("Please try again.")],
                 },
                 status=400,
             )

+ 3 - 3
misago/users/namechanges.py

@@ -11,7 +11,7 @@ from .models import UsernameChange
 class UsernameChanges(object):
     def __init__(self, user):
         self.left = 0
-        self.next_on = None
+        self.next_change_on = None
 
         if user.acl_cache['name_changes_allowed']:
             self.count_namechanges(user)
@@ -33,7 +33,7 @@ class UsernameChanges(object):
 
         if not self.left and name_changes_expire:
             try:
-                self.next_on = valid_changes_qs.latest().changed_on
-                self.next_on += timedelta(days=name_changes_expire)
+                self.next_change_on = valid_changes_qs.latest().changed_on
+                self.next_change_on += timedelta(days=name_changes_expire)
             except UsernameChange.DoesNotExist:
                 pass

+ 3 - 10
misago/users/serializers/options.py

@@ -44,20 +44,13 @@ class EditSignatureSerializer(serializers.ModelSerializer):
 
 
 class ChangeUsernameSerializer(serializers.Serializer):
-    username = serializers.CharField(max_length=200, required=False, allow_blank=True)
-
-    def validate(self, data):
-        username = data.get('username')
-
-        if not username:
-            raise serializers.ValidationError(_("Enter new username."))
+    username = serializers.CharField(max_length=200, required=True, allow_blank=False)
 
+    def validate_username(self, username):
         if username == self.context['user'].username:
             raise serializers.ValidationError(_("New username is same as current one."))
-
         validate_username(username)
-
-        return data
+        return username
 
     def change_username(self, changed_by):
         self.context['user'].set_username(self.validated_data['username'], changed_by=changed_by)

+ 91 - 48
misago/users/tests/test_user_username_api.py

@@ -19,25 +19,24 @@ class UserUsernameTests(AuthenticatedUserTestCase):
 
     def test_get_change_username_options(self):
         """get to API returns options"""
+
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'changes_left': self.user.acl_cache['name_changes_allowed'],
+            'next_change_on': None,
+            'length_min': settings.username_length_min,
+            'length_max': settings.username_length_max,
+        })
 
-        response_json = response.json()
-
-        self.assertIsNotNone(response_json['changes_left'])
-        self.assertEqual(response_json['length_min'], settings.username_length_min)
-        self.assertEqual(response_json['length_max'], settings.username_length_max)
-        self.assertIsNone(response_json['next_on'])
-
-        for i in range(response_json['changes_left']):
+        for i in range(response.json()['changes_left']):
             self.user.set_username('NewName%s' % i, self.user)
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = response.json()
-        self.assertEqual(response_json['changes_left'], 0)
-        self.assertIsNotNone(response_json['next_on'])
+        self.assertEqual(response.json()['changes_left'], 0)
+        self.assertIsNotNone(response.json()['next_change_on'])
 
     def test_change_username_no_changes_left(self):
         """api returns error 400 if there are no username changes left"""
@@ -55,18 +54,31 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.post(
             self.link,
             data={
-                'username': 'Pointless',
+                'username': 'FailedChanges',
             },
         )
-
-        self.assertContains(response, 'change your username now', status_code=400)
-        self.assertTrue(self.user.username != 'Pointless')
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["You can't change your username at this time."],
+            'next_change_on': response.json()['next_change_on'],
+        })
+        
+        self.reload_user()
+        self.assertTrue(self.user.username != 'FailedChanges')
 
     def test_change_username_no_input(self):
-        """api returns error 400 if new username is empty"""
+        """api returns error 400 if new username is omitted or empty"""
         response = self.client.post(self.link, data={})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["This field is required."],
+        })
 
-        self.assertContains(response, 'Enter new username.', status_code=400)
+        response = self.client.post(self.link, data={'username': ''})
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["This field may not be blank."],
+        })
 
     def test_change_username_invalid_name(self):
         """api returns error 400 if new username is wrong"""
@@ -76,14 +88,13 @@ class UserUsernameTests(AuthenticatedUserTestCase):
                 'username': '####',
             },
         )
-
-        self.assertContains(response, 'can only contain latin', status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["Username can only contain latin alphabet letters and digits."],
+        })
 
     def test_change_username(self):
         """api changes username and records change"""
-        response = self.client.get(self.link)
-        changes_left = response.json()['changes_left']
-
         old_username = self.user.username
         new_username = 'NewUsernamu'
 
@@ -93,14 +104,21 @@ class UserUsernameTests(AuthenticatedUserTestCase):
                 'username': new_username,
             },
         )
-
         self.assertEqual(response.status_code, 200)
-        options = response.json()['options']
-        self.assertEqual(changes_left, options['changes_left'] + 1)
+        self.assertEqual(response.json(), {
+            'username': 'NewUsernamu',
+            'slug': 'newusernamu',
+            'options': {
+                'changes_left': 1,
+                'next_change_on': None,
+                'length_min': settings.username_length_min,
+                'length_max': settings.username_length_max,
+            },
+        })
 
         self.reload_user()
         self.assertEqual(self.user.username, new_username)
-        self.assertTrue(self.user.username != old_username)
+        self.assertNotEqual(self.user.username, old_username)
 
         self.assertEqual(self.user.namechanges.last().new_username, new_username)
 
@@ -122,27 +140,38 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertContains(response, "can't rename users", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't rename users."
+        })
 
         override_acl(self.user, {
             'can_rename_users': 0,
         })
 
         response = self.client.post(self.link)
-        self.assertContains(response, "can't rename users", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't rename users."
+        })
 
-    def test_moderate_username(self):
-        """moderate username"""
+    def test_invalid_username(self):
+        """moderate username api validates username"""
         override_acl(self.user, {
             'can_rename_users': 1,
         })
 
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-
-        options = response.json()
-        self.assertEqual(options['length_min'], settings.username_length_min)
-        self.assertEqual(options['length_max'], settings.username_length_max)
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'username': None,
+            }),
+            content_type='application/json',
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["This field may not be null."],
+        })
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -155,8 +184,10 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-
-        self.assertContains(response, "Enter new username", status_code=400)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["This field may not be blank."],
+        })
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -169,12 +200,10 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-
-        self.assertContains(
-            response,
-            "Username can only contain latin alphabet letters and digits.",
-            status_code=400
-        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'username': ["Username can only contain latin alphabet letters and digits."],
+        })
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -187,12 +216,26 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type='application/json',
         )
-
         self.assertEqual(response.status_code, 400)
-        self.assertContains(
-            response, "Username must be at least 3 characters long.", status_code=400
-        )
+        self.assertEqual(response.json(), {
+            'username': ["Username must be at least 3 characters long."],
+        })
 
+    def test_get_username_requirements(self):
+        """get to API returns username requirements"""
+        override_acl(self.user, {
+            'can_rename_users': 1,
+        })
+
+        response = self.client.get(self.link)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'length_min': settings.username_length_min,
+            'length_max': settings.username_length_max,
+        })
+
+    def test_moderate_username(self):
+        """moderate username"""
         override_acl(self.user, {
             'can_rename_users': 1,
         })

+ 11 - 5
misago/users/tests/test_usernamechanges_api.py

@@ -42,21 +42,27 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
         override_acl(self.user, {'can_see_users_name_history': False})
 
         response = self.client.get('%s?user=%s&search=new' % (self.link, self.user.pk))
-
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, self.user.username)
 
-        response = self.client.get('%s?user=%s&search=usernew' % (self.link, self.user.pk))
+        override_acl(self.user, {'can_see_users_name_history': False})
 
+        response = self.client.get('%s?user=%s&search=usernew' % (self.link, self.user.pk))
         self.assertEqual(response.status_code, 200)
-        self.assertContains(response, '[]')
+        self.assertEqual(response.json()['results'], [])
 
     def test_list_denies_permission(self):
         """list denies permission for other user (or all) if no access"""
         override_acl(self.user, {'can_see_users_name_history': False})
 
         response = self.client.get('%s?user=%s' % (self.link, self.user.pk + 1))
-        self.assertContains(response, "don't have permission to", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You don't have permission to see other users name history.",
+        })
 
         response = self.client.get(self.link)
-        self.assertContains(response, "don't have permission to", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You don't have permission to see other users name history.",
+        })

+ 46 - 17
misago/users/tests/test_users_api.py

@@ -407,12 +407,18 @@ class UserFollowTests(AuthenticatedUserTestCase):
         self.logout_user()
 
         response = self.client.post(self.link)
-        self.assertContains(response, "action is not available to guests", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "This action is not available to guests.",
+        })
 
     def test_follow_myself(self):
         """you can't follow yourself"""
         response = self.client.post('/api/users/%s/follow/' % self.user.pk)
-        self.assertContains(response, "can't add yourself to followed", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't add yourself to followed.",
+        })
 
     def test_cant_follow(self):
         """no permission to follow users"""
@@ -421,7 +427,10 @@ class UserFollowTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.post(self.link)
-        self.assertContains(response, "can't follow other users", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't follow other users.",
+        })
 
     def test_follow(self):
         """follow and unfollow other user"""
@@ -471,7 +480,10 @@ class UserBanTests(AuthenticatedUserTestCase):
         override_acl(self.user, {'can_see_ban_details': 0})
 
         response = self.client.get(self.link)
-        self.assertContains(response, "can't see users bans details", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't see users bans details.",
+        })
 
     def test_no_ban(self):
         """api returns empty json"""
@@ -479,7 +491,7 @@ class UserBanTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(smart_str(response.content), '{}')
+        self.assertEqual(response.json(), {})
 
     def test_ban_details(self):
         """api returns ban json"""
@@ -493,10 +505,14 @@ class UserBanTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-
-        ban_json = response.json()
-        self.assertEqual(ban_json['user_message']['plain'], 'Nope!')
-        self.assertEqual(ban_json['user_message']['html'], '<p>Nope!</p>')
+        self.assertEqual(response.json(), {
+            'user_message': {
+                'plain': 'Nope!',
+                'html': '<p>Nope!</p>',
+            },
+            'staff_message': None,
+            'expires_on': None,
+        })
 
 
 class UserDeleteTests(AuthenticatedUserTestCase):
@@ -530,7 +546,9 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertContains(response, "can't delete users", status_code=403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete users.",
+        })
 
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
@@ -546,8 +564,9 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertContains(response, "can't delete users", status_code=403)
-        self.assertContains(response, "made more than 5 posts", status_code=403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete users that made more than 5 posts.",
+        })
 
     def test_delete_too_old_member(self):
         """raises 403 error when user is too old"""
@@ -563,8 +582,9 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertContains(response, "can't delete users", status_code=403)
-        self.assertContains(response, "members for more than 5 days", status_code=403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete users that are members for more than 5 days.",
+        })
 
     def test_delete_self(self):
         """raises 403 error when attempting to delete oneself"""
@@ -576,7 +596,10 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         )
 
         response = self.client.post('/api/users/%s/delete/' % self.user.pk)
-        self.assertContains(response, "can't delete yourself", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete yourself.",
+        })
 
     def test_delete_admin(self):
         """raises 403 error when attempting to delete admin"""
@@ -591,7 +614,10 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertContains(response, "can't delete administrators", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete administrators.",
+        })
 
     def test_delete_superadmin(self):
         """raises 403 error when attempting to delete superadmin"""
@@ -606,7 +632,10 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertContains(response, "can't delete administrators", status_code=403)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete administrators.",
+        })
 
     def test_delete_with_content(self):
         """returns 200 and deletes user with content"""