test_thread_postsplit_api.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. from __future__ import unicode_literals
  2. import json
  3. from django.urls import reverse
  4. from misago.acl.testutils import override_acl
  5. from misago.categories.models import Category
  6. from misago.readtracker import poststracker
  7. from misago.threads import testutils
  8. from misago.threads.models import Post, Thread
  9. from misago.threads.serializers.moderation import POSTS_LIMIT
  10. from misago.users.testutils import AuthenticatedUserTestCase
  11. class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
  12. def setUp(self):
  13. super(ThreadPostSplitApiTestCase, self).setUp()
  14. self.category = Category.objects.get(slug='first-category')
  15. self.thread = testutils.post_thread(category=self.category)
  16. self.posts = [
  17. testutils.reply_thread(self.thread).pk,
  18. testutils.reply_thread(self.thread).pk,
  19. ]
  20. self.api_link = reverse(
  21. 'misago:api:thread-post-split', kwargs={
  22. 'thread_pk': self.thread.pk,
  23. }
  24. )
  25. Category(
  26. name='Category B',
  27. slug='category-b',
  28. ).insert_at(
  29. self.category,
  30. position='last-child',
  31. save=True,
  32. )
  33. self.category_b = Category.objects.get(slug='category-b')
  34. self.override_acl()
  35. self.override_other_acl()
  36. def refresh_thread(self):
  37. self.thread = Thread.objects.get(pk=self.thread.pk)
  38. def override_acl(self, extra_acl=None):
  39. new_acl = self.user.acl_cache
  40. new_acl['categories'][self.category.pk].update({
  41. 'can_see': 1,
  42. 'can_browse': 1,
  43. 'can_start_threads': 1,
  44. 'can_reply_threads': 1,
  45. 'can_edit_posts': 1,
  46. 'can_approve_content': 0,
  47. 'can_move_posts': 1,
  48. })
  49. if extra_acl:
  50. new_acl['categories'][self.category.pk].update(extra_acl)
  51. override_acl(self.user, new_acl)
  52. def override_other_acl(self, acl=None):
  53. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  54. other_category_acl.update({
  55. 'can_see': 1,
  56. 'can_browse': 1,
  57. 'can_start_threads': 0,
  58. 'can_reply_threads': 0,
  59. 'can_edit_posts': 1,
  60. 'can_approve_content': 0,
  61. 'can_move_posts': 1,
  62. })
  63. if acl:
  64. other_category_acl.update(acl)
  65. categories_acl = self.user.acl_cache['categories']
  66. categories_acl[self.category_b.pk] = other_category_acl
  67. visible_categories = [self.category.pk]
  68. if other_category_acl['can_see']:
  69. visible_categories.append(self.category_b.pk)
  70. override_acl(
  71. self.user, {
  72. 'visible_categories': visible_categories,
  73. 'categories': categories_acl,
  74. }
  75. )
  76. def test_anonymous_user(self):
  77. """you need to authenticate to split posts"""
  78. self.logout_user()
  79. response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
  80. self.assertEqual(response.status_code, 403)
  81. def test_no_permission(self):
  82. """api validates permission to split"""
  83. self.override_acl({'can_move_posts': 0})
  84. response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
  85. self.assertContains(response, "You can't split posts from this thread.", status_code=403)
  86. def test_empty_data(self):
  87. """api handles empty data"""
  88. response = self.client.post(self.api_link)
  89. self.assertContains(
  90. response, "You have to specify at least one post to split.", status_code=400
  91. )
  92. def test_invalid_data(self):
  93. """api handles post that is invalid type"""
  94. self.override_acl()
  95. response = self.client.post(self.api_link, '[]', content_type="application/json")
  96. self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
  97. self.override_acl()
  98. response = self.client.post(self.api_link, '123', content_type="application/json")
  99. self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
  100. self.override_acl()
  101. response = self.client.post(self.api_link, '"string"', content_type="application/json")
  102. self.assertContains(response, "Invalid data. Expected a dictionary", status_code=400)
  103. self.override_acl()
  104. response = self.client.post(self.api_link, 'malformed', content_type="application/json")
  105. self.assertContains(response, "JSON parse error", status_code=400)
  106. def test_no_posts_ids(self):
  107. """api rejects no posts ids"""
  108. response = self.client.post(
  109. self.api_link,
  110. json.dumps({}),
  111. content_type="application/json",
  112. )
  113. self.assertContains(
  114. response, "You have to specify at least one post to split.", status_code=400
  115. )
  116. def test_empty_posts_ids(self):
  117. """api rejects empty posts ids list"""
  118. response = self.client.post(
  119. self.api_link,
  120. json.dumps({
  121. 'posts': [],
  122. }),
  123. content_type="application/json",
  124. )
  125. self.assertContains(
  126. response, "You have to specify at least one post to split.", status_code=400
  127. )
  128. def test_invalid_posts_data(self):
  129. """api handles invalid data"""
  130. response = self.client.post(
  131. self.api_link,
  132. json.dumps({
  133. 'posts': 'string',
  134. }),
  135. content_type="application/json",
  136. )
  137. self.assertContains(
  138. response, "Expected a list of items but got type", status_code=400
  139. )
  140. def test_invalid_posts_ids(self):
  141. """api handles invalid post id"""
  142. response = self.client.post(
  143. self.api_link,
  144. json.dumps({
  145. 'posts': [1, 2, 'string'],
  146. }),
  147. content_type="application/json",
  148. )
  149. self.assertContains(
  150. response, "One or more post ids received were invalid.", status_code=400
  151. )
  152. def test_split_limit(self):
  153. """api rejects more posts than split limit"""
  154. response = self.client.post(
  155. self.api_link,
  156. json.dumps({
  157. 'posts': list(range(POSTS_LIMIT + 1)),
  158. }),
  159. content_type="application/json",
  160. )
  161. self.assertContains(
  162. response, "No more than {} posts can be split".format(POSTS_LIMIT), status_code=400
  163. )
  164. def test_split_invisible(self):
  165. """api validates posts visibility"""
  166. response = self.client.post(
  167. self.api_link,
  168. json.dumps({
  169. 'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
  170. }),
  171. content_type="application/json",
  172. )
  173. self.assertContains(
  174. response, "One or more posts to split could not be found.", status_code=400
  175. )
  176. def test_split_event(self):
  177. """api rejects events split"""
  178. response = self.client.post(
  179. self.api_link,
  180. json.dumps({
  181. 'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
  182. }),
  183. content_type="application/json",
  184. )
  185. self.assertContains(response, "Events can't be split.", status_code=400)
  186. def test_split_first_post(self):
  187. """api rejects first post split"""
  188. response = self.client.post(
  189. self.api_link,
  190. json.dumps({
  191. 'posts': [self.thread.first_post_id],
  192. }),
  193. content_type="application/json",
  194. )
  195. self.assertContains(response, "You can't split thread's first post.", status_code=400)
  196. def test_split_hidden_posts(self):
  197. """api recjects attempt to split urneadable hidden post"""
  198. response = self.client.post(
  199. self.api_link,
  200. json.dumps({
  201. 'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
  202. }),
  203. content_type="application/json",
  204. )
  205. self.assertContains(
  206. response, "You can't split posts the content you can't see.", status_code=400
  207. )
  208. def test_split_posts_closed_thread_no_permission(self):
  209. """api recjects attempt to split posts from closed thread"""
  210. self.thread.is_closed = True
  211. self.thread.save()
  212. self.override_acl({'can_close_threads': 0})
  213. response = self.client.post(
  214. self.api_link,
  215. json.dumps({
  216. 'posts': [testutils.reply_thread(self.thread).pk],
  217. }),
  218. content_type="application/json",
  219. )
  220. self.assertContains(
  221. response, "This thread is closed. You can't split posts in it.", status_code=400
  222. )
  223. def test_split_posts_closed_category_no_permission(self):
  224. """api recjects attempt to split posts from closed thread"""
  225. self.category.is_closed = True
  226. self.category.save()
  227. self.override_acl({'can_close_threads': 0})
  228. response = self.client.post(
  229. self.api_link,
  230. json.dumps({
  231. 'posts': [testutils.reply_thread(self.thread).pk],
  232. }),
  233. content_type="application/json",
  234. )
  235. self.assertContains(
  236. response, "This category is closed. You can't split posts in it.", status_code=400
  237. )
  238. def test_split_other_thread_posts(self):
  239. """api recjects attempt to split other thread's post"""
  240. other_thread = testutils.post_thread(self.category)
  241. response = self.client.post(
  242. self.api_link,
  243. json.dumps({
  244. 'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
  245. }),
  246. content_type="application/json",
  247. )
  248. self.assertContains(
  249. response, "One or more posts to split could not be found.", status_code=400
  250. )
  251. def test_split_empty_new_thread_data(self):
  252. """api handles empty form data"""
  253. response = self.client.post(
  254. self.api_link,
  255. json.dumps({
  256. 'posts': self.posts,
  257. }),
  258. content_type="application/json",
  259. )
  260. self.assertEqual(response.status_code, 400)
  261. response_json = response.json()
  262. self.assertEqual(
  263. response_json, {
  264. 'title': ['This field is required.'],
  265. 'category': ['This field is required.'],
  266. }
  267. )
  268. def test_split_invalid_final_title(self):
  269. """api rejects split because final thread title was invalid"""
  270. response = self.client.post(
  271. self.api_link,
  272. json.dumps({
  273. 'posts': self.posts,
  274. 'title': '$$$',
  275. 'category': self.category.id,
  276. }),
  277. content_type="application/json",
  278. )
  279. self.assertEqual(response.status_code, 400)
  280. response_json = response.json()
  281. self.assertEqual(
  282. response_json, {
  283. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  284. }
  285. )
  286. def test_split_invalid_category(self):
  287. """api rejects split because final category was invalid"""
  288. self.override_other_acl({'can_see': 0})
  289. response = self.client.post(
  290. self.api_link,
  291. json.dumps({
  292. 'posts': self.posts,
  293. 'title': 'Valid thread title',
  294. 'category': self.category_b.id,
  295. }),
  296. content_type="application/json",
  297. )
  298. self.assertEqual(response.status_code, 400)
  299. response_json = response.json()
  300. self.assertEqual(
  301. response_json, {
  302. 'category': ["Requested category could not be found."],
  303. }
  304. )
  305. def test_split_unallowed_start_thread(self):
  306. """api rejects split because category isn't allowing starting threads"""
  307. self.override_acl({'can_start_threads': 0})
  308. response = self.client.post(
  309. self.api_link,
  310. json.dumps({
  311. 'posts': self.posts,
  312. 'title': 'Valid thread title',
  313. 'category': self.category.id,
  314. }),
  315. content_type="application/json",
  316. )
  317. self.assertEqual(response.status_code, 400)
  318. response_json = response.json()
  319. self.assertEqual(
  320. response_json, {
  321. 'category': ["You can't create new threads in selected category."],
  322. }
  323. )
  324. def test_split_invalid_weight(self):
  325. """api rejects split because final weight was invalid"""
  326. response = self.client.post(
  327. self.api_link,
  328. json.dumps({
  329. 'posts': self.posts,
  330. 'title': 'Valid thread title',
  331. 'category': self.category.id,
  332. 'weight': 4,
  333. }),
  334. content_type="application/json",
  335. )
  336. self.assertEqual(response.status_code, 400)
  337. response_json = response.json()
  338. self.assertEqual(
  339. response_json, {
  340. 'weight': ["Ensure this value is less than or equal to 2."],
  341. }
  342. )
  343. def test_split_unallowed_global_weight(self):
  344. """api rejects split because global weight was unallowed"""
  345. response = self.client.post(
  346. self.api_link,
  347. json.dumps({
  348. 'posts': self.posts,
  349. 'title': 'Valid thread title',
  350. 'category': self.category.id,
  351. 'weight': 2,
  352. }),
  353. content_type="application/json",
  354. )
  355. self.assertEqual(response.status_code, 400)
  356. response_json = response.json()
  357. self.assertEqual(
  358. response_json, {
  359. 'weight': ["You don't have permission to pin threads globally in this category."],
  360. }
  361. )
  362. def test_split_unallowed_local_weight(self):
  363. """api rejects split because local weight was unallowed"""
  364. response = self.client.post(
  365. self.api_link,
  366. json.dumps({
  367. 'posts': self.posts,
  368. 'title': 'Valid thread title',
  369. 'category': self.category.id,
  370. 'weight': 1,
  371. }),
  372. content_type="application/json"
  373. )
  374. self.assertEqual(response.status_code, 400)
  375. response_json = response.json()
  376. self.assertEqual(
  377. response_json, {
  378. 'weight': ["You don't have permission to pin threads in this category."],
  379. }
  380. )
  381. def test_split_allowed_local_weight(self):
  382. """api allows local weight"""
  383. self.override_acl({'can_pin_threads': 1})
  384. response = self.client.post(
  385. self.api_link,
  386. json.dumps({
  387. 'posts': self.posts,
  388. 'title': '$$$',
  389. 'category': self.category.id,
  390. 'weight': 1,
  391. }),
  392. content_type="application/json",
  393. )
  394. self.assertEqual(response.status_code, 400)
  395. response_json = response.json()
  396. self.assertEqual(
  397. response_json, {
  398. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  399. }
  400. )
  401. def test_split_allowed_global_weight(self):
  402. """api allows global weight"""
  403. self.override_acl({'can_pin_threads': 2})
  404. response = self.client.post(
  405. self.api_link,
  406. json.dumps({
  407. 'posts': self.posts,
  408. 'title': '$$$',
  409. 'category': self.category.id,
  410. 'weight': 2,
  411. }),
  412. content_type="application/json",
  413. )
  414. self.assertEqual(response.status_code, 400)
  415. response_json = response.json()
  416. self.assertEqual(
  417. response_json, {
  418. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  419. }
  420. )
  421. def test_split_unallowed_close(self):
  422. """api rejects split because closing thread was unallowed"""
  423. response = self.client.post(
  424. self.api_link,
  425. json.dumps({
  426. 'posts': self.posts,
  427. 'title': 'Valid thread title',
  428. 'category': self.category.id,
  429. 'is_closed': True,
  430. }),
  431. content_type="application/json",
  432. )
  433. self.assertEqual(response.status_code, 400)
  434. response_json = response.json()
  435. self.assertEqual(
  436. response_json, {
  437. 'is_closed': ["You don't have permission to close threads in this category."],
  438. }
  439. )
  440. def test_split_with_close(self):
  441. """api allows for closing thread"""
  442. self.override_acl({'can_close_threads': True})
  443. response = self.client.post(
  444. self.api_link,
  445. json.dumps({
  446. 'posts': self.posts,
  447. 'title': '$$$',
  448. 'category': self.category.id,
  449. 'weight': 0,
  450. 'is_closed': True,
  451. }),
  452. content_type="application/json",
  453. )
  454. self.assertEqual(response.status_code, 400)
  455. response_json = response.json()
  456. self.assertEqual(
  457. response_json, {
  458. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  459. }
  460. )
  461. def test_split_unallowed_hidden(self):
  462. """api rejects split because hidden thread was unallowed"""
  463. response = self.client.post(
  464. self.api_link,
  465. json.dumps({
  466. 'posts': self.posts,
  467. 'title': 'Valid thread title',
  468. 'category': self.category.id,
  469. 'is_hidden': True,
  470. }),
  471. content_type="application/json",
  472. )
  473. self.assertEqual(response.status_code, 400)
  474. response_json = response.json()
  475. self.assertEqual(
  476. response_json, {
  477. 'is_hidden': ["You don't have permission to hide threads in this category."],
  478. }
  479. )
  480. def test_split_with_hide(self):
  481. """api allows for hiding thread"""
  482. self.override_acl({'can_hide_threads': True})
  483. response = self.client.post(
  484. self.api_link,
  485. json.dumps({
  486. 'posts': self.posts,
  487. 'title': '$$$',
  488. 'category': self.category.id,
  489. 'weight': 0,
  490. 'is_hidden': True,
  491. }),
  492. content_type="application/json",
  493. )
  494. self.assertEqual(response.status_code, 400)
  495. response_json = response.json()
  496. self.assertEqual(
  497. response_json, {
  498. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  499. }
  500. )
  501. def test_split(self):
  502. """api splits posts to new thread"""
  503. self.refresh_thread()
  504. self.assertEqual(self.thread.replies, 2)
  505. response = self.client.post(
  506. self.api_link,
  507. json.dumps({
  508. 'posts': self.posts,
  509. 'title': 'Split thread.',
  510. 'category': self.category.id,
  511. }),
  512. content_type="application/json",
  513. )
  514. self.assertEqual(response.status_code, 200)
  515. # thread was created
  516. split_thread = self.category.thread_set.get(slug='split-thread')
  517. self.assertEqual(split_thread.replies, 1)
  518. # posts were removed from old thread
  519. self.refresh_thread()
  520. self.assertEqual(self.thread.replies, 0)
  521. # posts were moved to new thread
  522. self.assertEqual(split_thread.post_set.filter(pk__in=self.posts).count(), 2)
  523. def test_split_best_answer(self):
  524. """api splits best answer to new thread"""
  525. best_answer = testutils.reply_thread(self.thread)
  526. self.thread.set_best_answer(self.user, best_answer)
  527. self.thread.synchronize()
  528. self.thread.save()
  529. self.refresh_thread()
  530. self.assertEqual(self.thread.best_answer, best_answer)
  531. self.assertEqual(self.thread.replies, 3)
  532. response = self.client.post(
  533. self.api_link,
  534. json.dumps({
  535. 'posts': [best_answer.pk],
  536. 'title': 'Split thread.',
  537. 'category': self.category.id,
  538. }),
  539. content_type="application/json",
  540. )
  541. self.assertEqual(response.status_code, 200)
  542. # best_answer was moved and unmarked
  543. self.refresh_thread()
  544. self.assertEqual(self.thread.replies, 2)
  545. self.assertIsNone(self.thread.best_answer)
  546. split_thread = self.category.thread_set.get(slug='split-thread')
  547. self.assertEqual(split_thread.replies, 0)
  548. self.assertIsNone(split_thread.best_answer)
  549. def test_split_kitchensink(self):
  550. """api splits posts with kitchensink"""
  551. self.refresh_thread()
  552. self.assertEqual(self.thread.replies, 2)
  553. self.override_other_acl({
  554. 'can_start_threads': 2,
  555. 'can_close_threads': True,
  556. 'can_hide_threads': True,
  557. 'can_pin_threads': 2,
  558. })
  559. poststracker.save_read(self.user, self.thread.first_post)
  560. for post in self.posts:
  561. poststracker.save_read(self.user, Post.objects.select_related().get(pk=post))
  562. response = self.client.post(
  563. self.api_link,
  564. json.dumps({
  565. 'posts': self.posts,
  566. 'title': 'Split thread',
  567. 'category': self.category_b.id,
  568. 'weight': 2,
  569. 'is_closed': 1,
  570. 'is_hidden': 1,
  571. }),
  572. content_type="application/json",
  573. )
  574. self.assertEqual(response.status_code, 200)
  575. # thread was created
  576. split_thread = self.category_b.thread_set.get(slug='split-thread')
  577. self.assertEqual(split_thread.replies, 1)
  578. self.assertEqual(split_thread.weight, 2)
  579. self.assertTrue(split_thread.is_closed)
  580. self.assertTrue(split_thread.is_hidden)
  581. # posts were removed from old thread
  582. self.refresh_thread()
  583. self.assertEqual(self.thread.replies, 0)
  584. # posts were moved to new thread
  585. self.assertEqual(split_thread.post_set.filter(pk__in=self.posts).count(), 2)
  586. # postreads were removed
  587. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  588. postreads_threads = list(postreads.values_list('thread_id', flat=True))
  589. self.assertEqual(postreads_threads, [self.thread.pk])
  590. postreads_categories = list(postreads.values_list('category_id', flat=True))
  591. self.assertEqual(postreads_categories, [self.category.pk])