test_thread_postbulkpatch_api.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import json
  4. from datetime import timedelta
  5. from django.urls import reverse
  6. from django.utils import timezone
  7. from misago.acl.testutils import override_acl
  8. from misago.categories.models import Category
  9. from misago.threads import testutils
  10. from misago.threads.models import Post
  11. from misago.users.testutils import AuthenticatedUserTestCase
  12. class ThreadPostBulkPatchApiTestCase(AuthenticatedUserTestCase):
  13. def setUp(self):
  14. super(ThreadPostBulkPatchApiTestCase, self).setUp()
  15. self.category = Category.objects.get(slug='first-category')
  16. self.thread = testutils.post_thread(category=self.category)
  17. self.posts = [
  18. testutils.reply_thread(self.thread, poster=self.user),
  19. testutils.reply_thread(self.thread),
  20. testutils.reply_thread(self.thread, poster=self.user),
  21. ]
  22. self.ids = [p.id for p in self.posts]
  23. self.api_link = reverse(
  24. 'misago:api:thread-post-list',
  25. kwargs={
  26. 'thread_pk': self.thread.pk,
  27. }
  28. )
  29. def patch(self, api_link, ops):
  30. return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
  31. def override_acl(self, extra_acl=None):
  32. new_acl = self.user.acl_cache
  33. new_acl['categories'][self.category.pk].update({
  34. 'can_see': 1,
  35. 'can_browse': 1,
  36. 'can_start_threads': 0,
  37. 'can_reply_threads': 0,
  38. 'can_edit_posts': 1,
  39. })
  40. if extra_acl:
  41. new_acl['categories'][self.category.pk].update(extra_acl)
  42. override_acl(self.user, new_acl)
  43. class BulkPatchSerializerTests(ThreadPostBulkPatchApiTestCase):
  44. def test_invalid_input_type(self):
  45. """api rejects invalid input type"""
  46. response = self.patch(self.api_link, [1, 2, 3])
  47. self.assertEqual(response.status_code, 400)
  48. self.assertEqual(response.json(), {
  49. 'non_field_errors': [
  50. "Invalid data. Expected a dictionary, but got list.",
  51. ],
  52. })
  53. def test_missing_input_keys(self):
  54. """api rejects input with missing keys"""
  55. response = self.patch(self.api_link, {})
  56. self.assertEqual(response.status_code, 400)
  57. self.assertEqual(response.json(), {
  58. 'ids': [
  59. "This field is required.",
  60. ],
  61. 'ops': [
  62. "This field is required.",
  63. ],
  64. })
  65. def test_empty_input_keys(self):
  66. """api rejects input with empty keys"""
  67. response = self.patch(self.api_link, {
  68. 'ids': [],
  69. 'ops': [],
  70. })
  71. self.assertEqual(response.status_code, 400)
  72. self.assertEqual(response.json(), {
  73. 'ids': [
  74. "Ensure this field has at least 1 elements.",
  75. ],
  76. 'ops': [
  77. "Ensure this field has at least 1 elements.",
  78. ],
  79. })
  80. def test_invalid_input_keys(self):
  81. """api rejects input with invalid keys"""
  82. response = self.patch(self.api_link, {
  83. 'ids': ['a'],
  84. 'ops': [1],
  85. })
  86. self.assertEqual(response.status_code, 400)
  87. self.assertEqual(response.json(), {
  88. 'ids': [
  89. "A valid integer is required.",
  90. ],
  91. 'ops': [
  92. 'Expected a dictionary of items but got type "int".',
  93. ],
  94. })
  95. def test_too_small_id(self):
  96. """api rejects input with implausiple id"""
  97. response = self.patch(self.api_link, {
  98. 'ids': [0],
  99. 'ops': [{}],
  100. })
  101. self.assertEqual(response.status_code, 400)
  102. self.assertEqual(response.json(), {
  103. 'ids': [
  104. "Ensure this value is greater than or equal to 1.",
  105. ],
  106. })
  107. def test_too_large_input(self):
  108. """api rejects too large input"""
  109. response = self.patch(self.api_link, {
  110. 'ids': [i + 1 for i in range(200)],
  111. 'ops': [{} for i in range(200)],
  112. })
  113. self.assertEqual(response.status_code, 400)
  114. self.assertEqual(response.json(), {
  115. 'ids': [
  116. "Ensure this field has no more than 24 elements.",
  117. ],
  118. 'ops': [
  119. "Ensure this field has no more than 10 elements.",
  120. ],
  121. })
  122. def test_invalid_id(self):
  123. """api rejects too large input"""
  124. response = self.patch(self.api_link, {
  125. 'ids': [i + 1 for i in range(200)],
  126. 'ops': [{} for i in range(200)],
  127. })
  128. self.assertEqual(response.status_code, 400)
  129. self.assertEqual(response.json(), {
  130. 'ids': [
  131. "Ensure this field has no more than 24 elements.",
  132. ],
  133. 'ops': [
  134. "Ensure this field has no more than 10 elements.",
  135. ],
  136. })
  137. def test_posts_not_found(self):
  138. """api fails to find posts"""
  139. posts = [
  140. testutils.reply_thread(self.thread, is_hidden=True),
  141. testutils.reply_thread(self.thread, is_unapproved=True),
  142. ]
  143. response = self.patch(self.api_link, {
  144. 'ids': [p.id for p in posts],
  145. 'ops': [{}],
  146. })
  147. self.assertEqual(response.status_code, 403)
  148. self.assertEqual(response.json(), {
  149. 'detail': "One or more posts to update could not be found.",
  150. })
  151. def test_ops_invalid(self):
  152. """api validates descriptions"""
  153. response = self.patch(self.api_link, {
  154. 'ids': self.ids[:1],
  155. 'ops': [{}],
  156. })
  157. self.assertEqual(response.status_code, 400)
  158. self.assertEqual(response.json(), [
  159. {'id': self.ids[0], 'detail': ['undefined op']},
  160. ])
  161. def test_anonymous_user(self):
  162. """anonymous users can't use bulk actions"""
  163. self.logout_user()
  164. response = self.patch(self.api_link, {
  165. 'ids': self.ids[:1],
  166. 'ops': [{}],
  167. })
  168. self.assertEqual(response.status_code, 403)
  169. def test_events(self):
  170. """cant use bulk actions for events"""
  171. for post in self.posts:
  172. post.is_event = True
  173. post.save()
  174. response = self.patch(self.api_link, {
  175. 'ids': self.ids,
  176. 'ops': [{}],
  177. })
  178. self.assertEqual(response.status_code, 403)
  179. self.assertEqual(response.json(), {
  180. 'detail': "One or more posts to update could not be found.",
  181. })
  182. class PostsAddAclApiTests(ThreadPostBulkPatchApiTestCase):
  183. def test_add_acl_true(self):
  184. """api adds current event's acl to response"""
  185. response = self.patch(self.api_link, {
  186. 'ids': self.ids,
  187. 'ops': [
  188. {
  189. 'op': 'add',
  190. 'path': 'acl',
  191. 'value': True,
  192. },
  193. ]
  194. })
  195. self.assertEqual(response.status_code, 200)
  196. response_json = response.json()
  197. for i, post in enumerate(self.posts):
  198. self.assertEqual(response_json[i]['id'], post.id)
  199. self.assertTrue(response_json[i]['acl'])
  200. def test_add_acl_false(self):
  201. """if value is false, api won't add acl to the response, but will set empty key"""
  202. response = self.patch(self.api_link, {
  203. 'ids': self.ids,
  204. 'ops': [
  205. {
  206. 'op': 'add',
  207. 'path': 'acl',
  208. 'value': False,
  209. },
  210. ]
  211. })
  212. self.assertEqual(response.status_code, 200)
  213. response_json = response.json()
  214. for i, post in enumerate(self.posts):
  215. self.assertEqual(response_json[i]['id'], post.id)
  216. self.assertIsNone(response_json[i]['acl'])
  217. class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase):
  218. def test_protect_post(self):
  219. """api makes it possible to protect post"""
  220. self.override_acl({
  221. 'can_protect_posts': 1,
  222. 'can_edit_posts': 2,
  223. })
  224. response = self.patch(
  225. self.api_link, {
  226. 'ids': self.ids,
  227. 'ops': [
  228. {
  229. 'op': 'replace',
  230. 'path': 'is-protected',
  231. 'value': True,
  232. },
  233. ]
  234. }
  235. )
  236. self.assertEqual(response.status_code, 200)
  237. response_json = response.json()
  238. for i, post in enumerate(self.posts):
  239. self.assertEqual(response_json[i]['id'], post.id)
  240. self.assertTrue(response_json[i]['is_protected'])
  241. for post in Post.objects.filter(id__in=self.ids):
  242. self.assertTrue(post.is_protected)
  243. def test_unprotect_post(self):
  244. """api makes it possible to unprotect protected post"""
  245. self.override_acl({
  246. 'can_protect_posts': 1,
  247. 'can_edit_posts': 2,
  248. })
  249. for post in self.posts:
  250. post.is_protected = True
  251. post.save()
  252. response = self.patch(
  253. self.api_link, {
  254. 'ids': self.ids,
  255. 'ops': [
  256. {
  257. 'op': 'replace',
  258. 'path': 'is-protected',
  259. 'value': False,
  260. },
  261. ]
  262. }
  263. )
  264. self.assertEqual(response.status_code, 200)
  265. response_json = response.json()
  266. for i, post in enumerate(self.posts):
  267. self.assertEqual(response_json[i]['id'], post.id)
  268. self.assertFalse(response_json[i]['is_protected'])
  269. for post in Post.objects.filter(id__in=self.ids):
  270. self.assertFalse(post.is_protected)
  271. def test_protect_post_no_permission(self):
  272. """api validates permission to protect post"""
  273. self.override_acl({'can_protect_posts': 0})
  274. response = self.patch(
  275. self.api_link, {
  276. 'ids': self.ids,
  277. 'ops': [
  278. {
  279. 'op': 'replace',
  280. 'path': 'is-protected',
  281. 'value': True,
  282. },
  283. ]
  284. }
  285. )
  286. self.assertEqual(response.status_code, 400)
  287. response_json = response.json()
  288. for i, post in enumerate(self.posts):
  289. self.assertEqual(response_json[i]['id'], post.id)
  290. self.assertEqual(
  291. response_json[i]['detail'],
  292. ["You can't protect posts in this category."],
  293. )
  294. for post in Post.objects.filter(id__in=self.ids):
  295. self.assertFalse(post.is_protected)
  296. def test_unprotect_post_no_permission(self):
  297. """api validates permission to unprotect post"""
  298. for post in self.posts:
  299. post.is_protected = True
  300. post.save()
  301. self.override_acl({'can_protect_posts': 0})
  302. response = self.patch(
  303. self.api_link, {
  304. 'ids': self.ids,
  305. 'ops': [
  306. {
  307. 'op': 'replace',
  308. 'path': 'is-protected',
  309. 'value': False,
  310. },
  311. ]
  312. }
  313. )
  314. self.assertEqual(response.status_code, 400)
  315. response_json = response.json()
  316. for i, post in enumerate(self.posts):
  317. self.assertEqual(response_json[i]['id'], post.id)
  318. self.assertEqual(
  319. response_json[i]['detail'],
  320. ["You can't protect posts in this category."],
  321. )
  322. for post in Post.objects.filter(id__in=self.ids):
  323. self.assertTrue(post.is_protected)
  324. def test_protect_post_not_editable(self):
  325. """api validates if we can edit post we want to protect"""
  326. self.override_acl({
  327. 'can_protect_posts': 1,
  328. 'can_edit_posts': 0,
  329. })
  330. response = self.patch(
  331. self.api_link, {
  332. 'ids': self.ids,
  333. 'ops': [
  334. {
  335. 'op': 'replace',
  336. 'path': 'is-protected',
  337. 'value': True,
  338. },
  339. ]
  340. }
  341. )
  342. self.assertEqual(response.status_code, 400)
  343. response_json = response.json()
  344. for i, post in enumerate(self.posts):
  345. self.assertEqual(response_json[i]['id'], post.id)
  346. self.assertEqual(
  347. response_json[i]['detail'],
  348. ["You can't protect posts you can't edit."],
  349. )
  350. for post in Post.objects.filter(id__in=self.ids):
  351. self.assertFalse(post.is_protected)
  352. def test_unprotect_post_not_editable(self):
  353. """api validates if we can edit post we want to protect"""
  354. for post in self.posts:
  355. post.is_protected = True
  356. post.save()
  357. self.override_acl({
  358. 'can_protect_posts': 1,
  359. 'can_edit_posts': 0,
  360. })
  361. response = self.patch(
  362. self.api_link, {
  363. 'ids': self.ids,
  364. 'ops': [
  365. {
  366. 'op': 'replace',
  367. 'path': 'is-protected',
  368. 'value': False,
  369. },
  370. ]
  371. }
  372. )
  373. self.assertEqual(response.status_code, 400)
  374. response_json = response.json()
  375. for i, post in enumerate(self.posts):
  376. self.assertEqual(response_json[i]['id'], post.id)
  377. self.assertEqual(
  378. response_json[i]['detail'],
  379. ["You can't protect posts you can't edit."],
  380. )
  381. for post in Post.objects.filter(id__in=self.ids):
  382. self.assertTrue(post.is_protected)
  383. class PostsApproveApiTests(ThreadPostBulkPatchApiTestCase):
  384. def test_approve_post(self):
  385. """api makes it possible to approve post"""
  386. for post in self.posts:
  387. post.is_unapproved = True
  388. post.save()
  389. self.override_acl({'can_approve_content': 1})
  390. response = self.patch(
  391. self.api_link, {
  392. 'ids': self.ids,
  393. 'ops': [
  394. {
  395. 'op': 'replace',
  396. 'path': 'is-unapproved',
  397. 'value': False,
  398. },
  399. ]
  400. }
  401. )
  402. self.assertEqual(response.status_code, 200)
  403. response_json = response.json()
  404. for i, post in enumerate(self.posts):
  405. self.assertEqual(response_json[i]['id'], post.id)
  406. self.assertFalse(response_json[i]['is_unapproved'])
  407. for post in Post.objects.filter(id__in=self.ids):
  408. self.assertFalse(post.is_unapproved)
  409. def test_unapprove_post(self):
  410. """unapproving posts is not supported by api"""
  411. self.override_acl({'can_approve_content': 1})
  412. response = self.patch(
  413. self.api_link, {
  414. 'ids': self.ids,
  415. 'ops': [
  416. {
  417. 'op': 'replace',
  418. 'path': 'is-unapproved',
  419. 'value': True,
  420. },
  421. ]
  422. }
  423. )
  424. self.assertEqual(response.status_code, 400)
  425. response_json = response.json()
  426. for i, post in enumerate(self.posts):
  427. self.assertEqual(response_json[i]['id'], post.id)
  428. self.assertEqual(
  429. response_json[i]['detail'],
  430. ["Content approval can't be reversed."],
  431. )
  432. for post in Post.objects.filter(id__in=self.ids):
  433. self.assertFalse(post.is_unapproved)
  434. def test_approve_post_no_permission(self):
  435. """api validates approval permission"""
  436. for post in self.posts:
  437. post.poster = self.user
  438. post.is_unapproved = True
  439. post.save()
  440. self.override_acl({'can_approve_content': 0})
  441. response = self.patch(
  442. self.api_link, {
  443. 'ids': self.ids,
  444. 'ops': [
  445. {
  446. 'op': 'replace',
  447. 'path': 'is-unapproved',
  448. 'value': False,
  449. },
  450. ]
  451. }
  452. )
  453. self.assertEqual(response.status_code, 400)
  454. response_json = response.json()
  455. for i, post in enumerate(self.posts):
  456. self.assertEqual(response_json[i]['id'], post.id)
  457. self.assertEqual(
  458. response_json[i]['detail'],
  459. ["You can't approve posts in this category."],
  460. )
  461. for post in Post.objects.filter(id__in=self.ids):
  462. self.assertTrue(post.is_unapproved)
  463. def test_approve_post_closed_thread_no_permission(self):
  464. """api validates approval permission in closed threads"""
  465. for post in self.posts:
  466. post.is_unapproved = True
  467. post.save()
  468. self.thread.is_closed = True
  469. self.thread.save()
  470. self.override_acl({
  471. 'can_approve_content': 1,
  472. 'can_close_threads': 0,
  473. })
  474. response = self.patch(
  475. self.api_link, {
  476. 'ids': self.ids,
  477. 'ops': [
  478. {
  479. 'op': 'replace',
  480. 'path': 'is-unapproved',
  481. 'value': False,
  482. },
  483. ]
  484. }
  485. )
  486. self.assertEqual(response.status_code, 400)
  487. response_json = response.json()
  488. for i, post in enumerate(self.posts):
  489. self.assertEqual(response_json[i]['id'], post.id)
  490. self.assertEqual(
  491. response_json[i]['detail'],
  492. ["This thread is closed. You can't approve posts in it."],
  493. )
  494. for post in Post.objects.filter(id__in=self.ids):
  495. self.assertTrue(post.is_unapproved)
  496. def test_approve_post_closed_category_no_permission(self):
  497. """api validates approval permission in closed categories"""
  498. for post in self.posts:
  499. post.is_unapproved = True
  500. post.save()
  501. self.category.is_closed = True
  502. self.category.save()
  503. self.override_acl({
  504. 'can_approve_content': 1,
  505. 'can_close_threads': 0,
  506. })
  507. response = self.patch(
  508. self.api_link, {
  509. 'ids': self.ids,
  510. 'ops': [
  511. {
  512. 'op': 'replace',
  513. 'path': 'is-unapproved',
  514. 'value': False,
  515. },
  516. ]
  517. }
  518. )
  519. self.assertEqual(response.status_code, 400)
  520. response_json = response.json()
  521. for i, post in enumerate(self.posts):
  522. self.assertEqual(response_json[i]['id'], post.id)
  523. self.assertEqual(
  524. response_json[i]['detail'],
  525. ["This category is closed. You can't approve posts in it."],
  526. )
  527. for post in Post.objects.filter(id__in=self.ids):
  528. self.assertTrue(post.is_unapproved)
  529. def test_approve_first_post(self):
  530. """api approve first post fails"""
  531. for post in self.posts:
  532. post.is_unapproved = True
  533. post.save()
  534. self.thread.set_first_post(self.posts[0])
  535. self.thread.save()
  536. self.override_acl({'can_approve_content': 1})
  537. response = self.patch(
  538. self.api_link, {
  539. 'ids': self.ids,
  540. 'ops': [
  541. {
  542. 'op': 'replace',
  543. 'path': 'is-unapproved',
  544. 'value': False,
  545. },
  546. ]
  547. }
  548. )
  549. self.assertEqual(response.status_code, 400)
  550. response_json = response.json()
  551. self.assertEqual(response_json[0], {
  552. 'id': self.posts[0].id,
  553. 'detail': ["You can't approve thread's first post."],
  554. })
  555. for post in Post.objects.filter(id__in=self.ids):
  556. if post.id == self.ids[0]:
  557. self.assertTrue(post.is_unapproved)
  558. else:
  559. self.assertFalse(post.is_unapproved)
  560. def test_approve_hidden_post(self):
  561. """api approve hidden post fails"""
  562. for post in self.posts:
  563. post.is_unapproved = True
  564. post.is_hidden = True
  565. post.save()
  566. self.override_acl({'can_approve_content': 1})
  567. response = self.patch(
  568. self.api_link, {
  569. 'ids': self.ids,
  570. 'ops': [
  571. {
  572. 'op': 'replace',
  573. 'path': 'is-unapproved',
  574. 'value': False,
  575. },
  576. ]
  577. }
  578. )
  579. self.assertEqual(response.status_code, 400)
  580. response_json = response.json()
  581. for i, post in enumerate(self.posts):
  582. self.assertEqual(response_json[i]['id'], post.id)
  583. self.assertEqual(
  584. response_json[i]['detail'],
  585. ["You can't approve posts the content you can't see."],
  586. )
  587. for post in Post.objects.filter(id__in=self.ids):
  588. self.assertTrue(post.is_unapproved)