test_threads_editor_api.py 21 KB

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