test_thread_postbulkdelete_api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import json
  2. from datetime import timedelta
  3. from django.urls import reverse
  4. from django.utils import timezone
  5. from misago.threads import test
  6. from misago.threads.models import Post, Thread
  7. from misago.threads.test import patch_category_acl
  8. from .test_threads_api import ThreadsApiTestCase
  9. class PostBulkDeleteApiTests(ThreadsApiTestCase):
  10. def setUp(self):
  11. super().setUp()
  12. self.posts = [
  13. test.reply_thread(self.thread, poster=self.user),
  14. test.reply_thread(self.thread),
  15. test.reply_thread(self.thread, poster=self.user),
  16. ]
  17. self.api_link = reverse(
  18. "misago:api:thread-post-list", kwargs={"thread_pk": self.thread.pk}
  19. )
  20. def delete(self, url, data=None):
  21. return self.client.delete(
  22. url, json.dumps(data), content_type="application/json"
  23. )
  24. def test_delete_anonymous(self):
  25. """api validates if deleting user is authenticated"""
  26. self.logout_user()
  27. response = self.delete(self.api_link)
  28. self.assertEqual(response.status_code, 403)
  29. self.assertEqual(
  30. response.json(), {"detail": "This action is not available to guests."}
  31. )
  32. def test_delete_no_data(self):
  33. """api handles empty data"""
  34. response = self.client.delete(self.api_link, content_type="application/json")
  35. self.assertEqual(response.status_code, 400)
  36. self.assertEqual(
  37. response.json(), {"detail": 'Expected a list of items but got type "dict".'}
  38. )
  39. def test_delete_no_ids(self):
  40. """api requires ids to delete"""
  41. response = self.delete(self.api_link)
  42. self.assertEqual(response.status_code, 400)
  43. self.assertEqual(
  44. response.json(),
  45. {"detail": "You have to specify at least one post to delete."},
  46. )
  47. def test_delete_empty_ids(self):
  48. """api requires ids to delete"""
  49. response = self.delete(self.api_link, [])
  50. self.assertEqual(response.status_code, 400)
  51. self.assertEqual(
  52. response.json(),
  53. {"detail": "You have to specify at least one post to delete."},
  54. )
  55. @patch_category_acl({"can_hide_posts": 2})
  56. def test_validate_ids(self):
  57. """api validates that ids are list of ints"""
  58. response = self.delete(self.api_link, True)
  59. self.assertEqual(response.status_code, 400)
  60. self.assertEqual(
  61. response.json(), {"detail": 'Expected a list of items but got type "bool".'}
  62. )
  63. response = self.delete(self.api_link, "abbss")
  64. self.assertEqual(response.status_code, 400)
  65. self.assertEqual(
  66. response.json(), {"detail": 'Expected a list of items but got type "str".'}
  67. )
  68. response = self.delete(self.api_link, [1, 2, 3, "a", "b", "x"])
  69. self.assertEqual(response.status_code, 400)
  70. self.assertEqual(
  71. response.json(), {"detail": "One or more post ids received were invalid."}
  72. )
  73. @patch_category_acl({"can_hide_posts": 2})
  74. def test_validate_ids_length(self):
  75. """api validates that ids are list of ints"""
  76. response = self.delete(self.api_link, list(range(100)))
  77. self.assertEqual(response.status_code, 400)
  78. self.assertEqual(
  79. response.json(),
  80. {"detail": "No more than 24 posts can be deleted at single time."},
  81. )
  82. @patch_category_acl({"can_hide_posts": 2})
  83. def test_validate_posts_exist(self):
  84. """api validates that ids are visible posts"""
  85. response = self.delete(self.api_link, [p.id * 10 for p in self.posts])
  86. self.assertEqual(response.status_code, 403)
  87. self.assertEqual(
  88. response.json(),
  89. {"detail": "One or more posts to delete could not be found."},
  90. )
  91. @patch_category_acl({"can_hide_posts": 2})
  92. def test_validate_posts_visibility(self):
  93. """api validates that ids are visible posts"""
  94. self.posts[1].is_unapproved = True
  95. self.posts[1].save()
  96. response = self.delete(self.api_link, [p.id for p in self.posts])
  97. self.assertEqual(response.status_code, 403)
  98. self.assertEqual(
  99. response.json(),
  100. {"detail": "One or more posts to delete could not be found."},
  101. )
  102. @patch_category_acl({"can_hide_posts": 2})
  103. def test_validate_posts_same_thread(self):
  104. """api validates that ids are same thread posts"""
  105. other_thread = test.post_thread(category=self.category)
  106. self.posts.append(test.reply_thread(other_thread, poster=self.user))
  107. response = self.delete(self.api_link, [p.id for p in self.posts])
  108. self.assertEqual(response.status_code, 403)
  109. self.assertEqual(
  110. response.json(),
  111. {"detail": "One or more posts to delete could not be found."},
  112. )
  113. @patch_category_acl({"can_hide_posts": 1, "can_hide_own_posts": 1})
  114. def test_no_permission(self):
  115. """api validates permission to delete"""
  116. response = self.delete(self.api_link, [p.id for p in self.posts])
  117. self.assertEqual(response.status_code, 403)
  118. self.assertEqual(
  119. response.json(), {"detail": "You can't delete posts in this category."}
  120. )
  121. @patch_category_acl(
  122. {"can_hide_posts": 0, "can_hide_own_posts": 2, "post_edit_time": 10}
  123. )
  124. def test_delete_other_user_post_no_permission(self):
  125. """api valdiates if user can delete other users posts"""
  126. response = self.delete(self.api_link, [p.id for p in self.posts])
  127. self.assertEqual(response.status_code, 403)
  128. self.assertEqual(
  129. response.json(),
  130. {"detail": "You can't delete other users posts in this category."},
  131. )
  132. @patch_category_acl(
  133. {"can_hide_posts": 0, "can_hide_own_posts": 2, "can_protect_posts": False}
  134. )
  135. def test_delete_protected_post_no_permission(self):
  136. """api validates if user can delete protected post"""
  137. self.posts[0].is_protected = True
  138. self.posts[0].save()
  139. response = self.delete(self.api_link, [p.id for p in self.posts])
  140. self.assertEqual(response.status_code, 403)
  141. self.assertEqual(
  142. response.json(), {"detail": "This post is protected. You can't delete it."}
  143. )
  144. @patch_category_acl(
  145. {"can_hide_posts": 0, "can_hide_own_posts": 2, "post_edit_time": 1}
  146. )
  147. def test_delete_protected_post_after_edit_time(self):
  148. """api validates if user can delete delete post after edit time"""
  149. self.posts[0].posted_on = timezone.now() - timedelta(minutes=10)
  150. self.posts[0].save()
  151. response = self.delete(self.api_link, [p.id for p in self.posts])
  152. self.assertEqual(response.status_code, 403)
  153. self.assertEqual(
  154. response.json(),
  155. {"detail": "You can't delete posts that are older than 1 minute."},
  156. )
  157. @patch_category_acl(
  158. {"can_hide_posts": 2, "can_hide_own_posts": 2, "can_close_threads": False}
  159. )
  160. def test_delete_post_closed_thread_no_permission(self):
  161. """api valdiates if user can delete posts in closed threads"""
  162. self.thread.is_closed = True
  163. self.thread.save()
  164. response = self.delete(self.api_link, [p.id for p in self.posts])
  165. self.assertEqual(response.status_code, 403)
  166. self.assertEqual(
  167. response.json(),
  168. {"detail": "This thread is closed. You can't delete posts in it."},
  169. )
  170. @patch_category_acl(
  171. {"can_hide_posts": 2, "can_hide_own_posts": 2, "can_close_threads": False}
  172. )
  173. def test_delete_post_closed_category_no_permission(self):
  174. """api valdiates if user can delete posts in closed categories"""
  175. self.category.is_closed = True
  176. self.category.save()
  177. response = self.delete(self.api_link, [p.id for p in self.posts])
  178. self.assertEqual(response.status_code, 403)
  179. self.assertEqual(
  180. response.json(),
  181. {"detail": "This category is closed. You can't delete posts in it."},
  182. )
  183. @patch_category_acl({"can_hide_posts": 2, "can_hide_own_posts": 2})
  184. def test_delete_first_post(self):
  185. """api disallows first post's deletion"""
  186. ids = [p.id for p in self.posts]
  187. ids.append(self.thread.first_post_id)
  188. response = self.delete(self.api_link, ids)
  189. self.assertEqual(response.status_code, 403)
  190. self.assertEqual(
  191. response.json(), {"detail": "You can't delete thread's first post."}
  192. )
  193. @patch_category_acl({"can_hide_posts": 2, "can_hide_own_posts": 2})
  194. def test_delete_best_answer(self):
  195. """api disallows best answer deletion"""
  196. self.thread.set_best_answer(self.user, self.posts[0])
  197. self.thread.save()
  198. response = self.delete(self.api_link, [p.id for p in self.posts])
  199. self.assertEqual(response.status_code, 403)
  200. self.assertEqual(
  201. response.json(),
  202. {"detail": "You can't delete this post because its marked as best answer."},
  203. )
  204. @patch_category_acl(
  205. {"can_hide_posts": 2, "can_hide_own_posts": 2, "can_hide_events": 0}
  206. )
  207. def test_delete_event(self):
  208. """api differs posts from events"""
  209. self.posts[1].is_event = True
  210. self.posts[1].save()
  211. response = self.delete(self.api_link, [p.id for p in self.posts])
  212. self.assertEqual(response.status_code, 403)
  213. self.assertEqual(
  214. response.json(), {"detail": "You can't delete events in this category."}
  215. )
  216. @patch_category_acl(
  217. {"can_hide_posts": 0, "can_hide_own_posts": 2, "post_edit_time": 10}
  218. )
  219. def test_delete_owned_posts(self):
  220. """api deletes owned thread posts"""
  221. ids = [self.posts[0].id, self.posts[-1].id]
  222. response = self.delete(self.api_link, ids)
  223. self.thread = Thread.objects.get(pk=self.thread.pk)
  224. self.assertNotEqual(self.thread.last_post_id, ids[-1])
  225. for post in ids:
  226. with self.assertRaises(Post.DoesNotExist):
  227. self.thread.post_set.get(pk=post)
  228. @patch_category_acl({"can_hide_posts": 2, "can_hide_own_posts": 0})
  229. def test_delete_posts(self):
  230. """api deletes thread posts"""
  231. response = self.delete(self.api_link, [p.id for p in self.posts])
  232. self.assertEqual(response.status_code, 200)
  233. self.thread = Thread.objects.get(pk=self.thread.pk)
  234. self.assertNotEqual(self.thread.last_post_id, self.posts[-1].pk)
  235. for post in self.posts:
  236. with self.assertRaises(Post.DoesNotExist):
  237. self.thread.post_set.get(pk=post.pk)