test_threads_api.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from datetime import timedelta
  2. from django.utils import timezone
  3. from django.urls import reverse
  4. from misago.categories import THREADS_ROOT_NAME
  5. from misago.categories.models import Category
  6. from misago.threads import testutils
  7. from misago.threads.models import Thread
  8. from misago.threads.test import patch_category_acl
  9. from misago.threads.threadtypes import trees_map
  10. from misago.users.testutils import AuthenticatedUserTestCase
  11. class ThreadsApiTestCase(AuthenticatedUserTestCase):
  12. def setUp(self):
  13. super().setUp()
  14. threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
  15. self.root = Category.objects.get(tree_id=threads_tree_id, level=0)
  16. self.category = Category.objects.get(slug='first-category')
  17. self.thread = testutils.post_thread(category=self.category)
  18. self.api_link = self.thread.get_api_url()
  19. def get_thread_json(self):
  20. response = self.client.get(self.thread.get_api_url())
  21. self.assertEqual(response.status_code, 200)
  22. return response.json()
  23. class ThreadRetrieveApiTests(ThreadsApiTestCase):
  24. def setUp(self):
  25. super().setUp()
  26. self.tested_links = [
  27. self.api_link,
  28. '%sposts/' % self.api_link,
  29. '%sposts/?page=1' % self.api_link,
  30. ]
  31. @patch_category_acl()
  32. def test_api_returns_thread(self):
  33. """api has no showstoppers"""
  34. for link in self.tested_links:
  35. response = self.client.get(link)
  36. self.assertEqual(response.status_code, 200)
  37. response_json = response.json()
  38. self.assertEqual(response_json['id'], self.thread.pk)
  39. self.assertEqual(response_json['title'], self.thread.title)
  40. if 'posts' in link:
  41. self.assertIn('post_set', response_json)
  42. @patch_category_acl({"can_see_all_threads": False})
  43. def test_api_shows_owned_thread(self):
  44. """api handles "owned threads only"""
  45. for link in self.tested_links:
  46. response = self.client.get(link)
  47. self.assertEqual(response.status_code, 404)
  48. self.thread.starter = self.user
  49. self.thread.save()
  50. for link in self.tested_links:
  51. response = self.client.get(link)
  52. self.assertEqual(response.status_code, 200)
  53. @patch_category_acl({"can_see": False})
  54. def test_api_validates_category_see_permission(self):
  55. """api validates category visiblity"""
  56. for link in self.tested_links:
  57. response = self.client.get(link)
  58. self.assertEqual(response.status_code, 404)
  59. @patch_category_acl({"can_browse": False})
  60. def test_api_validates_category_browse_permission(self):
  61. """api validates category browsability"""
  62. for link in self.tested_links:
  63. response = self.client.get(link)
  64. self.assertEqual(response.status_code, 404)
  65. def test_api_validates_posts_visibility(self):
  66. """api validates posts visiblity"""
  67. hidden_post = testutils.reply_thread(
  68. self.thread,
  69. is_hidden=True,
  70. message="I'am hidden test message!",
  71. )
  72. with patch_category_acl({"can_hide_posts": 0}):
  73. response = self.client.get(self.tested_links[1])
  74. self.assertNotContains(response, hidden_post.parsed) # post's body is hidden
  75. # add permission to see hidden posts
  76. with patch_category_acl({"can_hide_posts": 1}):
  77. response = self.client.get(self.tested_links[1])
  78. self.assertContains(
  79. response, hidden_post.parsed
  80. ) # hidden post's body is visible with permission
  81. # unapproved posts shouldn't show at all
  82. unapproved_post = testutils.reply_thread(
  83. self.thread,
  84. is_unapproved=True,
  85. )
  86. with patch_category_acl({"can_approve_content": False}):
  87. response = self.client.get(self.tested_links[1])
  88. self.assertNotContains(response, unapproved_post.get_absolute_url())
  89. # add permission to see unapproved posts
  90. with patch_category_acl({"can_approve_content": True}):
  91. response = self.client.get(self.tested_links[1])
  92. self.assertContains(response, unapproved_post.get_absolute_url())
  93. def test_api_validates_has_unapproved_posts_visibility(self):
  94. """api checks acl before exposing unapproved flag"""
  95. self.thread.has_unapproved_posts = True
  96. self.thread.save()
  97. with patch_category_acl({"can_approve_content": False}):
  98. for link in self.tested_links:
  99. response = self.client.get(link)
  100. self.assertEqual(response.status_code, 200)
  101. response_json = response.json()
  102. self.assertEqual(response_json['id'], self.thread.pk)
  103. self.assertEqual(response_json['title'], self.thread.title)
  104. self.assertFalse(response_json['has_unapproved_posts'])
  105. with patch_category_acl({"can_approve_content": True}):
  106. for link in self.tested_links:
  107. response = self.client.get(link)
  108. self.assertEqual(response.status_code, 200)
  109. response_json = response.json()
  110. self.assertEqual(response_json['id'], self.thread.pk)
  111. self.assertEqual(response_json['title'], self.thread.title)
  112. self.assertTrue(response_json['has_unapproved_posts'])
  113. class ThreadDeleteApiTests(ThreadsApiTestCase):
  114. def setUp(self):
  115. super().setUp()
  116. self.last_thread = testutils.post_thread(category=self.category)
  117. self.api_link = self.last_thread.get_api_url()
  118. def test_delete_thread_no_permission(self):
  119. """api tests permission to delete threads"""
  120. with patch_category_acl({"can_hide_threads": 0}):
  121. response = self.client.delete(self.api_link)
  122. self.assertEqual(response.status_code, 403)
  123. self.assertEqual(
  124. response.json()['detail'], "You can't delete threads in this category."
  125. )
  126. with patch_category_acl({"can_hide_threads": 1}):
  127. response = self.client.delete(self.api_link)
  128. self.assertEqual(response.status_code, 403)
  129. self.assertEqual(
  130. response.json()['detail'], "You can't delete threads in this category."
  131. )
  132. @patch_category_acl({'can_hide_threads': 1, 'can_hide_own_threads': 2})
  133. def test_delete_other_user_thread_no_permission(self):
  134. """api tests thread owner when deleting own thread"""
  135. response = self.client.delete(self.api_link)
  136. self.assertEqual(response.status_code, 403)
  137. self.assertEqual(
  138. response.json()['detail'], "You can't delete other users theads in this category."
  139. )
  140. @patch_category_acl({
  141. 'can_hide_threads': 2,
  142. 'can_hide_own_threads': 2,
  143. 'can_close_threads': False,
  144. })
  145. def test_delete_thread_closed_category_no_permission(self):
  146. """api tests category's closed state"""
  147. self.category.is_closed = True
  148. self.category.save()
  149. response = self.client.delete(self.api_link)
  150. self.assertEqual(response.status_code, 403)
  151. self.assertEqual(
  152. response.json()['detail'], "This category is closed. You can't delete threads in it."
  153. )
  154. @patch_category_acl({
  155. 'can_hide_threads': 2,
  156. 'can_hide_own_threads': 2,
  157. 'can_close_threads': False,
  158. })
  159. def test_delete_thread_closed_no_permission(self):
  160. """api tests thread's closed state"""
  161. self.last_thread.is_closed = True
  162. self.last_thread.save()
  163. response = self.client.delete(self.api_link)
  164. self.assertEqual(response.status_code, 403)
  165. self.assertEqual(
  166. response.json()['detail'], "This thread is closed. You can't delete it."
  167. )
  168. @patch_category_acl({
  169. 'can_hide_threads': 1,
  170. 'can_hide_own_threads': 2,
  171. 'thread_edit_time': 1
  172. })
  173. def test_delete_owned_thread_no_time(self):
  174. """api tests permission to delete owned thread within time limit"""
  175. self.last_thread.starter = self.user
  176. self.last_thread.started_on = timezone.now() - timedelta(minutes=10)
  177. self.last_thread.save()
  178. response = self.client.delete(self.api_link)
  179. self.assertEqual(response.status_code, 403)
  180. self.assertEqual(
  181. response.json()['detail'], "You can't delete threads that are older than 1 minute."
  182. )
  183. @patch_category_acl({'can_hide_threads': 2})
  184. def test_delete_thread(self):
  185. """DELETE to API link with permission deletes thread"""
  186. category = Category.objects.get(slug='first-category')
  187. self.assertEqual(category.last_thread_id, self.last_thread.pk)
  188. response = self.client.delete(self.api_link)
  189. self.assertEqual(response.status_code, 200)
  190. with self.assertRaises(Thread.DoesNotExist):
  191. Thread.objects.get(pk=self.last_thread.pk)
  192. # category was synchronised after deletion
  193. category = Category.objects.get(slug='first-category')
  194. self.assertEqual(category.last_thread_id, self.thread.pk)
  195. # test that last thread's deletion triggers category sync
  196. response = self.client.delete(self.thread.get_api_url())
  197. self.assertEqual(response.status_code, 200)
  198. with self.assertRaises(Thread.DoesNotExist):
  199. Thread.objects.get(pk=self.thread.pk)
  200. category = Category.objects.get(slug='first-category')
  201. self.assertIsNone(category.last_thread_id)
  202. @patch_category_acl({
  203. 'can_hide_threads': 1,
  204. 'can_hide_own_threads': 2,
  205. 'thread_edit_time': 30
  206. })
  207. def test_delete_owned_thread(self):
  208. """api lets owner to delete owned thread within time limit"""
  209. self.last_thread.starter = self.user
  210. self.last_thread.started_on = timezone.now() - timedelta(minutes=10)
  211. self.last_thread.save()
  212. response = self.client.delete(self.api_link)
  213. self.assertEqual(response.status_code, 200)