test_thread_postsplit_api.py 17 KB

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