test_thread_merge_api.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. from django.urls import reverse
  2. from misago.acl.testutils import override_acl
  3. from misago.categories.models import Category
  4. from misago.readtracker import poststracker
  5. from misago.threads import testutils
  6. from misago.threads.models import Poll, PollVote, Thread
  7. from .test_threads_api import ThreadsApiTestCase
  8. class ThreadMergeApiTests(ThreadsApiTestCase):
  9. def setUp(self):
  10. super(ThreadMergeApiTests, self).setUp()
  11. Category(
  12. name='Category B',
  13. slug='category-b',
  14. ).insert_at(
  15. self.category,
  16. position='last-child',
  17. save=True,
  18. )
  19. self.category_b = Category.objects.get(slug='category-b')
  20. self.api_link = reverse(
  21. 'misago:api:thread-merge', kwargs={
  22. 'pk': self.thread.pk,
  23. }
  24. )
  25. def override_other_acl(self, acl=None):
  26. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  27. other_category_acl.update({
  28. 'can_see': 1,
  29. 'can_browse': 1,
  30. 'can_see_all_threads': 1,
  31. 'can_see_own_threads': 0,
  32. 'can_hide_threads': 0,
  33. 'can_approve_content': 0,
  34. 'can_edit_posts': 0,
  35. 'can_hide_posts': 0,
  36. 'can_hide_own_posts': 0,
  37. 'can_merge_threads': 0,
  38. 'can_close_threads': 0,
  39. })
  40. if acl:
  41. other_category_acl.update(acl)
  42. categories_acl = self.user.acl_cache['categories']
  43. categories_acl[self.category_b.pk] = other_category_acl
  44. visible_categories = [self.category.pk]
  45. if other_category_acl['can_see']:
  46. visible_categories.append(self.category_b.pk)
  47. override_acl(
  48. self.user, {
  49. 'visible_categories': visible_categories,
  50. 'categories': categories_acl,
  51. }
  52. )
  53. def test_merge_no_permission(self):
  54. """api validates if thread can be merged with other one"""
  55. self.override_acl({'can_merge_threads': 0})
  56. response = self.client.post(self.api_link)
  57. self.assertContains(response, "You can't merge threads in this category.", status_code=403)
  58. def test_merge_no_url(self):
  59. """api validates if thread url was given"""
  60. self.override_acl({'can_merge_threads': 1})
  61. response = self.client.post(self.api_link)
  62. self.assertEqual(response.status_code, 400)
  63. self.assertEqual(response.json(), {'other_thread': ["Enter link to new thread."]})
  64. def test_invalid_url(self):
  65. """api validates thread url"""
  66. self.override_acl({'can_merge_threads': 1})
  67. response = self.client.post(
  68. self.api_link, {
  69. 'other_thread': self.user.get_absolute_url(),
  70. }
  71. )
  72. self.assertEqual(response.status_code, 400)
  73. self.assertEqual(response.json(), {'other_thread': ["This is not a valid thread link."]})
  74. def test_current_other_thread(self):
  75. """api validates if thread url given is to current thread"""
  76. self.override_acl({'can_merge_threads': 1})
  77. response = self.client.post(
  78. self.api_link, {
  79. 'other_thread': self.thread.get_absolute_url(),
  80. }
  81. )
  82. self.assertEqual(response.status_code, 400)
  83. self.assertEqual(
  84. response.json(), {'other_thread': ["You can't merge thread with itself."]}
  85. )
  86. def test_other_thread_exists(self):
  87. """api validates if other thread exists"""
  88. self.override_acl({'can_merge_threads': 1})
  89. self.override_other_acl()
  90. other_thread = testutils.post_thread(self.category_b)
  91. other_other_thread = other_thread.get_absolute_url()
  92. other_thread.delete()
  93. response = self.client.post(self.api_link, {
  94. 'other_thread': other_other_thread,
  95. })
  96. self.assertEqual(response.status_code, 400)
  97. self.assertEqual(
  98. response.json(),
  99. {'other_thread': [
  100. "The thread you have entered link to doesn't exist or "
  101. "you don't have permission to see it."
  102. ]},
  103. )
  104. def test_other_thread_is_invisible(self):
  105. """api validates if other thread is visible"""
  106. self.override_acl({'can_merge_threads': 1})
  107. self.override_other_acl({'can_see': 0})
  108. other_thread = testutils.post_thread(self.category_b)
  109. response = self.client.post(
  110. self.api_link, {
  111. 'other_thread': other_thread.get_absolute_url(),
  112. }
  113. )
  114. self.assertEqual(response.status_code, 400)
  115. self.assertEqual(
  116. response.json(),
  117. {'other_thread': [
  118. "The thread you have entered link to doesn't exist or "
  119. "you don't have permission to see it."
  120. ]},
  121. )
  122. def test_other_thread_isnt_mergeable(self):
  123. """api validates if other thread can be merged"""
  124. self.override_acl({'can_merge_threads': 1})
  125. self.override_other_acl({'can_merge_threads': 0})
  126. other_thread = testutils.post_thread(self.category_b)
  127. response = self.client.post(
  128. self.api_link, {
  129. 'other_thread': other_thread.get_absolute_url(),
  130. }
  131. )
  132. self.assertEqual(response.status_code, 400)
  133. self.assertEqual(
  134. response.json(), {'other_thread': ["Other thread can't be merged with."]}
  135. )
  136. def test_thread_category_is_closed(self):
  137. """api validates if thread's category is open"""
  138. self.override_acl({'can_merge_threads': 1})
  139. self.override_other_acl({
  140. 'can_merge_threads': 1,
  141. 'can_reply_threads': 0,
  142. 'can_close_threads': 0,
  143. })
  144. other_thread = testutils.post_thread(self.category_b)
  145. self.category.is_closed = True
  146. self.category.save()
  147. response = self.client.post(
  148. self.api_link, {
  149. 'other_thread': other_thread.get_absolute_url(),
  150. }
  151. )
  152. self.assertEqual(response.status_code, 403)
  153. self.assertEqual(
  154. response.json(), {'detail': "This category is closed. You can't merge it's threads."},
  155. )
  156. def test_thread_is_closed(self):
  157. """api validates if thread is open"""
  158. self.override_acl({'can_merge_threads': 1})
  159. self.override_other_acl({
  160. 'can_merge_threads': 1,
  161. 'can_reply_threads': 0,
  162. 'can_close_threads': 0,
  163. })
  164. other_thread = testutils.post_thread(self.category_b)
  165. self.thread.is_closed = True
  166. self.thread.save()
  167. response = self.client.post(
  168. self.api_link, {
  169. 'other_thread': other_thread.get_absolute_url(),
  170. }
  171. )
  172. self.assertEqual(response.status_code, 403)
  173. self.assertEqual(
  174. response.json(),
  175. {'detail': "This thread is closed. You can't merge it with other threads."},
  176. )
  177. def test_other_thread_category_is_closed(self):
  178. """api validates if other thread's category is open"""
  179. self.override_acl({'can_merge_threads': 1})
  180. self.override_other_acl({
  181. 'can_merge_threads': 1,
  182. 'can_reply_threads': 0,
  183. 'can_close_threads': 0,
  184. })
  185. other_thread = testutils.post_thread(self.category_b)
  186. self.category_b.is_closed = True
  187. self.category_b.save()
  188. response = self.client.post(
  189. self.api_link, {
  190. 'other_thread': other_thread.get_absolute_url(),
  191. }
  192. )
  193. self.assertEqual(response.status_code, 400)
  194. self.assertEqual(
  195. response.json(),
  196. {'other_thread': ["Other thread's category is closed. You can't merge with it."]},
  197. )
  198. def test_other_thread_is_closed(self):
  199. """api validates if other thread is open"""
  200. self.override_acl({'can_merge_threads': 1})
  201. self.override_other_acl({
  202. 'can_merge_threads': 1,
  203. 'can_reply_threads': 0,
  204. 'can_close_threads': 0,
  205. })
  206. other_thread = testutils.post_thread(self.category_b)
  207. other_thread.is_closed = True
  208. other_thread.save()
  209. response = self.client.post(
  210. self.api_link, {
  211. 'other_thread': other_thread.get_absolute_url(),
  212. }
  213. )
  214. self.assertEqual(response.status_code, 400)
  215. self.assertEqual(
  216. response.json(),
  217. {'other_thread': ["Other thread is closed and can't be merged with."]},
  218. )
  219. def test_other_thread_isnt_replyable(self):
  220. """api validates if other thread can be replied, which is condition for merge"""
  221. self.override_acl({'can_merge_threads': 1})
  222. self.override_other_acl({
  223. 'can_merge_threads': 1,
  224. 'can_reply_threads': 0,
  225. })
  226. other_thread = testutils.post_thread(self.category_b)
  227. response = self.client.post(
  228. self.api_link, {
  229. 'other_thread': other_thread.get_absolute_url(),
  230. }
  231. )
  232. self.assertEqual(response.status_code, 400)
  233. self.assertEqual(
  234. response.json(),
  235. {'other_thread': ["You can't merge this thread into thread you can't reply."]},
  236. )
  237. def test_merge_threads(self):
  238. """api merges two threads successfully"""
  239. self.override_acl({'can_merge_threads': 1})
  240. self.override_other_acl({'can_merge_threads': 1})
  241. other_thread = testutils.post_thread(self.category_b)
  242. response = self.client.post(
  243. self.api_link, {
  244. 'other_thread': other_thread.get_absolute_url(),
  245. }
  246. )
  247. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  248. # other thread has two posts now
  249. self.assertEqual(other_thread.post_set.count(), 3)
  250. # first thread is gone
  251. with self.assertRaises(Thread.DoesNotExist):
  252. Thread.objects.get(pk=self.thread.pk)
  253. def test_merge_threads_kept_reads(self):
  254. """api keeps both threads readtrackers after merge"""
  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. poststracker.save_read(self.user, self.thread.first_post)
  259. poststracker.save_read(self.user, other_thread.first_post)
  260. response = self.client.post(
  261. self.api_link, {
  262. 'other_thread': other_thread.get_absolute_url(),
  263. }
  264. )
  265. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  266. # posts reads are kept
  267. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  268. self.assertEqual(
  269. list(postreads.values_list('post_id', flat=True)),
  270. [self.thread.first_post_id, other_thread.first_post_id]
  271. )
  272. self.assertEqual(postreads.filter(thread=other_thread).count(), 2)
  273. self.assertEqual(postreads.filter(category=self.category_b).count(), 2)
  274. def test_merge_threads_kept_subs(self):
  275. """api keeps other thread's subscription after merge"""
  276. self.override_acl({'can_merge_threads': 1})
  277. self.override_other_acl({'can_merge_threads': 1})
  278. other_thread = testutils.post_thread(self.category_b)
  279. self.user.subscription_set.create(
  280. thread=self.thread,
  281. category=self.thread.category,
  282. last_read_on=self.thread.last_post_on,
  283. send_email=False,
  284. )
  285. self.assertEqual(self.user.subscription_set.count(), 1)
  286. self.user.subscription_set.get(thread=self.thread)
  287. self.user.subscription_set.get(category=self.category)
  288. response = self.client.post(
  289. self.api_link, {
  290. 'other_thread': other_thread.get_absolute_url(),
  291. }
  292. )
  293. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  294. # subscriptions are kept
  295. self.assertEqual(self.user.subscription_set.count(), 1)
  296. self.user.subscription_set.get(thread=other_thread)
  297. self.user.subscription_set.get(category=self.category_b)
  298. def test_merge_threads_moved_subs(self):
  299. """api keeps other thread's subscription after merge"""
  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. self.user.subscription_set.create(
  304. thread=other_thread,
  305. category=other_thread.category,
  306. last_read_on=other_thread.last_post_on,
  307. send_email=False,
  308. )
  309. self.assertEqual(self.user.subscription_set.count(), 1)
  310. self.user.subscription_set.get(thread=other_thread)
  311. self.user.subscription_set.get(category=self.category_b)
  312. response = self.client.post(
  313. self.api_link, {
  314. 'other_thread': other_thread.get_absolute_url(),
  315. }
  316. )
  317. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  318. # subscriptions are kept
  319. self.assertEqual(self.user.subscription_set.count(), 1)
  320. self.user.subscription_set.get(thread=other_thread)
  321. self.user.subscription_set.get(category=self.category_b)
  322. def test_merge_threads_handle_subs_colision(self):
  323. """api resolves conflicting thread subscriptions after merge"""
  324. self.override_acl({'can_merge_threads': 1})
  325. self.override_other_acl({'can_merge_threads': 1})
  326. self.user.subscription_set.create(
  327. thread=self.thread,
  328. category=self.thread.category,
  329. last_read_on=self.thread.last_post_on,
  330. send_email=False,
  331. )
  332. other_thread = testutils.post_thread(self.category_b)
  333. self.user.subscription_set.create(
  334. thread=other_thread,
  335. category=other_thread.category,
  336. last_read_on=other_thread.last_post_on,
  337. send_email=False,
  338. )
  339. self.assertEqual(self.user.subscription_set.count(), 2)
  340. self.user.subscription_set.get(thread=self.thread)
  341. self.user.subscription_set.get(category=self.category)
  342. self.user.subscription_set.get(thread=other_thread)
  343. self.user.subscription_set.get(category=self.category_b)
  344. response = self.client.post(
  345. self.api_link, {
  346. 'other_thread': other_thread.get_absolute_url(),
  347. }
  348. )
  349. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  350. # subscriptions are kept
  351. self.assertEqual(self.user.subscription_set.count(), 1)
  352. self.user.subscription_set.get(thread=other_thread)
  353. self.user.subscription_set.get(category=self.category_b)
  354. def test_merge_threads_kept_poll(self):
  355. """api merges two threads successfully, keeping poll from old thread"""
  356. self.override_acl({'can_merge_threads': 1})
  357. self.override_other_acl({'can_merge_threads': 1})
  358. other_thread = testutils.post_thread(self.category_b)
  359. poll = testutils.post_poll(other_thread, self.user)
  360. response = self.client.post(
  361. self.api_link, {
  362. 'other_thread': other_thread.get_absolute_url(),
  363. }
  364. )
  365. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  366. # other thread has two posts now
  367. self.assertEqual(other_thread.post_set.count(), 3)
  368. # first thread is gone
  369. with self.assertRaises(Thread.DoesNotExist):
  370. Thread.objects.get(pk=self.thread.pk)
  371. # poll and its votes were kept
  372. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  373. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  374. def test_merge_threads_moved_poll(self):
  375. """api merges two threads successfully, moving poll from other thread"""
  376. self.override_acl({'can_merge_threads': 1})
  377. self.override_other_acl({'can_merge_threads': 1})
  378. other_thread = testutils.post_thread(self.category_b)
  379. poll = testutils.post_poll(self.thread, self.user)
  380. response = self.client.post(
  381. self.api_link, {
  382. 'other_thread': other_thread.get_absolute_url(),
  383. }
  384. )
  385. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  386. # other thread has two posts now
  387. self.assertEqual(other_thread.post_set.count(), 3)
  388. # first thread is gone
  389. with self.assertRaises(Thread.DoesNotExist):
  390. Thread.objects.get(pk=self.thread.pk)
  391. # poll and its votes were moved
  392. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  393. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  394. def test_threads_merge_conflict(self):
  395. """api errors on merge conflict, returning list of available polls"""
  396. self.override_acl({'can_merge_threads': 1})
  397. self.override_other_acl({'can_merge_threads': 1})
  398. other_thread = testutils.post_thread(self.category_b)
  399. poll = testutils.post_poll(self.thread, self.user)
  400. other_poll = testutils.post_poll(other_thread, self.user)
  401. response = self.client.post(
  402. self.api_link, {
  403. 'other_thread': other_thread.get_absolute_url(),
  404. }
  405. )
  406. self.assertEqual(response.status_code, 400)
  407. self.assertEqual(
  408. response.json(), {
  409. 'polls': [
  410. [0, "Delete all polls"],
  411. [poll.pk, poll.question],
  412. [other_poll.pk, other_poll.question],
  413. ]
  414. }
  415. )
  416. # polls and votes were untouched
  417. self.assertEqual(Poll.objects.count(), 2)
  418. self.assertEqual(PollVote.objects.count(), 8)
  419. def test_threads_merge_conflict_invalid_resolution(self):
  420. """api errors on invalid merge conflict resolution"""
  421. self.override_acl({'can_merge_threads': 1})
  422. self.override_other_acl({'can_merge_threads': 1})
  423. other_thread = testutils.post_thread(self.category_b)
  424. testutils.post_poll(self.thread, self.user)
  425. testutils.post_poll(other_thread, self.user)
  426. response = self.client.post(
  427. self.api_link, {
  428. 'other_thread': other_thread.get_absolute_url(),
  429. 'poll': 'jhdkajshdsak',
  430. }
  431. )
  432. self.assertEqual(response.status_code, 400)
  433. self.assertEqual(response.json(), {'poll': ["Invalid choice."]})
  434. # polls and votes were untouched
  435. self.assertEqual(Poll.objects.count(), 2)
  436. self.assertEqual(PollVote.objects.count(), 8)
  437. def test_threads_merge_conflict_delete_all(self):
  438. """api deletes all polls when delete all choice is selected"""
  439. self.override_acl({'can_merge_threads': 1})
  440. self.override_other_acl({'can_merge_threads': 1})
  441. other_thread = testutils.post_thread(self.category_b)
  442. testutils.post_poll(self.thread, self.user)
  443. testutils.post_poll(other_thread, self.user)
  444. response = self.client.post(
  445. self.api_link, {
  446. 'other_thread': other_thread.get_absolute_url(),
  447. 'poll': 0,
  448. }
  449. )
  450. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  451. # other thread has two posts now
  452. self.assertEqual(other_thread.post_set.count(), 3)
  453. # first thread is gone
  454. with self.assertRaises(Thread.DoesNotExist):
  455. Thread.objects.get(pk=self.thread.pk)
  456. # polls and votes are gone
  457. self.assertEqual(Poll.objects.count(), 0)
  458. self.assertEqual(PollVote.objects.count(), 0)
  459. def test_threads_merge_conflict_keep_first_poll(self):
  460. """api deletes other poll on merge"""
  461. self.override_acl({'can_merge_threads': 1})
  462. self.override_other_acl({'can_merge_threads': 1})
  463. other_thread = testutils.post_thread(self.category_b)
  464. poll = testutils.post_poll(self.thread, self.user)
  465. other_poll = testutils.post_poll(other_thread, self.user)
  466. response = self.client.post(
  467. self.api_link, {
  468. 'other_thread': other_thread.get_absolute_url(),
  469. 'poll': poll.pk,
  470. }
  471. )
  472. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  473. # other thread has two posts now
  474. self.assertEqual(other_thread.post_set.count(), 3)
  475. # first thread is gone
  476. with self.assertRaises(Thread.DoesNotExist):
  477. Thread.objects.get(pk=self.thread.pk)
  478. # other poll and its votes are gone
  479. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  480. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  481. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  482. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  483. Poll.objects.get(pk=poll.pk)
  484. with self.assertRaises(Poll.DoesNotExist):
  485. Poll.objects.get(pk=other_poll.pk)
  486. def test_threads_merge_conflict_keep_other_poll(self):
  487. """api deletes first poll on merge"""
  488. self.override_acl({'can_merge_threads': 1})
  489. self.override_other_acl({'can_merge_threads': 1})
  490. other_thread = testutils.post_thread(self.category_b)
  491. poll = testutils.post_poll(self.thread, self.user)
  492. other_poll = testutils.post_poll(other_thread, self.user)
  493. response = self.client.post(
  494. self.api_link, {
  495. 'other_thread': other_thread.get_absolute_url(),
  496. 'poll': other_poll.pk,
  497. }
  498. )
  499. self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
  500. # other thread has two posts now
  501. self.assertEqual(other_thread.post_set.count(), 3)
  502. # first thread is gone
  503. with self.assertRaises(Thread.DoesNotExist):
  504. Thread.objects.get(pk=self.thread.pk)
  505. # other poll and its votes are gone
  506. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  507. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  508. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  509. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  510. Poll.objects.get(pk=other_poll.pk)
  511. with self.assertRaises(Poll.DoesNotExist):
  512. Poll.objects.get(pk=poll.pk)