test_mergeconflict.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. from rest_framework.exceptions import ValidationError
  2. from django.contrib.auth import get_user_model
  3. from django.test import TestCase
  4. from misago.categories.models import Category
  5. from misago.threads import testutils
  6. from misago.threads.mergeconflict import MergeConflict
  7. User = get_user_model()
  8. class MergeConflictTests(TestCase):
  9. def setUp(self):
  10. self.category = Category.objects.get(slug="first-category")
  11. self.user = User.objects.create_user("bob", "bob@test.com", "Pass.123")
  12. def create_plain_thread(self):
  13. return testutils.post_thread(self.category)
  14. def create_poll_thread(self):
  15. thread = testutils.post_thread(self.category)
  16. testutils.post_poll(thread, self.user)
  17. return thread
  18. def create_best_answer_thread(self):
  19. thread = testutils.post_thread(self.category)
  20. best_answer = testutils.reply_thread(thread)
  21. thread.set_best_answer(self.user, best_answer)
  22. thread.synchronize()
  23. thread.save()
  24. return thread
  25. def test_plain_threads_no_conflicts(self):
  26. """threads without items of interest don't conflict"""
  27. threads = [self.create_plain_thread() for i in range(10)]
  28. merge_conflict = MergeConflict(threads=threads)
  29. self.assertFalse(merge_conflict.is_merge_conflict())
  30. self.assertEqual(merge_conflict.get_conflicting_fields(), [])
  31. merge_conflict.is_valid(raise_exception=True)
  32. self.assertEqual(merge_conflict.get_resolution(), {})
  33. def test_one_best_answer_one_plain(self):
  34. """thread with best answer and plain thread don't conflict"""
  35. threads = [self.create_best_answer_thread(), self.create_plain_thread()]
  36. merge_conflict = MergeConflict(threads=threads)
  37. self.assertFalse(merge_conflict.is_merge_conflict())
  38. self.assertEqual(merge_conflict.get_conflicting_fields(), [])
  39. merge_conflict.is_valid(raise_exception=True)
  40. self.assertEqual(merge_conflict.get_resolution(), {"best_answer": threads[0]})
  41. def test_one_poll_one_plain(self):
  42. """thread with poll and plain thread don't conflict"""
  43. threads = [self.create_poll_thread(), self.create_plain_thread()]
  44. merge_conflict = MergeConflict(threads=threads)
  45. self.assertFalse(merge_conflict.is_merge_conflict())
  46. self.assertEqual(merge_conflict.get_conflicting_fields(), [])
  47. merge_conflict.is_valid(raise_exception=True)
  48. self.assertEqual(merge_conflict.get_resolution(), {"poll": threads[0].poll})
  49. def test_one_best_answer_one_poll(self):
  50. """thread with best answer and thread with poll don't conflict"""
  51. threads = [self.create_poll_thread(), self.create_best_answer_thread()]
  52. merge_conflict = MergeConflict(threads=threads)
  53. self.assertFalse(merge_conflict.is_merge_conflict())
  54. def test_one_best_answer_one_poll_one_plain(self):
  55. """thread with best answer, thread with poll and plain thread don't conflict"""
  56. threads = [
  57. self.create_plain_thread(),
  58. self.create_poll_thread(),
  59. self.create_best_answer_thread(),
  60. ]
  61. merge_conflict = MergeConflict(threads=threads)
  62. self.assertFalse(merge_conflict.is_merge_conflict())
  63. def test_three_best_answers_one_poll_two_plain_conflict(self):
  64. """three threads with best answer, thread with poll and two plain threads conflict"""
  65. best_answers = [self.create_best_answer_thread() for i in range(3)]
  66. polls = [self.create_poll_thread()]
  67. threads = (
  68. [self.create_plain_thread(), self.create_plain_thread()]
  69. + best_answers
  70. + polls
  71. )
  72. merge_conflict = MergeConflict(threads=threads)
  73. self.assertTrue(merge_conflict.is_merge_conflict())
  74. self.assertEqual(merge_conflict.get_conflicting_fields(), ["best_answer"])
  75. # without choice, conflict lists resolutions
  76. try:
  77. merge_conflict.is_valid(raise_exception=True)
  78. self.fail("merge_conflict.is_valid() should raise ValidationError")
  79. except ValidationError as e:
  80. self.assertTrue(merge_conflict.is_merge_conflict())
  81. self.assertEqual(merge_conflict.get_conflicting_fields(), ["best_answer"])
  82. self.assertEqual(
  83. e.detail,
  84. {
  85. "best_answers": [["0", "Unmark all best answers"]]
  86. + [[str(thread.id), thread.title] for thread in best_answers]
  87. },
  88. )
  89. # conflict validates choice
  90. try:
  91. merge_conflict = MergeConflict({"best_answer": threads[0].id}, threads)
  92. merge_conflict.is_valid(raise_exception=True)
  93. self.fail("merge_conflict.is_valid() should raise ValidationError")
  94. except ValidationError as e:
  95. self.assertTrue(merge_conflict.is_merge_conflict())
  96. self.assertEqual(e.detail, {"best_answer": ["Invalid choice."]})
  97. # conflict returns selected resolution
  98. merge_conflict = MergeConflict({"best_answer": best_answers[0].id}, threads)
  99. self.assertTrue(merge_conflict.is_merge_conflict())
  100. self.assertEqual(merge_conflict.get_conflicting_fields(), ["best_answer"])
  101. merge_conflict.is_valid(raise_exception=True)
  102. self.assertEqual(
  103. merge_conflict.get_resolution(),
  104. {"best_answer": best_answers[0], "poll": polls[0].poll},
  105. )
  106. # conflict returns no-choice resolution
  107. merge_conflict = MergeConflict({"best_answer": 0}, threads)
  108. self.assertTrue(merge_conflict.is_merge_conflict())
  109. self.assertEqual(merge_conflict.get_conflicting_fields(), ["best_answer"])
  110. merge_conflict.is_valid(raise_exception=True)
  111. self.assertEqual(
  112. merge_conflict.get_resolution(),
  113. {"best_answer": None, "poll": polls[0].poll},
  114. )
  115. def test_one_best_answer_three_polls_two_plain_conflict(self):
  116. """one thread with best answer, three threads with poll and two plain threads conflict"""
  117. best_answers = [self.create_best_answer_thread()]
  118. polls = [self.create_poll_thread() for i in range(3)]
  119. threads = (
  120. [self.create_plain_thread(), self.create_plain_thread()]
  121. + best_answers
  122. + polls
  123. )
  124. merge_conflict = MergeConflict(threads=threads)
  125. self.assertTrue(merge_conflict.is_merge_conflict())
  126. self.assertEqual(merge_conflict.get_conflicting_fields(), ["poll"])
  127. # without choice, conflict lists resolutions
  128. try:
  129. merge_conflict.is_valid(raise_exception=True)
  130. self.fail("merge_conflict.is_valid() should raise ValidationError")
  131. except ValidationError as e:
  132. self.assertTrue(merge_conflict.is_merge_conflict())
  133. self.assertEqual(merge_conflict.get_conflicting_fields(), ["poll"])
  134. self.assertEqual(
  135. e.detail,
  136. {
  137. "polls": [["0", "Delete all polls"]]
  138. + [
  139. [
  140. str(thread.poll.id),
  141. "%s (%s)" % (thread.poll.question, thread.title),
  142. ]
  143. for thread in polls
  144. ]
  145. },
  146. )
  147. # conflict validates choice
  148. try:
  149. merge_conflict = MergeConflict({"poll": threads[0].id}, threads)
  150. merge_conflict.is_valid(raise_exception=True)
  151. self.fail("merge_conflict.is_valid() should raise ValidationError")
  152. except ValidationError as e:
  153. self.assertTrue(merge_conflict.is_merge_conflict())
  154. self.assertEqual(e.detail, {"poll": ["Invalid choice."]})
  155. # conflict returns selected resolution
  156. merge_conflict = MergeConflict({"poll": polls[0].poll.id}, threads)
  157. self.assertTrue(merge_conflict.is_merge_conflict())
  158. self.assertEqual(merge_conflict.get_conflicting_fields(), ["poll"])
  159. merge_conflict.is_valid(raise_exception=True)
  160. self.assertEqual(
  161. merge_conflict.get_resolution(),
  162. {"best_answer": best_answers[0], "poll": polls[0].poll},
  163. )
  164. # conflict returns no-choice resolution
  165. merge_conflict = MergeConflict({"poll": 0}, threads)
  166. self.assertTrue(merge_conflict.is_merge_conflict())
  167. self.assertEqual(merge_conflict.get_conflicting_fields(), ["poll"])
  168. merge_conflict.is_valid(raise_exception=True)
  169. self.assertEqual(
  170. merge_conflict.get_resolution(),
  171. {"best_answer": best_answers[0], "poll": None},
  172. )
  173. def test_three_best_answers_three_polls_two_plain_conflict(self):
  174. """multiple conflict is handled"""
  175. best_answers = [self.create_best_answer_thread() for i in range(3)]
  176. polls = [self.create_poll_thread() for i in range(3)]
  177. threads = (
  178. [self.create_plain_thread(), self.create_plain_thread()]
  179. + best_answers
  180. + polls
  181. )
  182. merge_conflict = MergeConflict(threads=threads)
  183. self.assertTrue(merge_conflict.is_merge_conflict())
  184. self.assertEqual(
  185. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  186. )
  187. # without choice, conflict lists all resolutions
  188. try:
  189. merge_conflict.is_valid(raise_exception=True)
  190. self.fail("merge_conflict.is_valid() should raise ValidationError")
  191. except ValidationError as e:
  192. self.assertTrue(merge_conflict.is_merge_conflict())
  193. self.assertEqual(
  194. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  195. )
  196. self.assertEqual(
  197. e.detail,
  198. {
  199. "best_answers": [["0", "Unmark all best answers"]]
  200. + [[str(thread.id), thread.title] for thread in best_answers],
  201. "polls": [["0", "Delete all polls"]]
  202. + [
  203. [
  204. str(thread.poll.id),
  205. "%s (%s)" % (thread.poll.question, thread.title),
  206. ]
  207. for thread in polls
  208. ],
  209. },
  210. )
  211. # conflict validates all choices if single choice was given
  212. try:
  213. merge_conflict = MergeConflict({"best_answer": threads[0].id}, threads)
  214. merge_conflict.is_valid(raise_exception=True)
  215. self.fail("merge_conflict.is_valid() should raise ValidationError")
  216. except ValidationError as e:
  217. self.assertTrue(merge_conflict.is_merge_conflict())
  218. self.assertEqual(
  219. e.detail,
  220. {"best_answer": ["Invalid choice."], "poll": ["Invalid choice."]},
  221. )
  222. try:
  223. merge_conflict = MergeConflict({"poll": threads[0].id}, threads)
  224. merge_conflict.is_valid(raise_exception=True)
  225. self.fail("merge_conflict.is_valid() should raise ValidationError")
  226. except ValidationError as e:
  227. self.assertTrue(merge_conflict.is_merge_conflict())
  228. self.assertEqual(
  229. e.detail,
  230. {"best_answer": ["Invalid choice."], "poll": ["Invalid choice."]},
  231. )
  232. # conflict validates all choices if all choices were given
  233. try:
  234. merge_conflict = MergeConflict(
  235. {"best_answer": threads[0].id, "poll": threads[0].id}, threads
  236. )
  237. merge_conflict.is_valid(raise_exception=True)
  238. self.fail("merge_conflict.is_valid() should raise ValidationError")
  239. except ValidationError as e:
  240. self.assertTrue(merge_conflict.is_merge_conflict())
  241. self.assertEqual(
  242. e.detail,
  243. {"best_answer": ["Invalid choice."], "poll": ["Invalid choice."]},
  244. )
  245. # conflict returns selected resolutions
  246. valid_choices = {"best_answer": best_answers[0].id, "poll": polls[0].poll.id}
  247. merge_conflict = MergeConflict(valid_choices, threads)
  248. self.assertTrue(merge_conflict.is_merge_conflict())
  249. self.assertEqual(
  250. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  251. )
  252. merge_conflict.is_valid(raise_exception=True)
  253. self.assertEqual(
  254. merge_conflict.get_resolution(),
  255. {"best_answer": best_answers[0], "poll": polls[0].poll},
  256. )
  257. # conflict returns no-choice resolution
  258. merge_conflict = MergeConflict({"best_answer": 0, "poll": 0}, threads)
  259. self.assertTrue(merge_conflict.is_merge_conflict())
  260. self.assertEqual(
  261. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  262. )
  263. merge_conflict.is_valid(raise_exception=True)
  264. self.assertEqual(
  265. merge_conflict.get_resolution(), {"best_answer": None, "poll": None}
  266. )
  267. # conflict allows mixing no-choice with choice
  268. merge_conflict = MergeConflict(
  269. {"best_answer": best_answers[0].id, "poll": 0}, threads
  270. )
  271. self.assertTrue(merge_conflict.is_merge_conflict())
  272. self.assertEqual(
  273. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  274. )
  275. merge_conflict.is_valid(raise_exception=True)
  276. self.assertEqual(
  277. merge_conflict.get_resolution(),
  278. {"best_answer": best_answers[0], "poll": None},
  279. )
  280. merge_conflict = MergeConflict(
  281. {"best_answer": 0, "poll": polls[0].poll.id}, threads
  282. )
  283. self.assertTrue(merge_conflict.is_merge_conflict())
  284. self.assertEqual(
  285. merge_conflict.get_conflicting_fields(), ["best_answer", "poll"]
  286. )
  287. merge_conflict.is_valid(raise_exception=True)
  288. self.assertEqual(
  289. merge_conflict.get_resolution(),
  290. {"best_answer": None, "poll": polls[0].poll},
  291. )