test_thread_merge_api.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. from django.urls import reverse
  2. from misago.acl.testutils import override_acl
  3. from misago.categories.models import Category
  4. from misago.threads import testutils
  5. from misago.threads.models import Poll, PollVote, Thread
  6. from .test_threads_api import ThreadsApiTestCase
  7. class ThreadMergeApiTests(ThreadsApiTestCase):
  8. def setUp(self):
  9. super(ThreadMergeApiTests, self).setUp()
  10. Category(
  11. name='Category B',
  12. slug='category-b',
  13. ).insert_at(
  14. self.category,
  15. position='last-child',
  16. save=True,
  17. )
  18. self.category_b = Category.objects.get(slug='category-b')
  19. self.api_link = reverse(
  20. 'misago:api:thread-merge', kwargs={
  21. 'pk': self.thread.pk,
  22. }
  23. )
  24. def override_other_acl(self, acl=None):
  25. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  26. other_category_acl.update({
  27. 'can_see': 1,
  28. 'can_browse': 1,
  29. 'can_see_all_threads': 1,
  30. 'can_see_own_threads': 0,
  31. 'can_hide_threads': 0,
  32. 'can_approve_content': 0,
  33. 'can_edit_posts': 0,
  34. 'can_hide_posts': 0,
  35. 'can_hide_own_posts': 0,
  36. 'can_merge_threads': 0,
  37. 'can_close_threads': 0,
  38. })
  39. if acl:
  40. other_category_acl.update(acl)
  41. categories_acl = self.user.acl_cache['categories']
  42. categories_acl[self.category_b.pk] = other_category_acl
  43. visible_categories = [self.category.pk]
  44. if other_category_acl['can_see']:
  45. visible_categories.append(self.category_b.pk)
  46. override_acl(
  47. self.user, {
  48. 'visible_categories': visible_categories,
  49. 'categories': categories_acl,
  50. }
  51. )
  52. def test_merge_no_permission(self):
  53. """api validates if thread can be merged with other one"""
  54. self.override_acl({'can_merge_threads': 0})
  55. response = self.client.post(self.api_link)
  56. self.assertContains(
  57. response,
  58. "You can't merge threads in this category.",
  59. status_code=403
  60. )
  61. def test_merge_no_url(self):
  62. """api validates if thread url was given"""
  63. self.override_acl({'can_merge_threads': 1})
  64. response = self.client.post(self.api_link)
  65. self.assertContains(response, "Enter link to new thread.", status_code=400)
  66. def test_invalid_url(self):
  67. """api validates thread url"""
  68. self.override_acl({'can_merge_threads': 1})
  69. response = self.client.post(self.api_link, {
  70. 'other_thread': self.user.get_absolute_url(),
  71. })
  72. self.assertContains(response, "This is not a valid thread link.", status_code=400)
  73. def test_current_other_thread(self):
  74. """api validates if thread url given is to current thread"""
  75. self.override_acl({'can_merge_threads': 1})
  76. response = self.client.post(
  77. self.api_link, {
  78. 'other_thread': self.thread.get_absolute_url(),
  79. }
  80. )
  81. self.assertContains(response, "You can't merge thread with itself.", status_code=400)
  82. def test_other_thread_exists(self):
  83. """api validates if other thread exists"""
  84. self.override_acl({'can_merge_threads': 1})
  85. self.override_other_acl()
  86. other_thread = testutils.post_thread(self.category_b)
  87. other_other_thread = other_thread.get_absolute_url()
  88. other_thread.delete()
  89. response = self.client.post(self.api_link, {
  90. 'other_thread': other_other_thread,
  91. })
  92. self.assertContains(
  93. response, "The thread you have entered link to doesn't exist", status_code=400
  94. )
  95. def test_other_thread_is_invisible(self):
  96. """api validates if other thread is visible"""
  97. self.override_acl({'can_merge_threads': 1})
  98. self.override_other_acl({'can_see': 0})
  99. other_thread = testutils.post_thread(self.category_b)
  100. response = self.client.post(
  101. self.api_link, {
  102. 'other_thread': other_thread.get_absolute_url(),
  103. }
  104. )
  105. self.assertContains(
  106. response, "The thread you have entered link to doesn't exist", status_code=400
  107. )
  108. def test_other_thread_isnt_mergeable(self):
  109. """api validates if other thread can be merged"""
  110. self.override_acl({'can_merge_threads': 1})
  111. self.override_other_acl({'can_merge_threads': 0})
  112. other_thread = testutils.post_thread(self.category_b)
  113. response = self.client.post(
  114. self.api_link, {
  115. 'other_thread': other_thread.get_absolute_url(),
  116. }
  117. )
  118. self.assertContains(
  119. response, "Other thread can't be merged with.", status_code=400
  120. )
  121. def test_thread_category_is_closed(self):
  122. """api validates if thread's category is open"""
  123. self.override_acl({'can_merge_threads': 1})
  124. self.override_other_acl({
  125. 'can_merge_threads': 1,
  126. 'can_reply_threads': 0,
  127. 'can_close_threads': 0,
  128. })
  129. other_thread = testutils.post_thread(self.category_b)
  130. self.category.is_closed = True
  131. self.category.save()
  132. response = self.client.post(
  133. self.api_link, {
  134. 'other_thread': other_thread.get_absolute_url(),
  135. }
  136. )
  137. self.assertContains(
  138. response,
  139. "This category is closed. You can't merge it's threads.",
  140. status_code=403,
  141. )
  142. def test_thread_is_closed(self):
  143. """api validates if thread is open"""
  144. self.override_acl({'can_merge_threads': 1})
  145. self.override_other_acl({
  146. 'can_merge_threads': 1,
  147. 'can_reply_threads': 0,
  148. 'can_close_threads': 0,
  149. })
  150. other_thread = testutils.post_thread(self.category_b)
  151. self.thread.is_closed = True
  152. self.thread.save()
  153. response = self.client.post(
  154. self.api_link, {
  155. 'other_thread': other_thread.get_absolute_url(),
  156. }
  157. )
  158. self.assertContains(
  159. response,
  160. "This thread is closed. You can't merge it with other threads.",
  161. status_code=403,
  162. )
  163. def test_other_thread_category_is_closed(self):
  164. """api validates if other thread's category is open"""
  165. self.override_acl({'can_merge_threads': 1})
  166. self.override_other_acl({
  167. 'can_merge_threads': 1,
  168. 'can_reply_threads': 0,
  169. 'can_close_threads': 0,
  170. })
  171. other_thread = testutils.post_thread(self.category_b)
  172. self.category_b.is_closed = True
  173. self.category_b.save()
  174. response = self.client.post(
  175. self.api_link, {
  176. 'other_thread': other_thread.get_absolute_url(),
  177. }
  178. )
  179. self.assertContains(
  180. response, "Other thread's category is closed. You can't merge with it.", status_code=400
  181. )
  182. def test_other_thread_is_closed(self):
  183. """api validates if other thread is open"""
  184. self.override_acl({'can_merge_threads': 1})
  185. self.override_other_acl({
  186. 'can_merge_threads': 1,
  187. 'can_reply_threads': 0,
  188. 'can_close_threads': 0,
  189. })
  190. other_thread = testutils.post_thread(self.category_b)
  191. other_thread.is_closed = True
  192. other_thread.save()
  193. response = self.client.post(
  194. self.api_link, {
  195. 'other_thread': other_thread.get_absolute_url(),
  196. }
  197. )
  198. self.assertContains(
  199. response, "Other thread is closed and can't be merged with", status_code=400
  200. )
  201. def test_other_thread_isnt_replyable(self):
  202. """api validates if other thread can be replied, which is condition for merge"""
  203. self.override_acl({'can_merge_threads': 1})
  204. self.override_other_acl({
  205. 'can_merge_threads': 1,
  206. 'can_reply_threads': 0,
  207. })
  208. other_thread = testutils.post_thread(self.category_b)
  209. response = self.client.post(
  210. self.api_link, {
  211. 'other_thread': other_thread.get_absolute_url(),
  212. }
  213. )
  214. self.assertContains(
  215. response, "You can't merge this thread into thread you can't reply.", status_code=400
  216. )
  217. def test_merge_threads(self):
  218. """api merges two threads successfully"""
  219. self.override_acl({'can_merge_threads': 1})
  220. self.override_other_acl({'can_merge_threads': 1})
  221. other_thread = testutils.post_thread(self.category_b)
  222. response = self.client.post(
  223. self.api_link, {
  224. 'other_thread': other_thread.get_absolute_url(),
  225. }
  226. )
  227. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  228. # other thread has two posts now
  229. self.assertEqual(other_thread.post_set.count(), 3)
  230. # first thread is gone
  231. with self.assertRaises(Thread.DoesNotExist):
  232. Thread.objects.get(pk=self.thread.pk)
  233. def test_merge_threads_kept_poll(self):
  234. """api merges two threads successfully, keeping poll from old thread"""
  235. self.override_acl({'can_merge_threads': 1})
  236. self.override_other_acl({'can_merge_threads': 1})
  237. other_thread = testutils.post_thread(self.category_b)
  238. poll = testutils.post_poll(other_thread, self.user)
  239. response = self.client.post(
  240. self.api_link, {
  241. 'other_thread': other_thread.get_absolute_url(),
  242. }
  243. )
  244. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  245. # other thread has two posts now
  246. self.assertEqual(other_thread.post_set.count(), 3)
  247. # first thread is gone
  248. with self.assertRaises(Thread.DoesNotExist):
  249. Thread.objects.get(pk=self.thread.pk)
  250. # poll and its votes were kept
  251. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  252. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  253. def test_merge_threads_moved_poll(self):
  254. """api merges two threads successfully, moving poll from other thread"""
  255. self.override_acl({'can_merge_threads': 1})
  256. self.override_other_acl({'can_merge_threads': 1})
  257. other_thread = testutils.post_thread(self.category_b)
  258. poll = testutils.post_poll(self.thread, self.user)
  259. response = self.client.post(
  260. self.api_link, {
  261. 'other_thread': other_thread.get_absolute_url(),
  262. }
  263. )
  264. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  265. # other thread has two posts now
  266. self.assertEqual(other_thread.post_set.count(), 3)
  267. # first thread is gone
  268. with self.assertRaises(Thread.DoesNotExist):
  269. Thread.objects.get(pk=self.thread.pk)
  270. # poll and its votes were moved
  271. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  272. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  273. def test_threads_merge_conflict(self):
  274. """api errors on merge conflict, returning list of available polls"""
  275. self.override_acl({'can_merge_threads': 1})
  276. self.override_other_acl({'can_merge_threads': 1})
  277. other_thread = testutils.post_thread(self.category_b)
  278. poll = testutils.post_poll(self.thread, self.user)
  279. other_poll = testutils.post_poll(other_thread, self.user)
  280. response = self.client.post(
  281. self.api_link, {
  282. 'other_thread': other_thread.get_absolute_url(),
  283. }
  284. )
  285. self.assertEqual(response.status_code, 400)
  286. self.assertEqual(
  287. response.json(), {
  288. 'polls': [
  289. [0, "Delete all polls"],
  290. [poll.pk, poll.question],
  291. [other_poll.pk, other_poll.question],
  292. ]
  293. }
  294. )
  295. # polls and votes were untouched
  296. self.assertEqual(Poll.objects.count(), 2)
  297. self.assertEqual(PollVote.objects.count(), 8)
  298. def test_threads_merge_conflict_invalid_resolution(self):
  299. """api errors on invalid merge conflict resolution"""
  300. self.override_acl({'can_merge_threads': 1})
  301. self.override_other_acl({'can_merge_threads': 1})
  302. other_thread = testutils.post_thread(self.category_b)
  303. testutils.post_poll(self.thread, self.user)
  304. testutils.post_poll(other_thread, self.user)
  305. response = self.client.post(
  306. self.api_link, {
  307. 'other_thread': other_thread.get_absolute_url(),
  308. 'poll': 'jhdkajshdsak',
  309. }
  310. )
  311. self.assertEqual(response.status_code, 400)
  312. self.assertEqual(response.json(), {'detail': "Invalid choice."})
  313. # polls and votes were untouched
  314. self.assertEqual(Poll.objects.count(), 2)
  315. self.assertEqual(PollVote.objects.count(), 8)
  316. def test_threads_merge_conflict_delete_all(self):
  317. """api deletes all polls when delete all choice is selected"""
  318. self.override_acl({'can_merge_threads': 1})
  319. self.override_other_acl({'can_merge_threads': 1})
  320. other_thread = testutils.post_thread(self.category_b)
  321. testutils.post_poll(self.thread, self.user)
  322. testutils.post_poll(other_thread, self.user)
  323. response = self.client.post(
  324. self.api_link, {
  325. 'other_thread': other_thread.get_absolute_url(),
  326. 'poll': 0,
  327. }
  328. )
  329. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  330. # other thread has two posts now
  331. self.assertEqual(other_thread.post_set.count(), 3)
  332. # first thread is gone
  333. with self.assertRaises(Thread.DoesNotExist):
  334. Thread.objects.get(pk=self.thread.pk)
  335. # polls and votes are gone
  336. self.assertEqual(Poll.objects.count(), 0)
  337. self.assertEqual(PollVote.objects.count(), 0)
  338. def test_threads_merge_conflict_keep_first_poll(self):
  339. """api deletes other poll on merge"""
  340. self.override_acl({'can_merge_threads': 1})
  341. self.override_other_acl({'can_merge_threads': 1})
  342. other_thread = testutils.post_thread(self.category_b)
  343. poll = testutils.post_poll(self.thread, self.user)
  344. other_poll = testutils.post_poll(other_thread, self.user)
  345. response = self.client.post(
  346. self.api_link, {
  347. 'other_thread': other_thread.get_absolute_url(),
  348. 'poll': poll.pk,
  349. }
  350. )
  351. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  352. # other thread has two posts now
  353. self.assertEqual(other_thread.post_set.count(), 3)
  354. # first thread is gone
  355. with self.assertRaises(Thread.DoesNotExist):
  356. Thread.objects.get(pk=self.thread.pk)
  357. # other poll and its votes are gone
  358. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  359. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  360. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  361. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  362. Poll.objects.get(pk=poll.pk)
  363. with self.assertRaises(Poll.DoesNotExist):
  364. Poll.objects.get(pk=other_poll.pk)
  365. def test_threads_merge_conflict_keep_other_poll(self):
  366. """api deletes first poll on merge"""
  367. self.override_acl({'can_merge_threads': 1})
  368. self.override_other_acl({'can_merge_threads': 1})
  369. other_thread = testutils.post_thread(self.category_b)
  370. poll = testutils.post_poll(self.thread, self.user)
  371. other_poll = testutils.post_poll(other_thread, self.user)
  372. response = self.client.post(
  373. self.api_link, {
  374. 'other_thread': other_thread.get_absolute_url(),
  375. 'poll': other_poll.pk,
  376. }
  377. )
  378. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  379. # other thread has two posts now
  380. self.assertEqual(other_thread.post_set.count(), 3)
  381. # first thread is gone
  382. with self.assertRaises(Thread.DoesNotExist):
  383. Thread.objects.get(pk=self.thread.pk)
  384. # other poll and its votes are gone
  385. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  386. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  387. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  388. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  389. Poll.objects.get(pk=other_poll.pk)
  390. with self.assertRaises(Poll.DoesNotExist):
  391. Poll.objects.get(pk=poll.pk)