test_thread_postbulkpatch_api.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import json
  2. from django.urls import reverse
  3. from .. import test
  4. from ...categories.models import Category
  5. from ...conf.test import override_dynamic_settings
  6. from ...users.test import AuthenticatedUserTestCase
  7. from ..models import Post, Thread
  8. from ..test import patch_category_acl
  9. class ThreadPostBulkPatchApiTestCase(AuthenticatedUserTestCase):
  10. def setUp(self):
  11. super().setUp()
  12. self.category = Category.objects.get(slug="first-category")
  13. self.thread = test.post_thread(category=self.category)
  14. self.posts = [
  15. test.reply_thread(self.thread, poster=self.user),
  16. test.reply_thread(self.thread),
  17. test.reply_thread(self.thread, poster=self.user),
  18. ]
  19. self.ids = [p.id for p in self.posts]
  20. self.api_link = reverse(
  21. "misago:api:thread-post-list", kwargs={"thread_pk": self.thread.pk}
  22. )
  23. def patch(self, api_link, ops):
  24. return self.client.patch(
  25. api_link, json.dumps(ops), content_type="application/json"
  26. )
  27. class BulkPatchSerializerTests(ThreadPostBulkPatchApiTestCase):
  28. def test_invalid_input_type(self):
  29. """api rejects invalid input type"""
  30. response = self.patch(self.api_link, [1, 2, 3])
  31. self.assertEqual(response.status_code, 400)
  32. self.assertEqual(
  33. response.json(),
  34. {
  35. "non_field_errors": [
  36. "Invalid data. Expected a dictionary, but got list."
  37. ]
  38. },
  39. )
  40. def test_missing_input_keys(self):
  41. """api rejects input with missing keys"""
  42. response = self.patch(self.api_link, {})
  43. self.assertEqual(response.status_code, 400)
  44. self.assertEqual(
  45. response.json(),
  46. {"ids": ["This field is required."], "ops": ["This field is required."]},
  47. )
  48. def test_empty_input_keys(self):
  49. """api rejects input with empty keys"""
  50. response = self.patch(self.api_link, {"ids": [], "ops": []})
  51. self.assertEqual(response.status_code, 400)
  52. self.assertEqual(
  53. response.json(),
  54. {
  55. "ids": ["Ensure this field has at least 1 elements."],
  56. "ops": ["Ensure this field has at least 1 elements."],
  57. },
  58. )
  59. def test_invalid_input_keys(self):
  60. """api rejects input with invalid keys"""
  61. response = self.patch(self.api_link, {"ids": ["a"], "ops": [1]})
  62. self.assertEqual(response.status_code, 400)
  63. self.assertEqual(
  64. response.json(),
  65. {
  66. "ids": {"0": ["A valid integer is required."]},
  67. "ops": {"0": ['Expected a dictionary of items but got type "int".']},
  68. },
  69. )
  70. def test_too_small_id(self):
  71. """api rejects input with implausiple id"""
  72. response = self.patch(self.api_link, {"ids": [0], "ops": [{}]})
  73. self.assertEqual(response.status_code, 400)
  74. self.assertEqual(
  75. response.json(),
  76. {"ids": {"0": ["Ensure this value is greater than or equal to 1."]}},
  77. )
  78. @override_dynamic_settings(posts_per_page=4, posts_per_page_orphans=3)
  79. def test_too_large_input(self):
  80. """api rejects too large input"""
  81. response = self.patch(
  82. self.api_link,
  83. {"ids": [i + 1 for i in range(8)], "ops": [{} for i in range(200)]},
  84. )
  85. self.assertEqual(response.status_code, 400)
  86. self.assertEqual(
  87. response.json(),
  88. {
  89. "ids": ["No more than 7 posts can be updated at a single time."],
  90. "ops": ["Ensure this field has no more than 10 elements."],
  91. },
  92. )
  93. def test_posts_not_found(self):
  94. """api fails to find posts"""
  95. posts = [
  96. test.reply_thread(self.thread, is_hidden=True),
  97. test.reply_thread(self.thread, is_unapproved=True),
  98. ]
  99. response = self.patch(
  100. self.api_link, {"ids": [p.id for p in posts], "ops": [{}]}
  101. )
  102. self.assertEqual(response.status_code, 403)
  103. self.assertEqual(
  104. response.json(),
  105. {"detail": "One or more posts to update could not be found."},
  106. )
  107. def test_ops_invalid(self):
  108. """api validates descriptions"""
  109. response = self.patch(self.api_link, {"ids": self.ids[:1], "ops": [{}]})
  110. self.assertEqual(response.status_code, 400)
  111. self.assertEqual(
  112. response.json(), [{"id": self.ids[0], "detail": ["undefined op"]}]
  113. )
  114. def test_anonymous_user(self):
  115. """anonymous users can't use bulk actions"""
  116. self.logout_user()
  117. response = self.patch(self.api_link, {"ids": self.ids[:1], "ops": [{}]})
  118. self.assertEqual(response.status_code, 403)
  119. def test_events(self):
  120. """cant use bulk actions for events"""
  121. for post in self.posts:
  122. post.is_event = True
  123. post.save()
  124. response = self.patch(self.api_link, {"ids": self.ids, "ops": [{}]})
  125. self.assertEqual(response.status_code, 403)
  126. self.assertEqual(
  127. response.json(),
  128. {"detail": "One or more posts to update could not be found."},
  129. )
  130. class PostsAddAclApiTests(ThreadPostBulkPatchApiTestCase):
  131. def test_add_acl_true(self):
  132. """api adds posts acls to response"""
  133. response = self.patch(
  134. self.api_link,
  135. {"ids": self.ids, "ops": [{"op": "add", "path": "acl", "value": True}]},
  136. )
  137. self.assertEqual(response.status_code, 200)
  138. response_json = response.json()
  139. for i, post in enumerate(self.posts):
  140. self.assertEqual(response_json[i]["id"], post.id)
  141. self.assertTrue(response_json[i]["acl"])
  142. class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase):
  143. @patch_category_acl({"can_protect_posts": True, "can_edit_posts": 2})
  144. def test_protect_post(self):
  145. """api makes it possible to protect posts"""
  146. response = self.patch(
  147. self.api_link,
  148. {
  149. "ids": self.ids,
  150. "ops": [{"op": "replace", "path": "is-protected", "value": True}],
  151. },
  152. )
  153. self.assertEqual(response.status_code, 200)
  154. response_json = response.json()
  155. for i, post in enumerate(self.posts):
  156. self.assertEqual(response_json[i]["id"], post.id)
  157. self.assertTrue(response_json[i]["is_protected"])
  158. for post in Post.objects.filter(id__in=self.ids):
  159. self.assertTrue(post.is_protected)
  160. @patch_category_acl({"can_protect_posts": False})
  161. def test_protect_post_no_permission(self):
  162. """api validates permission to protect posts and returns errors"""
  163. response = self.patch(
  164. self.api_link,
  165. {
  166. "ids": self.ids,
  167. "ops": [{"op": "replace", "path": "is-protected", "value": True}],
  168. },
  169. )
  170. self.assertEqual(response.status_code, 400)
  171. response_json = response.json()
  172. for i, post in enumerate(self.posts):
  173. self.assertEqual(response_json[i]["id"], post.id)
  174. self.assertEqual(
  175. response_json[i]["detail"],
  176. ["You can't protect posts in this category."],
  177. )
  178. for post in Post.objects.filter(id__in=self.ids):
  179. self.assertFalse(post.is_protected)
  180. class BulkPostsApproveApiTests(ThreadPostBulkPatchApiTestCase):
  181. @patch_category_acl({"can_approve_content": True})
  182. def test_approve_post(self):
  183. """api resyncs thread and categories on posts approval"""
  184. for post in self.posts:
  185. post.is_unapproved = True
  186. post.save()
  187. self.thread.synchronize()
  188. self.thread.save()
  189. self.assertNotIn(self.thread.last_post_id, self.ids)
  190. response = self.patch(
  191. self.api_link,
  192. {
  193. "ids": self.ids,
  194. "ops": [{"op": "replace", "path": "is-unapproved", "value": False}],
  195. },
  196. )
  197. self.assertEqual(response.status_code, 200)
  198. response_json = response.json()
  199. for i, post in enumerate(self.posts):
  200. self.assertEqual(response_json[i]["id"], post.id)
  201. self.assertFalse(response_json[i]["is_unapproved"])
  202. for post in Post.objects.filter(id__in=self.ids):
  203. self.assertFalse(post.is_unapproved)
  204. thread = Thread.objects.get(pk=self.thread.pk)
  205. self.assertIn(thread.last_post_id, self.ids)
  206. category = Category.objects.get(pk=self.category.pk)
  207. self.assertEqual(category.posts, 4)