test_threads_editor_api.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import os
  2. from django.urls import reverse
  3. from misago.acl import add_acl, useracl
  4. from misago.categories.models import Category
  5. from misago.threads import testutils
  6. from misago.threads.models import Attachment
  7. from misago.threads.serializers import AttachmentSerializer
  8. from misago.threads.test import patch_category_acl
  9. from misago.users.testutils import AuthenticatedUserTestCase
  10. TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
  11. TEST_DOCUMENT_PATH = os.path.join(TESTFILES_DIR, 'document.pdf')
  12. cache_versions = {"acl": "abcdefgh"}
  13. class EditorApiTestCase(AuthenticatedUserTestCase):
  14. def setUp(self):
  15. super().setUp()
  16. self.category = Category.objects.get(slug='first-category')
  17. class ThreadPostEditorApiTests(EditorApiTestCase):
  18. def setUp(self):
  19. super().setUp()
  20. self.api_link = reverse('misago:api:thread-editor')
  21. def test_anonymous_user_request(self):
  22. """endpoint validates if user is authenticated"""
  23. self.logout_user()
  24. response = self.client.get(self.api_link)
  25. self.assertEqual(response.status_code, 403)
  26. self.assertEqual(response.json(), {
  27. "detail": "You need to be signed in to start threads.",
  28. })
  29. @patch_category_acl({'can_browse': False})
  30. def test_category_visibility_validation(self):
  31. """endpoint omits non-browseable categories"""
  32. response = self.client.get(self.api_link)
  33. self.assertEqual(response.status_code, 403)
  34. self.assertEqual(response.json(), {
  35. "detail": "No categories that allow new threads are available to you at the moment.",
  36. })
  37. @patch_category_acl({'can_start_threads': False})
  38. def test_category_disallowing_new_threads(self):
  39. """endpoint omits category disallowing starting threads"""
  40. response = self.client.get(self.api_link)
  41. self.assertEqual(response.status_code, 403)
  42. self.assertEqual(response.json(), {
  43. "detail": "No categories that allow new threads are available to you at the moment.",
  44. })
  45. @patch_category_acl({'can_close_threads': False, 'can_start_threads': True})
  46. def test_category_closed_disallowing_new_threads(self):
  47. """endpoint omits closed category"""
  48. self.category.is_closed = True
  49. self.category.save()
  50. response = self.client.get(self.api_link)
  51. self.assertEqual(response.status_code, 403)
  52. self.assertEqual(response.json(), {
  53. "detail": "No categories that allow new threads are available to you at the moment.",
  54. })
  55. @patch_category_acl({'can_close_threads': True, 'can_start_threads': True})
  56. def test_category_closed_allowing_new_threads(self):
  57. """endpoint adds closed category that allows new threads"""
  58. self.category.is_closed = True
  59. self.category.save()
  60. response = self.client.get(self.api_link)
  61. self.assertEqual(response.status_code, 200)
  62. response_json = response.json()
  63. self.assertEqual(
  64. response_json[0], {
  65. 'id': self.category.pk,
  66. 'name': self.category.name,
  67. 'level': 0,
  68. 'post': {
  69. 'close': True,
  70. 'hide': False,
  71. 'pin': 0,
  72. },
  73. }
  74. )
  75. @patch_category_acl({'can_start_threads': True})
  76. def test_category_allowing_new_threads(self):
  77. """endpoint adds category that allows new threads"""
  78. response = self.client.get(self.api_link)
  79. self.assertEqual(response.status_code, 200)
  80. response_json = response.json()
  81. self.assertEqual(
  82. response_json[0], {
  83. 'id': self.category.pk,
  84. 'name': self.category.name,
  85. 'level': 0,
  86. 'post': {
  87. 'close': False,
  88. 'hide': False,
  89. 'pin': 0,
  90. },
  91. }
  92. )
  93. @patch_category_acl({'can_close_threads': True, 'can_start_threads': True})
  94. def test_category_allowing_closing_threads(self):
  95. """endpoint adds category that allows new closed threads"""
  96. response = self.client.get(self.api_link)
  97. self.assertEqual(response.status_code, 200)
  98. response_json = response.json()
  99. self.assertEqual(
  100. response_json[0], {
  101. 'id': self.category.pk,
  102. 'name': self.category.name,
  103. 'level': 0,
  104. 'post': {
  105. 'close': True,
  106. 'hide': False,
  107. 'pin': 0,
  108. },
  109. }
  110. )
  111. @patch_category_acl({'can_start_threads': True, 'can_pin_threads': 1})
  112. def test_category_allowing_locally_pinned_threads(self):
  113. """endpoint adds category that allows locally pinned threads"""
  114. response = self.client.get(self.api_link)
  115. self.assertEqual(response.status_code, 200)
  116. response_json = response.json()
  117. self.assertEqual(
  118. response_json[0], {
  119. 'id': self.category.pk,
  120. 'name': self.category.name,
  121. 'level': 0,
  122. 'post': {
  123. 'close': False,
  124. 'hide': False,
  125. 'pin': 1,
  126. },
  127. }
  128. )
  129. @patch_category_acl({'can_start_threads': True, 'can_pin_threads': 2})
  130. def test_category_allowing_globally_pinned_threads(self):
  131. """endpoint adds category that allows globally pinned threads"""
  132. response = self.client.get(self.api_link)
  133. self.assertEqual(response.status_code, 200)
  134. response_json = response.json()
  135. self.assertEqual(
  136. response_json[0], {
  137. 'id': self.category.pk,
  138. 'name': self.category.name,
  139. 'level': 0,
  140. 'post': {
  141. 'close': False,
  142. 'hide': False,
  143. 'pin': 2,
  144. },
  145. }
  146. )
  147. @patch_category_acl({'can_start_threads': True, 'can_hide_threads': 1})
  148. def test_category_allowing_hidding_threads(self):
  149. """endpoint adds category that allows hiding threads"""
  150. response = self.client.get(self.api_link)
  151. self.assertEqual(response.status_code, 200)
  152. response_json = response.json()
  153. self.assertEqual(
  154. response_json[0], {
  155. 'id': self.category.pk,
  156. 'name': self.category.name,
  157. 'level': 0,
  158. 'post': {
  159. 'close': 0,
  160. 'hide': 1,
  161. 'pin': 0,
  162. },
  163. }
  164. )
  165. @patch_category_acl({'can_start_threads': True, 'can_hide_threads': 2})
  166. def test_category_allowing_hidding_and_deleting_threads(self):
  167. """endpoint adds category that allows hiding and deleting threads"""
  168. response = self.client.get(self.api_link)
  169. self.assertEqual(response.status_code, 200)
  170. response_json = response.json()
  171. self.assertEqual(
  172. response_json[0], {
  173. 'id': self.category.pk,
  174. 'name': self.category.name,
  175. 'level': 0,
  176. 'post': {
  177. 'close': False,
  178. 'hide': 1,
  179. 'pin': 0,
  180. },
  181. }
  182. )
  183. class ThreadReplyEditorApiTests(EditorApiTestCase):
  184. def setUp(self):
  185. super().setUp()
  186. self.thread = testutils.post_thread(category=self.category)
  187. self.api_link = reverse(
  188. 'misago:api:thread-post-editor', kwargs={
  189. 'thread_pk': self.thread.pk,
  190. }
  191. )
  192. def test_anonymous_user_request(self):
  193. """endpoint validates if user is authenticated"""
  194. self.logout_user()
  195. response = self.client.get(self.api_link)
  196. self.assertEqual(response.status_code, 403)
  197. self.assertEqual(response.json(), {
  198. "detail": "You have to sign in to reply threads.",
  199. })
  200. def test_thread_visibility(self):
  201. """thread's visibility is validated"""
  202. with patch_category_acl({'can_see': False}):
  203. response = self.client.get(self.api_link)
  204. self.assertEqual(response.status_code, 404)
  205. with patch_category_acl({'can_browse': False}):
  206. response = self.client.get(self.api_link)
  207. self.assertEqual(response.status_code, 404)
  208. with patch_category_acl({'can_see_all_threads': False}):
  209. response = self.client.get(self.api_link)
  210. self.assertEqual(response.status_code, 404)
  211. @patch_category_acl({'can_reply_threads': False})
  212. def test_no_reply_permission(self):
  213. """permssion to reply is validated"""
  214. response = self.client.get(self.api_link)
  215. self.assertEqual(response.status_code, 403)
  216. self.assertEqual(response.json(), {
  217. "detail": "You can't reply to threads in this category.",
  218. })
  219. def test_closed_category(self):
  220. """permssion to reply in closed category is validated"""
  221. self.category.is_closed = True
  222. self.category.save()
  223. with patch_category_acl({'can_reply_threads': True, 'can_close_threads': False}):
  224. response = self.client.get(self.api_link)
  225. self.assertEqual(response.status_code, 403)
  226. self.assertEqual(response.json(), {
  227. "detail": "This category is closed. You can't reply to threads in it.",
  228. })
  229. # allow to post in closed category
  230. with patch_category_acl({'can_reply_threads': True, 'can_close_threads': True}):
  231. response = self.client.get(self.api_link)
  232. self.assertEqual(response.status_code, 200)
  233. def test_closed_thread(self):
  234. """permssion to reply in closed thread is validated"""
  235. self.thread.is_closed = True
  236. self.thread.save()
  237. with patch_category_acl({'can_reply_threads': True, 'can_close_threads': False}):
  238. response = self.client.get(self.api_link)
  239. self.assertEqual(response.status_code, 403)
  240. self.assertEqual(response.json(), {
  241. "detail": "You can't reply to closed threads in this category.",
  242. })
  243. # allow to post in closed thread
  244. with patch_category_acl({'can_reply_threads': True, 'can_close_threads': True}):
  245. response = self.client.get(self.api_link)
  246. self.assertEqual(response.status_code, 200)
  247. @patch_category_acl({'can_reply_threads': True})
  248. def test_allow_reply_thread(self):
  249. """api returns 200 code if thread reply is allowed"""
  250. response = self.client.get(self.api_link)
  251. self.assertEqual(response.status_code, 200)
  252. def test_reply_to_visibility(self):
  253. """api validates replied post visibility"""
  254. # unapproved reply can't be replied to
  255. unapproved_reply = testutils.reply_thread(self.thread, is_unapproved=True)
  256. with patch_category_acl({'can_reply_threads': True}):
  257. response = self.client.get('%s?reply=%s' % (self.api_link, unapproved_reply.pk))
  258. self.assertEqual(response.status_code, 404)
  259. # hidden reply can't be replied to
  260. hidden_reply = testutils.reply_thread(self.thread, is_hidden=True)
  261. with patch_category_acl({'can_reply_threads': True}):
  262. response = self.client.get('%s?reply=%s' % (self.api_link, hidden_reply.pk))
  263. self.assertEqual(response.status_code, 403)
  264. self.assertEqual(response.json(), {
  265. "detail": "You can't reply to hidden posts.",
  266. })
  267. def test_reply_to_other_thread_post(self):
  268. """api validates is replied post belongs to same thread"""
  269. other_thread = testutils.post_thread(category=self.category)
  270. reply_to = testutils.reply_thread(other_thread)
  271. response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk))
  272. self.assertEqual(response.status_code, 404)
  273. @patch_category_acl({'can_reply_threads': True})
  274. def test_reply_to_event(self):
  275. """events can't be replied to"""
  276. reply_to = testutils.reply_thread(self.thread, is_event=True)
  277. response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk))
  278. self.assertEqual(response.status_code, 403)
  279. self.assertEqual(response.json(), {
  280. "detail": "You can't reply to events.",
  281. })
  282. @patch_category_acl({'can_reply_threads': True})
  283. def test_reply_to(self):
  284. """api includes replied to post details in response"""
  285. reply_to = testutils.reply_thread(self.thread)
  286. response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk))
  287. self.assertEqual(response.status_code, 200)
  288. self.assertEqual(
  289. response.json(), {
  290. 'id': reply_to.pk,
  291. 'post': reply_to.original,
  292. 'poster': reply_to.poster_name,
  293. }
  294. )
  295. class EditReplyEditorApiTests(EditorApiTestCase):
  296. def setUp(self):
  297. super().setUp()
  298. self.thread = testutils.post_thread(category=self.category)
  299. self.post = testutils.reply_thread(self.thread, poster=self.user)
  300. self.api_link = reverse(
  301. 'misago:api:thread-post-editor',
  302. kwargs={
  303. 'thread_pk': self.thread.pk,
  304. 'pk': self.post.pk,
  305. }
  306. )
  307. def test_anonymous_user_request(self):
  308. """endpoint validates if user is authenticated"""
  309. self.logout_user()
  310. response = self.client.get(self.api_link)
  311. self.assertEqual(response.status_code, 403)
  312. self.assertEqual(response.json(), {
  313. "detail": "You have to sign in to edit posts.",
  314. })
  315. def test_thread_visibility(self):
  316. """thread's visibility is validated"""
  317. with patch_category_acl({'can_see': False}):
  318. response = self.client.get(self.api_link)
  319. self.assertEqual(response.status_code, 404)
  320. with patch_category_acl({'can_browse': False}):
  321. response = self.client.get(self.api_link)
  322. self.assertEqual(response.status_code, 404)
  323. with patch_category_acl({'can_see_all_threads': False}):
  324. response = self.client.get(self.api_link)
  325. self.assertEqual(response.status_code, 404)
  326. @patch_category_acl({'can_edit_posts': 0})
  327. def test_no_edit_permission(self):
  328. """permssion to edit is validated"""
  329. response = self.client.get(self.api_link)
  330. self.assertEqual(response.status_code, 403)
  331. self.assertEqual(response.json(), {
  332. "detail": "You can't edit posts in this category.",
  333. })
  334. def test_closed_category(self):
  335. """permssion to edit in closed category is validated"""
  336. self.category.is_closed = True
  337. self.category.save()
  338. with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': False}):
  339. response = self.client.get(self.api_link)
  340. self.assertEqual(response.status_code, 403)
  341. self.assertEqual(response.json(), {
  342. "detail": "This category is closed. You can't edit posts in it.",
  343. })
  344. # allow to edit in closed category
  345. with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': True}):
  346. response = self.client.get(self.api_link)
  347. self.assertEqual(response.status_code, 200)
  348. def test_closed_thread(self):
  349. """permssion to edit in closed thread is validated"""
  350. self.thread.is_closed = True
  351. self.thread.save()
  352. with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': False}):
  353. response = self.client.get(self.api_link)
  354. self.assertEqual(response.status_code, 403)
  355. self.assertEqual(response.json(), {
  356. "detail": "This thread is closed. You can't edit posts in it.",
  357. })
  358. # allow to edit in closed thread
  359. with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': True}):
  360. response = self.client.get(self.api_link)
  361. self.assertEqual(response.status_code, 200)
  362. def test_protected_post(self):
  363. """permssion to edit protected post is validated"""
  364. self.post.is_protected = True
  365. self.post.save()
  366. with patch_category_acl({'can_edit_posts': 1, 'can_protect_posts': False}):
  367. response = self.client.get(self.api_link)
  368. self.assertEqual(response.status_code, 403)
  369. self.assertEqual(response.json(), {
  370. "detail": "This post is protected. You can't edit it.",
  371. })
  372. # allow to post in closed thread
  373. with patch_category_acl({'can_edit_posts': 1, 'can_protect_posts': True}):
  374. response = self.client.get(self.api_link)
  375. self.assertEqual(response.status_code, 200)
  376. def test_post_visibility(self):
  377. """edited posts visibility is validated"""
  378. self.post.is_hidden = True
  379. self.post.save()
  380. with patch_category_acl({'can_edit_posts': 1}):
  381. response = self.client.get(self.api_link)
  382. self.assertEqual(response.status_code, 403)
  383. self.assertEqual(response.json(), {
  384. "detail": "This post is hidden, you can't edit it.",
  385. })
  386. # allow hidden edition
  387. with patch_category_acl({'can_edit_posts': 1, 'can_hide_posts': 1}):
  388. response = self.client.get(self.api_link)
  389. self.assertEqual(response.status_code, 200)
  390. # test unapproved post
  391. self.post.is_unapproved = True
  392. self.post.is_hidden = False
  393. self.post.poster = None
  394. self.post.save()
  395. with patch_category_acl({'can_edit_posts': 2, 'can_approve_content': 0}):
  396. response = self.client.get(self.api_link)
  397. self.assertEqual(response.status_code, 404)
  398. # allow unapproved edition
  399. with patch_category_acl({'can_edit_posts': 2, 'can_approve_content': 1}):
  400. response = self.client.get(self.api_link)
  401. self.assertEqual(response.status_code, 200)
  402. @patch_category_acl({'can_edit_posts': 2})
  403. def test_post_is_event(self):
  404. """events can't be edited"""
  405. self.post.is_event = True
  406. self.post.save()
  407. response = self.client.get(self.api_link)
  408. self.assertEqual(response.status_code, 403)
  409. self.assertEqual(response.json(), {
  410. "detail": "Events can't be edited.",
  411. })
  412. def test_other_user_post(self):
  413. """api validates if other user's post can be edited"""
  414. self.post.poster = None
  415. self.post.save()
  416. with patch_category_acl({'can_edit_posts': 1}):
  417. response = self.client.get(self.api_link)
  418. self.assertEqual(response.status_code, 403)
  419. self.assertEqual(response.json(), {
  420. "detail": "You can't edit other users posts in this category.",
  421. })
  422. # allow other users post edition
  423. with patch_category_acl({'can_edit_posts': 2}):
  424. response = self.client.get(self.api_link)
  425. self.assertEqual(response.status_code, 200)
  426. @patch_category_acl({'can_hide_threads': 1, 'can_edit_posts': 2})
  427. def test_edit_first_post_hidden(self):
  428. """endpoint returns valid configuration for editor of hidden thread's first post"""
  429. self.thread.is_hidden = True
  430. self.thread.save()
  431. self.thread.first_post.is_hidden = True
  432. self.thread.first_post.save()
  433. api_link = reverse(
  434. 'misago:api:thread-post-editor',
  435. kwargs={
  436. 'thread_pk': self.thread.pk,
  437. 'pk': self.thread.first_post.pk,
  438. }
  439. )
  440. response = self.client.get(api_link)
  441. self.assertEqual(response.status_code, 200)
  442. @patch_category_acl({'can_edit_posts': 1})
  443. def test_edit(self):
  444. """endpoint returns valid configuration for editor"""
  445. with patch_category_acl({'max_attachment_size': 1000}):
  446. for _ in range(3):
  447. with open(TEST_DOCUMENT_PATH, 'rb') as upload:
  448. response = self.client.post(
  449. reverse('misago:api:attachment-list'), data={
  450. 'upload': upload,
  451. }
  452. )
  453. self.assertEqual(response.status_code, 200)
  454. attachments = list(Attachment.objects.order_by('id'))
  455. attachments[0].uploader = None
  456. attachments[0].save()
  457. for attachment in attachments[:2]:
  458. attachment.post = self.post
  459. attachment.save()
  460. response = self.client.get(self.api_link)
  461. user_acl = useracl.get_user_acl(self.user, cache_versions)
  462. for attachment in attachments:
  463. add_acl(user_acl, attachment)
  464. self.assertEqual(response.status_code, 200)
  465. self.assertEqual(
  466. response.json(), {
  467. 'id': self.post.pk,
  468. 'api': self.post.get_api_url(),
  469. 'post': self.post.original,
  470. 'can_protect': False,
  471. 'is_protected': self.post.is_protected,
  472. 'poster': self.post.poster_name,
  473. 'attachments': [
  474. AttachmentSerializer(attachments[1], context={'user': self.user}).data,
  475. AttachmentSerializer(attachments[0], context={'user': self.user}).data,
  476. ],
  477. }
  478. )