test_thread_bulkpatch_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import json
  2. from django.urls import reverse
  3. from .. import test
  4. from ...categories.models import Category
  5. from ..models import Thread
  6. from ..test import patch_category_acl, patch_other_category_acl
  7. from .test_threads_api import ThreadsApiTestCase
  8. class ThreadsBulkPatchApiTestCase(ThreadsApiTestCase):
  9. def setUp(self):
  10. super().setUp()
  11. self.threads = list(
  12. reversed(
  13. [
  14. test.post_thread(category=self.category),
  15. test.post_thread(category=self.category),
  16. test.post_thread(category=self.category),
  17. ]
  18. )
  19. )
  20. self.ids = list(reversed([t.id for t in self.threads]))
  21. self.api_link = reverse("misago:api:thread-list")
  22. def patch(self, api_link, ops):
  23. return self.client.patch(
  24. api_link, json.dumps(ops), content_type="application/json"
  25. )
  26. class BulkPatchSerializerTests(ThreadsBulkPatchApiTestCase):
  27. def test_invalid_input_type(self):
  28. """api rejects invalid input type"""
  29. response = self.patch(self.api_link, [1, 2, 3])
  30. self.assertEqual(response.status_code, 400)
  31. self.assertEqual(
  32. response.json(),
  33. {
  34. "non_field_errors": [
  35. "Invalid data. Expected a dictionary, but got list."
  36. ]
  37. },
  38. )
  39. def test_missing_input_keys(self):
  40. """api rejects input with missing keys"""
  41. response = self.patch(self.api_link, {})
  42. self.assertEqual(response.status_code, 400)
  43. self.assertEqual(
  44. response.json(),
  45. {"ids": ["This field is required."], "ops": ["This field is required."]},
  46. )
  47. def test_empty_input_keys(self):
  48. """api rejects input with empty keys"""
  49. response = self.patch(self.api_link, {"ids": [], "ops": []})
  50. self.assertEqual(response.status_code, 400)
  51. self.assertEqual(
  52. response.json(),
  53. {
  54. "ids": ["Ensure this field has at least 1 elements."],
  55. "ops": ["Ensure this field has at least 1 elements."],
  56. },
  57. )
  58. def test_invalid_input_keys(self):
  59. """api rejects input with invalid keys"""
  60. response = self.patch(self.api_link, {"ids": ["a"], "ops": [1]})
  61. self.assertEqual(response.status_code, 400)
  62. self.assertEqual(
  63. response.json(),
  64. {
  65. "ids": ["A valid integer is required."],
  66. "ops": ['Expected a dictionary of items but got type "int".'],
  67. },
  68. )
  69. def test_too_small_id(self):
  70. """api rejects input with implausiple id"""
  71. response = self.patch(self.api_link, {"ids": [0], "ops": [{}]})
  72. self.assertEqual(response.status_code, 400)
  73. self.assertEqual(
  74. response.json(),
  75. {"ids": ["Ensure this value is greater than or equal to 1."]},
  76. )
  77. def test_too_large_input(self):
  78. """api rejects too large input"""
  79. response = self.patch(
  80. self.api_link,
  81. {"ids": [i + 1 for i in range(200)], "ops": [{} for i in range(200)]},
  82. )
  83. self.assertEqual(response.status_code, 400)
  84. self.assertEqual(
  85. response.json(),
  86. {
  87. "ids": ["Ensure this field has no more than 25 elements."],
  88. "ops": ["Ensure this field has no more than 10 elements."],
  89. },
  90. )
  91. def test_threads_not_found(self):
  92. """api fails to find threads"""
  93. threads = [
  94. test.post_thread(category=self.category, is_hidden=True),
  95. test.post_thread(category=self.category, is_unapproved=True),
  96. ]
  97. response = self.patch(
  98. self.api_link, {"ids": [t.id for t in threads], "ops": [{}]}
  99. )
  100. self.assertEqual(response.status_code, 403)
  101. self.assertEqual(
  102. response.json(),
  103. {"detail": "One or more threads to update could not be found."},
  104. )
  105. def test_ops_invalid(self):
  106. """api validates descriptions"""
  107. response = self.patch(self.api_link, {"ids": self.ids[:1], "ops": [{}]})
  108. self.assertEqual(response.status_code, 400)
  109. self.assertEqual(
  110. response.json(), [{"id": self.ids[0], "detail": ["undefined op"]}]
  111. )
  112. def test_anonymous_user(self):
  113. """anonymous users can't use bulk actions"""
  114. self.logout_user()
  115. response = self.patch(self.api_link, {"ids": self.ids[:1], "ops": [{}]})
  116. self.assertEqual(response.status_code, 403)
  117. class ThreadAddAclApiTests(ThreadsBulkPatchApiTestCase):
  118. def test_add_acl_true(self):
  119. """api adds current threads acl to response"""
  120. response = self.patch(
  121. self.api_link,
  122. {"ids": self.ids, "ops": [{"op": "add", "path": "acl", "value": True}]},
  123. )
  124. self.assertEqual(response.status_code, 200)
  125. response_json = response.json()
  126. for i, thread in enumerate(self.threads):
  127. self.assertEqual(response_json[i]["id"], thread.id)
  128. self.assertTrue(response_json[i]["acl"])
  129. class BulkThreadChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
  130. @patch_category_acl({"can_edit_threads": 2})
  131. def test_change_thread_title(self):
  132. """api changes thread title and resyncs the category"""
  133. response = self.patch(
  134. self.api_link,
  135. {
  136. "ids": self.ids,
  137. "ops": [
  138. {"op": "replace", "path": "title", "value": "Changed the title!"}
  139. ],
  140. },
  141. )
  142. self.assertEqual(response.status_code, 200)
  143. response_json = response.json()
  144. for i, thread in enumerate(self.threads):
  145. self.assertEqual(response_json[i]["id"], thread.id)
  146. self.assertEqual(response_json[i]["title"], "Changed the title!")
  147. for thread in Thread.objects.filter(id__in=self.ids):
  148. self.assertEqual(thread.title, "Changed the title!")
  149. category = Category.objects.get(pk=self.category.id)
  150. self.assertEqual(category.last_thread_title, "Changed the title!")
  151. @patch_category_acl({"can_edit_threads": 0})
  152. def test_change_thread_title_no_permission(self):
  153. """api validates permission to change title, returns errors"""
  154. response = self.patch(
  155. self.api_link,
  156. {
  157. "ids": self.ids,
  158. "ops": [
  159. {"op": "replace", "path": "title", "value": "Changed the title!"}
  160. ],
  161. },
  162. )
  163. self.assertEqual(response.status_code, 400)
  164. response_json = response.json()
  165. for i, thread in enumerate(self.threads):
  166. self.assertEqual(response_json[i]["id"], thread.id)
  167. self.assertEqual(
  168. response_json[i]["detail"], ["You can't edit threads in this category."]
  169. )
  170. class BulkThreadMoveApiTests(ThreadsBulkPatchApiTestCase):
  171. def setUp(self):
  172. super().setUp()
  173. Category(name="Other Category", slug="other-category").insert_at(
  174. self.category, position="last-child", save=True
  175. )
  176. self.other_category = Category.objects.get(slug="other-category")
  177. @patch_category_acl({"can_move_threads": True})
  178. @patch_other_category_acl({"can_start_threads": 2})
  179. def test_move_thread(self):
  180. """api moves threads to other category and syncs both categories"""
  181. response = self.patch(
  182. self.api_link,
  183. {
  184. "ids": self.ids,
  185. "ops": [
  186. {
  187. "op": "replace",
  188. "path": "category",
  189. "value": self.other_category.id,
  190. },
  191. {"op": "replace", "path": "flatten-categories", "value": None},
  192. ],
  193. },
  194. )
  195. self.assertEqual(response.status_code, 200)
  196. response_json = response.json()
  197. for i, thread in enumerate(self.threads):
  198. self.assertEqual(response_json[i]["id"], thread.id)
  199. self.assertEqual(response_json[i]["category"], self.other_category.id)
  200. for thread in Thread.objects.filter(id__in=self.ids):
  201. self.assertEqual(thread.category_id, self.other_category.id)
  202. category = Category.objects.get(pk=self.category.id)
  203. self.assertEqual(category.threads, self.category.threads - 3)
  204. new_category = Category.objects.get(pk=self.other_category.id)
  205. self.assertEqual(new_category.threads, 3)
  206. class BulkThreadsHideApiTests(ThreadsBulkPatchApiTestCase):
  207. @patch_category_acl({"can_hide_threads": 1})
  208. def test_hide_thread(self):
  209. """api makes it possible to hide thread"""
  210. response = self.patch(
  211. self.api_link,
  212. {
  213. "ids": self.ids,
  214. "ops": [{"op": "replace", "path": "is-hidden", "value": True}],
  215. },
  216. )
  217. self.assertEqual(response.status_code, 200)
  218. response_json = response.json()
  219. for i, thread in enumerate(self.threads):
  220. self.assertEqual(response_json[i]["id"], thread.id)
  221. self.assertTrue(response_json[i]["is_hidden"])
  222. for thread in Thread.objects.filter(id__in=self.ids):
  223. self.assertTrue(thread.is_hidden)
  224. category = Category.objects.get(pk=self.category.id)
  225. self.assertNotIn(category.last_thread_id, self.ids)
  226. class BulkThreadsApproveApiTests(ThreadsBulkPatchApiTestCase):
  227. @patch_category_acl({"can_approve_content": True})
  228. def test_approve_thread(self):
  229. """api approvse threads and syncs category"""
  230. for thread in self.threads:
  231. thread.first_post.is_unapproved = True
  232. thread.first_post.save()
  233. thread.synchronize()
  234. thread.save()
  235. self.assertTrue(thread.is_unapproved)
  236. self.assertTrue(thread.has_unapproved_posts)
  237. self.category.synchronize()
  238. self.category.save()
  239. response = self.patch(
  240. self.api_link,
  241. {
  242. "ids": self.ids,
  243. "ops": [{"op": "replace", "path": "is-unapproved", "value": False}],
  244. },
  245. )
  246. self.assertEqual(response.status_code, 200)
  247. response_json = response.json()
  248. for i, thread in enumerate(self.threads):
  249. self.assertEqual(response_json[i]["id"], thread.id)
  250. self.assertFalse(response_json[i]["is_unapproved"])
  251. self.assertFalse(response_json[i]["has_unapproved_posts"])
  252. for thread in Thread.objects.filter(id__in=self.ids):
  253. self.assertFalse(thread.is_unapproved)
  254. self.assertFalse(thread.has_unapproved_posts)
  255. category = Category.objects.get(pk=self.category.id)
  256. self.assertIn(category.last_thread_id, self.ids)