test_thread_postsplit_api.py 18 KB

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