test_threads_merge_api.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. import json
  2. from django.urls import reverse
  3. from misago.acl import add_acl
  4. from misago.categories.models import Category
  5. from misago.threads import testutils
  6. from misago.threads.api.threadendpoints.merge import MERGE_LIMIT
  7. from misago.threads.models import Poll, PollVote, Post, Thread
  8. from misago.threads.serializers import ThreadsListSerializer
  9. from .test_threads_api import ThreadsApiTestCase
  10. class ThreadsMergeApiTests(ThreadsApiTestCase):
  11. def setUp(self):
  12. super(ThreadsMergeApiTests, self).setUp()
  13. self.api_link = reverse('misago:api:thread-merge')
  14. Category(
  15. name='Category B',
  16. slug='category-b',
  17. ).insert_at(
  18. self.category,
  19. position='last-child',
  20. save=True,
  21. )
  22. self.category_b = Category.objects.get(slug='category-b')
  23. def test_merge_no_threads(self):
  24. """api validates if we are trying to merge no threads"""
  25. response = self.client.post(self.api_link, content_type="application/json")
  26. self.assertEqual(response.status_code, 403)
  27. response_json = response.json()
  28. self.assertEqual(
  29. response_json, {
  30. 'detail': "You have to select at least two threads to merge.",
  31. }
  32. )
  33. def test_merge_empty_threads(self):
  34. """api validates if we are trying to empty threads list"""
  35. response = self.client.post(
  36. self.api_link,
  37. json.dumps({
  38. 'threads': [],
  39. }),
  40. content_type="application/json",
  41. )
  42. self.assertEqual(response.status_code, 403)
  43. response_json = response.json()
  44. self.assertEqual(
  45. response_json, {
  46. 'detail': "You have to select at least two threads to merge.",
  47. }
  48. )
  49. def test_merge_invalid_threads(self):
  50. """api validates if we are trying to merge invalid thread ids"""
  51. response = self.client.post(
  52. self.api_link,
  53. json.dumps({
  54. 'threads': 'abcd',
  55. }),
  56. content_type="application/json",
  57. )
  58. self.assertEqual(response.status_code, 403)
  59. response_json = response.json()
  60. self.assertEqual(
  61. response_json, {
  62. 'detail': "One or more thread ids received were invalid.",
  63. }
  64. )
  65. response = self.client.post(
  66. self.api_link,
  67. json.dumps({
  68. 'threads': ['a', '-', 'c'],
  69. }),
  70. content_type="application/json",
  71. )
  72. self.assertEqual(response.status_code, 403)
  73. response_json = response.json()
  74. self.assertEqual(
  75. response_json, {
  76. 'detail': "One or more thread ids received were invalid.",
  77. }
  78. )
  79. def test_merge_single_thread(self):
  80. """api validates if we are trying to merge single thread"""
  81. response = self.client.post(
  82. self.api_link,
  83. json.dumps({
  84. 'threads': [self.thread.id],
  85. }),
  86. content_type="application/json",
  87. )
  88. self.assertEqual(response.status_code, 403)
  89. response_json = response.json()
  90. self.assertEqual(
  91. response_json, {
  92. 'detail': "You have to select at least two threads to merge.",
  93. }
  94. )
  95. def test_merge_with_nonexisting_thread(self):
  96. """api validates if we are trying to merge with invalid thread"""
  97. response = self.client.post(
  98. self.api_link,
  99. json.dumps({
  100. 'threads': [self.thread.id, self.thread.id + 1000],
  101. }),
  102. content_type="application/json",
  103. )
  104. self.assertEqual(response.status_code, 403)
  105. response_json = response.json()
  106. self.assertEqual(
  107. response_json, {
  108. 'detail': "One or more threads to merge could not be found.",
  109. }
  110. )
  111. def test_merge_with_invisible_thread(self):
  112. """api validates if we are trying to merge with inaccesible thread"""
  113. unaccesible_thread = testutils.post_thread(category=self.category_b)
  114. response = self.client.post(
  115. self.api_link,
  116. json.dumps({
  117. 'threads': [self.thread.id, unaccesible_thread.id],
  118. }),
  119. content_type="application/json",
  120. )
  121. self.assertEqual(response.status_code, 403)
  122. response_json = response.json()
  123. self.assertEqual(
  124. response_json, {
  125. 'detail': "One or more threads to merge could not be found.",
  126. }
  127. )
  128. def test_merge_no_permission(self):
  129. """api validates permission to merge threads"""
  130. thread = testutils.post_thread(category=self.category)
  131. response = self.client.post(
  132. self.api_link,
  133. json.dumps({
  134. 'threads': [self.thread.id, thread.id],
  135. }),
  136. content_type="application/json",
  137. )
  138. self.assertEqual(response.status_code, 403)
  139. response_json = response.json()
  140. self.assertEqual(
  141. response_json, [
  142. {
  143. 'id': thread.pk,
  144. 'title': thread.title,
  145. 'errors': ["You can't merge threads in this category."],
  146. },
  147. {
  148. 'id': self.thread.pk,
  149. 'title': self.thread.title,
  150. 'errors': ["You can't merge threads in this category."],
  151. },
  152. ]
  153. )
  154. def test_thread_category_is_closed(self):
  155. """api validates if thread's category is open"""
  156. self.override_acl({
  157. 'can_merge_threads': 1,
  158. 'can_close_threads': 0,
  159. })
  160. other_thread = testutils.post_thread(self.category)
  161. self.category.is_closed = True
  162. self.category.save()
  163. response = self.client.post(
  164. self.api_link,
  165. json.dumps({
  166. 'threads': [self.thread.id, other_thread.id],
  167. }),
  168. content_type="application/json",
  169. )
  170. self.assertContains(
  171. response,
  172. "This category is closed. You can't merge it's threads.",
  173. status_code=403,
  174. )
  175. def test_thread_is_closed(self):
  176. """api validates if thread is open"""
  177. self.override_acl({
  178. 'can_merge_threads': 1,
  179. 'can_close_threads': 0,
  180. })
  181. other_thread = testutils.post_thread(self.category)
  182. other_thread.is_closed = True
  183. other_thread.save()
  184. response = self.client.post(
  185. self.api_link,
  186. json.dumps({
  187. 'threads': [self.thread.id, other_thread.id],
  188. }),
  189. content_type="application/json",
  190. )
  191. self.assertContains(
  192. response,
  193. "This thread is closed. You can't merge it with other threads.",
  194. status_code=403,
  195. )
  196. def test_merge_too_many_threads(self):
  197. """api rejects too many threads to merge"""
  198. threads = []
  199. for _ in range(MERGE_LIMIT + 1):
  200. threads.append(testutils.post_thread(category=self.category).pk)
  201. self.override_acl({
  202. 'can_merge_threads': True,
  203. 'can_close_threads': False,
  204. 'can_edit_threads': False,
  205. 'can_reply_threads': False,
  206. })
  207. response = self.client.post(
  208. self.api_link,
  209. json.dumps({
  210. 'threads': threads,
  211. }),
  212. content_type="application/json",
  213. )
  214. self.assertEqual(response.status_code, 403)
  215. response_json = response.json()
  216. self.assertEqual(
  217. response_json, {
  218. 'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT,
  219. }
  220. )
  221. def test_merge_no_final_thread(self):
  222. """api rejects merge because no data to merge threads was specified"""
  223. self.override_acl({
  224. 'can_merge_threads': True,
  225. 'can_close_threads': False,
  226. 'can_edit_threads': False,
  227. 'can_reply_threads': False,
  228. })
  229. thread = testutils.post_thread(category=self.category)
  230. response = self.client.post(
  231. self.api_link,
  232. json.dumps({
  233. 'threads': [self.thread.id, thread.id],
  234. }),
  235. content_type="application/json",
  236. )
  237. self.assertEqual(response.status_code, 400)
  238. response_json = response.json()
  239. self.assertEqual(
  240. response_json, {
  241. 'title': ['This field is required.'],
  242. 'category': ['This field is required.'],
  243. }
  244. )
  245. def test_merge_invalid_final_title(self):
  246. """api rejects merge because final thread title was invalid"""
  247. self.override_acl({
  248. 'can_merge_threads': True,
  249. 'can_close_threads': False,
  250. 'can_edit_threads': False,
  251. 'can_reply_threads': False,
  252. })
  253. thread = testutils.post_thread(category=self.category)
  254. response = self.client.post(
  255. self.api_link,
  256. json.dumps({
  257. 'threads': [self.thread.id, thread.id],
  258. 'title': '$$$',
  259. 'category': self.category.id,
  260. }),
  261. content_type="application/json",
  262. )
  263. self.assertEqual(response.status_code, 400)
  264. response_json = response.json()
  265. self.assertEqual(
  266. response_json, {
  267. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  268. }
  269. )
  270. def test_merge_invalid_category(self):
  271. """api rejects merge because final category was invalid"""
  272. self.override_acl({
  273. 'can_merge_threads': True,
  274. 'can_close_threads': False,
  275. 'can_edit_threads': False,
  276. 'can_reply_threads': False,
  277. })
  278. thread = testutils.post_thread(category=self.category)
  279. response = self.client.post(
  280. self.api_link,
  281. json.dumps({
  282. 'threads': [self.thread.id, thread.id],
  283. 'title': 'Valid thread title',
  284. 'category': self.category_b.id,
  285. }),
  286. content_type="application/json",
  287. )
  288. self.assertEqual(response.status_code, 400)
  289. response_json = response.json()
  290. self.assertEqual(
  291. response_json, {
  292. 'category': ["Requested category could not be found."],
  293. }
  294. )
  295. def test_merge_unallowed_start_thread(self):
  296. """api rejects merge because category isn't allowing starting threads"""
  297. self.override_acl({
  298. 'can_merge_threads': True,
  299. 'can_close_threads': False,
  300. 'can_edit_threads': False,
  301. 'can_reply_threads': False,
  302. 'can_start_threads': 0,
  303. })
  304. thread = testutils.post_thread(category=self.category)
  305. response = self.client.post(
  306. self.api_link,
  307. json.dumps({
  308. 'threads': [self.thread.id, thread.id],
  309. 'title': 'Valid thread title',
  310. 'category': self.category.id,
  311. }),
  312. content_type="application/json",
  313. )
  314. self.assertEqual(response.status_code, 400)
  315. response_json = response.json()
  316. self.assertEqual(
  317. response_json, {
  318. 'category': ["You can't create new threads in selected category."],
  319. }
  320. )
  321. def test_merge_invalid_weight(self):
  322. """api rejects merge because final weight was invalid"""
  323. self.override_acl({
  324. 'can_merge_threads': True,
  325. 'can_close_threads': False,
  326. 'can_edit_threads': False,
  327. 'can_reply_threads': False,
  328. })
  329. thread = testutils.post_thread(category=self.category)
  330. response = self.client.post(
  331. self.api_link,
  332. json.dumps({
  333. 'threads': [self.thread.id, thread.id],
  334. 'title': 'Valid thread title',
  335. 'category': self.category.id,
  336. 'weight': 4,
  337. }),
  338. content_type="application/json",
  339. )
  340. self.assertEqual(response.status_code, 400)
  341. response_json = response.json()
  342. self.assertEqual(
  343. response_json, {
  344. 'weight': ["Ensure this value is less than or equal to 2."],
  345. }
  346. )
  347. def test_merge_unallowed_global_weight(self):
  348. """api rejects merge because global weight was unallowed"""
  349. self.override_acl({
  350. 'can_merge_threads': True,
  351. 'can_close_threads': False,
  352. 'can_edit_threads': False,
  353. 'can_reply_threads': False,
  354. })
  355. thread = testutils.post_thread(category=self.category)
  356. response = self.client.post(
  357. self.api_link,
  358. json.dumps({
  359. 'threads': [self.thread.id, thread.id],
  360. 'title': 'Valid thread title',
  361. 'category': self.category.id,
  362. 'weight': 2,
  363. }),
  364. content_type="application/json",
  365. )
  366. self.assertEqual(response.status_code, 400)
  367. response_json = response.json()
  368. self.assertEqual(
  369. response_json, {
  370. 'weight': ["You don't have permission to pin threads globally in this category."],
  371. }
  372. )
  373. def test_merge_unallowed_local_weight(self):
  374. """api rejects merge because local weight was unallowed"""
  375. self.override_acl({
  376. 'can_merge_threads': True,
  377. 'can_close_threads': False,
  378. 'can_edit_threads': False,
  379. 'can_reply_threads': False,
  380. })
  381. thread = testutils.post_thread(category=self.category)
  382. response = self.client.post(
  383. self.api_link,
  384. json.dumps({
  385. 'threads': [self.thread.id, thread.id],
  386. 'title': 'Valid thread title',
  387. 'category': self.category.id,
  388. 'weight': 1,
  389. }),
  390. content_type="application/json",
  391. )
  392. self.assertEqual(response.status_code, 400)
  393. response_json = response.json()
  394. self.assertEqual(
  395. response_json, {
  396. 'weight': ["You don't have permission to pin threads in this category."],
  397. }
  398. )
  399. def test_merge_allowed_local_weight(self):
  400. """api allows local weight"""
  401. self.override_acl({
  402. 'can_merge_threads': True,
  403. 'can_close_threads': False,
  404. 'can_edit_threads': False,
  405. 'can_reply_threads': False,
  406. 'can_pin_threads': 1,
  407. })
  408. thread = testutils.post_thread(category=self.category)
  409. response = self.client.post(
  410. self.api_link,
  411. json.dumps({
  412. 'threads': [self.thread.id, thread.id],
  413. 'title': '$$$',
  414. 'category': self.category.id,
  415. 'weight': 1,
  416. }),
  417. content_type="application/json",
  418. )
  419. self.assertEqual(response.status_code, 400)
  420. response_json = response.json()
  421. self.assertEqual(
  422. response_json, {
  423. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  424. }
  425. )
  426. def test_merge_allowed_global_weight(self):
  427. """api allows global weight"""
  428. self.override_acl({
  429. 'can_merge_threads': True,
  430. 'can_close_threads': False,
  431. 'can_edit_threads': False,
  432. 'can_reply_threads': False,
  433. 'can_pin_threads': 2,
  434. })
  435. thread = testutils.post_thread(category=self.category)
  436. response = self.client.post(
  437. self.api_link,
  438. json.dumps({
  439. 'threads': [self.thread.id, thread.id],
  440. 'title': '$$$',
  441. 'category': self.category.id,
  442. 'weight': 2,
  443. }),
  444. content_type="application/json",
  445. )
  446. self.assertEqual(response.status_code, 400)
  447. response_json = response.json()
  448. self.assertEqual(
  449. response_json, {
  450. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  451. }
  452. )
  453. def test_merge_unallowed_close(self):
  454. """api rejects merge because closing thread was unallowed"""
  455. self.override_acl({
  456. 'can_merge_threads': True,
  457. 'can_close_threads': False,
  458. 'can_edit_threads': False,
  459. 'can_reply_threads': False,
  460. })
  461. thread = testutils.post_thread(category=self.category)
  462. response = self.client.post(
  463. self.api_link,
  464. json.dumps({
  465. 'threads': [self.thread.id, thread.id],
  466. 'title': 'Valid thread title',
  467. 'category': self.category.id,
  468. 'is_closed': True,
  469. }),
  470. content_type="application/json",
  471. )
  472. self.assertEqual(response.status_code, 400)
  473. response_json = response.json()
  474. self.assertEqual(
  475. response_json, {
  476. 'is_closed': ["You don't have permission to close threads in this category."],
  477. }
  478. )
  479. def test_merge_with_close(self):
  480. """api allows for closing thread"""
  481. self.override_acl({
  482. 'can_merge_threads': True,
  483. 'can_edit_threads': False,
  484. 'can_reply_threads': False,
  485. 'can_close_threads': True,
  486. })
  487. thread = testutils.post_thread(category=self.category)
  488. response = self.client.post(
  489. self.api_link,
  490. json.dumps({
  491. 'threads': [self.thread.id, thread.id],
  492. 'title': '$$$',
  493. 'category': self.category.id,
  494. 'weight': 0,
  495. 'is_closed': True,
  496. }),
  497. content_type="application/json",
  498. )
  499. self.assertEqual(response.status_code, 400)
  500. response_json = response.json()
  501. self.assertEqual(
  502. response_json, {
  503. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  504. }
  505. )
  506. def test_merge_unallowed_hidden(self):
  507. """api rejects merge because hidden thread was unallowed"""
  508. self.override_acl({
  509. 'can_merge_threads': True,
  510. 'can_close_threads': False,
  511. 'can_edit_threads': False,
  512. 'can_reply_threads': False,
  513. 'can_hide_threads': 0,
  514. })
  515. thread = testutils.post_thread(category=self.category)
  516. response = self.client.post(
  517. self.api_link,
  518. json.dumps({
  519. 'threads': [self.thread.id, thread.id],
  520. 'title': 'Valid thread title',
  521. 'category': self.category.id,
  522. 'is_hidden': True,
  523. }),
  524. content_type="application/json",
  525. )
  526. self.assertEqual(response.status_code, 400)
  527. response_json = response.json()
  528. self.assertEqual(
  529. response_json, {
  530. 'is_hidden': ["You don't have permission to hide threads in this category."],
  531. }
  532. )
  533. def test_merge_with_hide(self):
  534. """api allows for hiding thread"""
  535. self.override_acl({
  536. 'can_merge_threads': True,
  537. 'can_close_threads': False,
  538. 'can_edit_threads': False,
  539. 'can_reply_threads': False,
  540. 'can_hide_threads': 1,
  541. })
  542. thread = testutils.post_thread(category=self.category)
  543. response = self.client.post(
  544. self.api_link,
  545. json.dumps({
  546. 'threads': [self.thread.id, thread.id],
  547. 'title': '$$$',
  548. 'category': self.category.id,
  549. 'weight': 0,
  550. 'is_hidden': True,
  551. }),
  552. content_type="application/json",
  553. )
  554. self.assertEqual(response.status_code, 400)
  555. response_json = response.json()
  556. self.assertEqual(
  557. response_json, {
  558. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  559. }
  560. )
  561. def test_merge(self):
  562. """api performs basic merge"""
  563. posts_ids = [p.id for p in Post.objects.all()]
  564. self.override_acl({
  565. 'can_merge_threads': True,
  566. 'can_close_threads': False,
  567. 'can_edit_threads': False,
  568. 'can_reply_threads': False,
  569. })
  570. thread = testutils.post_thread(category=self.category)
  571. response = self.client.post(
  572. self.api_link,
  573. json.dumps({
  574. 'threads': [self.thread.id, thread.id],
  575. 'title': 'Merged thread!',
  576. 'category': self.category.id,
  577. }),
  578. content_type="application/json",
  579. )
  580. self.assertEqual(response.status_code, 200)
  581. # is response json with new thread?
  582. response_json = response.json()
  583. new_thread = Thread.objects.get(pk=response_json['id'])
  584. new_thread.is_read = False
  585. new_thread.subscription = None
  586. add_acl(self.user, new_thread.category)
  587. add_acl(self.user, new_thread)
  588. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  589. # did posts move to new thread?
  590. for post in Post.objects.filter(id__in=posts_ids):
  591. self.assertEqual(post.thread_id, new_thread.id)
  592. # are old threads gone?
  593. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  594. def test_merge_kitchensink(self):
  595. """api performs merge"""
  596. posts_ids = [p.id for p in Post.objects.all()]
  597. self.override_acl({
  598. 'can_merge_threads': True,
  599. 'can_close_threads': True,
  600. 'can_hide_threads': 1,
  601. 'can_pin_threads': 2,
  602. })
  603. thread = testutils.post_thread(category=self.category)
  604. response = self.client.post(
  605. self.api_link,
  606. json.dumps({
  607. 'threads': [self.thread.id, thread.id],
  608. 'title': 'Merged thread!',
  609. 'category': self.category.id,
  610. 'is_closed': 1,
  611. 'is_hidden': 1,
  612. 'weight': 2,
  613. }),
  614. content_type="application/json",
  615. )
  616. self.assertEqual(response.status_code, 200)
  617. # is response json with new thread?
  618. response_json = response.json()
  619. new_thread = Thread.objects.get(pk=response_json['id'])
  620. new_thread.is_read = False
  621. new_thread.subscription = None
  622. self.assertEqual(new_thread.weight, 2)
  623. self.assertTrue(new_thread.is_closed)
  624. self.assertTrue(new_thread.is_hidden)
  625. add_acl(self.user, new_thread.category)
  626. add_acl(self.user, new_thread)
  627. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  628. # did posts move to new thread?
  629. for post in Post.objects.filter(id__in=posts_ids):
  630. self.assertEqual(post.thread_id, new_thread.id)
  631. # are old threads gone?
  632. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  633. def test_merge_threads_kept_poll(self):
  634. """api merges two threads successfully, keeping poll from old thread"""
  635. self.override_acl({'can_merge_threads': True})
  636. other_thread = testutils.post_thread(self.category)
  637. poll = testutils.post_poll(other_thread, self.user)
  638. response = self.client.post(
  639. self.api_link,
  640. json.dumps({
  641. 'threads': [self.thread.id, other_thread.id],
  642. 'title': 'Merged thread!',
  643. 'category': self.category.id,
  644. }),
  645. content_type="application/json",
  646. )
  647. self.assertEqual(response.status_code, 200)
  648. response_json = response.json()
  649. new_thread = Thread.objects.get(pk=response_json['id'])
  650. # poll and its votes were kept
  651. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=new_thread).count(), 1)
  652. self.assertEqual(PollVote.objects.filter(poll=poll, thread=new_thread).count(), 4)
  653. self.assertEqual(Poll.objects.count(), 1)
  654. self.assertEqual(PollVote.objects.count(), 4)
  655. def test_merge_threads_moved_poll(self):
  656. """api merges two threads successfully, moving poll from other thread"""
  657. self.override_acl({'can_merge_threads': True})
  658. other_thread = testutils.post_thread(self.category)
  659. poll = testutils.post_poll(self.thread, self.user)
  660. response = self.client.post(
  661. self.api_link,
  662. json.dumps({
  663. 'threads': [self.thread.id, other_thread.id],
  664. 'title': 'Merged thread!',
  665. 'category': self.category.id,
  666. }),
  667. content_type="application/json",
  668. )
  669. self.assertEqual(response.status_code, 200)
  670. response_json = response.json()
  671. new_thread = Thread.objects.get(pk=response_json['id'])
  672. # poll and its votes were kept
  673. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=new_thread).count(), 1)
  674. self.assertEqual(PollVote.objects.filter(poll=poll, thread=new_thread).count(), 4)
  675. self.assertEqual(Poll.objects.count(), 1)
  676. self.assertEqual(PollVote.objects.count(), 4)
  677. def test_threads_merge_conflict(self):
  678. """api errors on merge conflict, returning list of available polls"""
  679. self.override_acl({'can_merge_threads': True})
  680. other_thread = testutils.post_thread(self.category)
  681. poll = testutils.post_poll(self.thread, self.user)
  682. other_poll = testutils.post_poll(other_thread, self.user)
  683. response = self.client.post(
  684. self.api_link,
  685. json.dumps({
  686. 'threads': [self.thread.id, other_thread.id],
  687. 'title': 'Merged thread!',
  688. 'category': self.category.id,
  689. }),
  690. content_type="application/json",
  691. )
  692. self.assertEqual(response.status_code, 400)
  693. self.assertEqual(
  694. response.json(), {
  695. 'polls': [
  696. [0, "Delete all polls"],
  697. [poll.pk, poll.question],
  698. [other_poll.pk, other_poll.question],
  699. ],
  700. }
  701. )
  702. # polls and votes were untouched
  703. self.assertEqual(Poll.objects.count(), 2)
  704. self.assertEqual(PollVote.objects.count(), 8)
  705. def test_threads_merge_conflict_invalid_resolution(self):
  706. """api errors on invalid merge conflict resolution"""
  707. self.override_acl({'can_merge_threads': True})
  708. other_thread = testutils.post_thread(self.category)
  709. testutils.post_poll(self.thread, self.user)
  710. testutils.post_poll(other_thread, self.user)
  711. response = self.client.post(
  712. self.api_link,
  713. json.dumps({
  714. 'threads': [self.thread.id, other_thread.id],
  715. 'title': 'Merged thread!',
  716. 'category': self.category.id,
  717. 'poll': 'dsa7dsadsa9789',
  718. }),
  719. content_type="application/json",
  720. )
  721. self.assertEqual(response.status_code, 400)
  722. self.assertEqual(response.json(), {
  723. 'detail': "Invalid choice.",
  724. })
  725. # polls and votes were untouched
  726. self.assertEqual(Poll.objects.count(), 2)
  727. self.assertEqual(PollVote.objects.count(), 8)
  728. def test_threads_merge_conflict_delete_all(self):
  729. """api deletes all polls when delete all choice is selected"""
  730. self.override_acl({'can_merge_threads': True})
  731. other_thread = testutils.post_thread(self.category)
  732. testutils.post_poll(self.thread, self.user)
  733. testutils.post_poll(other_thread, self.user)
  734. response = self.client.post(
  735. self.api_link,
  736. json.dumps({
  737. 'threads': [self.thread.id, other_thread.id],
  738. 'title': 'Merged thread!',
  739. 'category': self.category.id,
  740. 'poll': 0,
  741. }),
  742. content_type="application/json",
  743. )
  744. self.assertEqual(response.status_code, 200)
  745. # polls and votes are gone
  746. self.assertEqual(Poll.objects.count(), 0)
  747. self.assertEqual(PollVote.objects.count(), 0)
  748. def test_threads_merge_conflict_keep_first_poll(self):
  749. """api deletes other poll on merge"""
  750. self.override_acl({'can_merge_threads': True})
  751. other_thread = testutils.post_thread(self.category)
  752. poll = testutils.post_poll(self.thread, self.user)
  753. other_poll = testutils.post_poll(other_thread, self.user)
  754. response = self.client.post(
  755. self.api_link,
  756. json.dumps({
  757. 'threads': [self.thread.id, other_thread.id],
  758. 'title': 'Merged thread!',
  759. 'category': self.category.id,
  760. 'poll': poll.pk,
  761. }),
  762. content_type="application/json",
  763. )
  764. self.assertEqual(response.status_code, 200)
  765. # other poll and its votes are gone
  766. self.assertEqual(Poll.objects.count(), 1)
  767. self.assertEqual(PollVote.objects.count(), 4)
  768. Poll.objects.get(pk=poll.pk)
  769. with self.assertRaises(Poll.DoesNotExist):
  770. Poll.objects.get(pk=other_poll.pk)
  771. def test_threads_merge_conflict_keep_other_poll(self):
  772. """api deletes first poll on merge"""
  773. self.override_acl({'can_merge_threads': True})
  774. other_thread = testutils.post_thread(self.category)
  775. poll = testutils.post_poll(self.thread, self.user)
  776. other_poll = testutils.post_poll(other_thread, self.user)
  777. response = self.client.post(
  778. self.api_link,
  779. json.dumps({
  780. 'threads': [self.thread.id, other_thread.id],
  781. 'title': 'Merged thread!',
  782. 'category': self.category.id,
  783. 'poll': other_poll.pk,
  784. }),
  785. content_type="application/json",
  786. )
  787. self.assertEqual(response.status_code, 200)
  788. # other poll and its votes are gone
  789. self.assertEqual(Poll.objects.count(), 1)
  790. self.assertEqual(PollVote.objects.count(), 4)
  791. Poll.objects.get(pk=other_poll.pk)
  792. with self.assertRaises(Poll.DoesNotExist):
  793. Poll.objects.get(pk=poll.pk)