test_thread_postsplit_api.py 17 KB

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