test_thread_bulkpatch_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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 ..models import Thread
  7. from ..test import patch_category_acl, patch_other_category_acl
  8. from .test_threads_api import ThreadsApiTestCase
  9. class ThreadsBulkPatchApiTestCase(ThreadsApiTestCase):
  10. def setUp(self):
  11. super().setUp()
  12. self.threads = list(
  13. reversed(
  14. [
  15. test.post_thread(category=self.category),
  16. test.post_thread(category=self.category),
  17. test.post_thread(category=self.category),
  18. ]
  19. )
  20. )
  21. self.ids = list(reversed([t.id for t in self.threads]))
  22. self.api_link = reverse("misago:api:thread-list")
  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(ThreadsBulkPatchApiTestCase):
  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(threads_per_page=5)
  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(6)], "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 5 threads can be updated at a single time."],
  90. "ops": ["Ensure this field has no more than 10 elements."],
  91. },
  92. )
  93. def test_threads_not_found(self):
  94. """api fails to find threads"""
  95. threads = [
  96. test.post_thread(category=self.category, is_hidden=True),
  97. test.post_thread(category=self.category, is_unapproved=True),
  98. ]
  99. response = self.patch(
  100. self.api_link, {"ids": [t.id for t in threads], "ops": [{}]}
  101. )
  102. self.assertEqual(response.status_code, 403)
  103. self.assertEqual(
  104. response.json(),
  105. {"detail": "One or more threads 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. class ThreadAddAclApiTests(ThreadsBulkPatchApiTestCase):
  120. def test_add_acl_true(self):
  121. """api adds current threads acl to response"""
  122. response = self.patch(
  123. self.api_link,
  124. {"ids": self.ids, "ops": [{"op": "add", "path": "acl", "value": True}]},
  125. )
  126. self.assertEqual(response.status_code, 200)
  127. response_json = response.json()
  128. for i, thread in enumerate(self.threads):
  129. self.assertEqual(response_json[i]["id"], thread.id)
  130. self.assertTrue(response_json[i]["acl"])
  131. class BulkThreadChangeTitleApiTests(ThreadsBulkPatchApiTestCase):
  132. @patch_category_acl({"can_edit_threads": 2})
  133. def test_change_thread_title(self):
  134. """api changes thread title and resyncs the category"""
  135. response = self.patch(
  136. self.api_link,
  137. {
  138. "ids": self.ids,
  139. "ops": [
  140. {"op": "replace", "path": "title", "value": "Changed the title!"}
  141. ],
  142. },
  143. )
  144. self.assertEqual(response.status_code, 200)
  145. response_json = response.json()
  146. for i, thread in enumerate(self.threads):
  147. self.assertEqual(response_json[i]["id"], thread.id)
  148. self.assertEqual(response_json[i]["title"], "Changed the title!")
  149. for thread in Thread.objects.filter(id__in=self.ids):
  150. self.assertEqual(thread.title, "Changed the title!")
  151. category = Category.objects.get(pk=self.category.id)
  152. self.assertEqual(category.last_thread_title, "Changed the title!")
  153. @patch_category_acl({"can_edit_threads": 0})
  154. def test_change_thread_title_no_permission(self):
  155. """api validates permission to change title, returns errors"""
  156. response = self.patch(
  157. self.api_link,
  158. {
  159. "ids": self.ids,
  160. "ops": [
  161. {"op": "replace", "path": "title", "value": "Changed the title!"}
  162. ],
  163. },
  164. )
  165. self.assertEqual(response.status_code, 400)
  166. response_json = response.json()
  167. for i, thread in enumerate(self.threads):
  168. self.assertEqual(response_json[i]["id"], thread.id)
  169. self.assertEqual(
  170. response_json[i]["detail"], ["You can't edit threads in this category."]
  171. )
  172. class BulkThreadMoveApiTests(ThreadsBulkPatchApiTestCase):
  173. def setUp(self):
  174. super().setUp()
  175. Category(name="Other Category", slug="other-category").insert_at(
  176. self.category, position="last-child", save=True
  177. )
  178. self.other_category = Category.objects.get(slug="other-category")
  179. @patch_category_acl({"can_move_threads": True})
  180. @patch_other_category_acl({"can_start_threads": 2})
  181. def test_move_thread(self):
  182. """api moves threads to other category and syncs both categories"""
  183. response = self.patch(
  184. self.api_link,
  185. {
  186. "ids": self.ids,
  187. "ops": [
  188. {
  189. "op": "replace",
  190. "path": "category",
  191. "value": self.other_category.id,
  192. },
  193. {"op": "replace", "path": "flatten-categories", "value": None},
  194. ],
  195. },
  196. )
  197. self.assertEqual(response.status_code, 200)
  198. response_json = response.json()
  199. for i, thread in enumerate(self.threads):
  200. self.assertEqual(response_json[i]["id"], thread.id)
  201. self.assertEqual(response_json[i]["category"], self.other_category.id)
  202. for thread in Thread.objects.filter(id__in=self.ids):
  203. self.assertEqual(thread.category_id, self.other_category.id)
  204. category = Category.objects.get(pk=self.category.id)
  205. self.assertEqual(category.threads, self.category.threads - 3)
  206. new_category = Category.objects.get(pk=self.other_category.id)
  207. self.assertEqual(new_category.threads, 3)
  208. class BulkThreadsHideApiTests(ThreadsBulkPatchApiTestCase):
  209. @patch_category_acl({"can_hide_threads": 1})
  210. def test_hide_thread(self):
  211. """api makes it possible to hide thread"""
  212. response = self.patch(
  213. self.api_link,
  214. {
  215. "ids": self.ids,
  216. "ops": [{"op": "replace", "path": "is-hidden", "value": True}],
  217. },
  218. )
  219. self.assertEqual(response.status_code, 200)
  220. response_json = response.json()
  221. for i, thread in enumerate(self.threads):
  222. self.assertEqual(response_json[i]["id"], thread.id)
  223. self.assertTrue(response_json[i]["is_hidden"])
  224. for thread in Thread.objects.filter(id__in=self.ids):
  225. self.assertTrue(thread.is_hidden)
  226. category = Category.objects.get(pk=self.category.id)
  227. self.assertNotIn(category.last_thread_id, self.ids)
  228. class BulkThreadsApproveApiTests(ThreadsBulkPatchApiTestCase):
  229. @patch_category_acl({"can_approve_content": True})
  230. def test_approve_thread(self):
  231. """api approvse threads and syncs category"""
  232. for thread in self.threads:
  233. thread.first_post.is_unapproved = True
  234. thread.first_post.save()
  235. thread.synchronize()
  236. thread.save()
  237. self.assertTrue(thread.is_unapproved)
  238. self.assertTrue(thread.has_unapproved_posts)
  239. self.category.synchronize()
  240. self.category.save()
  241. response = self.patch(
  242. self.api_link,
  243. {
  244. "ids": self.ids,
  245. "ops": [{"op": "replace", "path": "is-unapproved", "value": False}],
  246. },
  247. )
  248. self.assertEqual(response.status_code, 200)
  249. response_json = response.json()
  250. for i, thread in enumerate(self.threads):
  251. self.assertEqual(response_json[i]["id"], thread.id)
  252. self.assertFalse(response_json[i]["is_unapproved"])
  253. self.assertFalse(response_json[i]["has_unapproved_posts"])
  254. for thread in Thread.objects.filter(id__in=self.ids):
  255. self.assertFalse(thread.is_unapproved)
  256. self.assertFalse(thread.has_unapproved_posts)
  257. category = Category.objects.get(pk=self.category.id)
  258. self.assertIn(category.last_thread_id, self.ids)