test_thread_patch_api.py 78 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387
  1. import json
  2. from datetime import timedelta
  3. from django.utils import timezone
  4. from misago.categories.models import Category
  5. from misago.readtracker import poststracker
  6. from misago.threads import testutils
  7. from misago.threads.test import patch_category_acl, patch_other_category_acl
  8. from misago.threads.models import Thread
  9. from .test_threads_api import ThreadsApiTestCase
  10. class ThreadPatchApiTestCase(ThreadsApiTestCase):
  11. def patch(self, api_link, ops):
  12. return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
  13. class ThreadAddAclApiTests(ThreadPatchApiTestCase):
  14. def test_add_acl_true(self):
  15. """api adds current thread's acl to response"""
  16. response = self.patch(self.api_link, [
  17. {
  18. 'op': 'add',
  19. 'path': 'acl',
  20. 'value': True,
  21. },
  22. ])
  23. self.assertEqual(response.status_code, 200)
  24. response_json = response.json()
  25. self.assertTrue(response_json['acl'])
  26. def test_add_acl_false(self):
  27. """if value is false, api won't add acl to the response, but will set empty key"""
  28. response = self.patch(self.api_link, [
  29. {
  30. 'op': 'add',
  31. 'path': 'acl',
  32. 'value': False,
  33. },
  34. ])
  35. self.assertEqual(response.status_code, 200)
  36. response_json = response.json()
  37. self.assertIsNone(response_json['acl'])
  38. class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
  39. @patch_category_acl({'can_edit_threads': 2})
  40. def test_change_thread_title(self):
  41. """api makes it possible to change thread title"""
  42. response = self.patch(
  43. self.api_link, [
  44. {
  45. 'op': 'replace',
  46. 'path': 'title',
  47. 'value': "Lorem ipsum change!",
  48. },
  49. ]
  50. )
  51. self.assertEqual(response.status_code, 200)
  52. response_json = response.json()
  53. self.assertEqual(response_json['title'], "Lorem ipsum change!")
  54. thread_json = self.get_thread_json()
  55. self.assertEqual(thread_json['title'], "Lorem ipsum change!")
  56. @patch_category_acl({'can_edit_threads': 0})
  57. def test_change_thread_title_no_permission(self):
  58. """api validates permission to change title"""
  59. response = self.patch(
  60. self.api_link, [
  61. {
  62. 'op': 'replace',
  63. 'path': 'title',
  64. 'value': "Lorem ipsum change!",
  65. },
  66. ]
  67. )
  68. self.assertEqual(response.status_code, 400)
  69. response_json = response.json()
  70. self.assertEqual(response_json['detail'][0], "You can't edit threads in this category.")
  71. @patch_category_acl({'can_edit_threads': 2, 'can_close_threads': 0})
  72. def test_change_thread_title_closed_category_no_permission(self):
  73. """api test permission to edit thread title in closed category"""
  74. self.category.is_closed = True
  75. self.category.save()
  76. response = self.patch(
  77. self.api_link, [
  78. {
  79. 'op': 'replace',
  80. 'path': 'title',
  81. 'value': "Lorem ipsum change!",
  82. },
  83. ]
  84. )
  85. self.assertEqual(response.status_code, 400)
  86. response_json = response.json()
  87. self.assertEqual(
  88. response_json['detail'][0], "This category is closed. You can't edit threads in it."
  89. )
  90. @patch_category_acl({'can_edit_threads': 2, 'can_close_threads': 0})
  91. def test_change_thread_title_closed_thread_no_permission(self):
  92. """api test permission to edit closed thread title"""
  93. self.thread.is_closed = True
  94. self.thread.save()
  95. response = self.patch(
  96. self.api_link, [
  97. {
  98. 'op': 'replace',
  99. 'path': 'title',
  100. 'value': "Lorem ipsum change!",
  101. },
  102. ]
  103. )
  104. self.assertEqual(response.status_code, 400)
  105. response_json = response.json()
  106. self.assertEqual(
  107. response_json['detail'][0], "This thread is closed. You can't edit it."
  108. )
  109. @patch_category_acl({'can_edit_threads': 1, 'thread_edit_time': 1})
  110. def test_change_thread_title_after_edit_time(self):
  111. """api cleans, validates and rejects too short title"""
  112. self.thread.started_on = timezone.now() - timedelta(minutes=10)
  113. self.thread.starter = self.user
  114. self.thread.save()
  115. response = self.patch(
  116. self.api_link, [
  117. {
  118. 'op': 'replace',
  119. 'path': 'title',
  120. 'value': "Lorem ipsum change!",
  121. },
  122. ]
  123. )
  124. self.assertEqual(response.status_code, 400)
  125. response_json = response.json()
  126. self.assertEqual(
  127. response_json['detail'][0], "You can't edit threads that are older than 1 minute."
  128. )
  129. @patch_category_acl({'can_edit_threads': 2})
  130. def test_change_thread_title_invalid(self):
  131. """api cleans, validates and rejects too short title"""
  132. response = self.patch(
  133. self.api_link, [
  134. {
  135. 'op': 'replace',
  136. 'path': 'title',
  137. 'value': 12,
  138. },
  139. ]
  140. )
  141. self.assertEqual(response.status_code, 400)
  142. response_json = response.json()
  143. self.assertEqual(
  144. response_json['detail'][0],
  145. "Thread title should be at least 5 characters long (it has 2)."
  146. )
  147. class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
  148. @patch_category_acl({'can_pin_threads': 2})
  149. def test_pin_thread(self):
  150. """api makes it possible to pin globally thread"""
  151. response = self.patch(
  152. self.api_link, [
  153. {
  154. 'op': 'replace',
  155. 'path': 'weight',
  156. 'value': 2,
  157. },
  158. ]
  159. )
  160. self.assertEqual(response.status_code, 200)
  161. response_json = response.json()
  162. self.assertEqual(response_json['weight'], 2)
  163. thread_json = self.get_thread_json()
  164. self.assertEqual(thread_json['weight'], 2)
  165. @patch_category_acl({'can_pin_threads': 2, 'can_close_threads': 0})
  166. def test_pin_thread_closed_category_no_permission(self):
  167. """api checks if category is closed"""
  168. self.category.is_closed = True
  169. self.category.save()
  170. response = self.patch(
  171. self.api_link, [
  172. {
  173. 'op': 'replace',
  174. 'path': 'weight',
  175. 'value': 2,
  176. },
  177. ]
  178. )
  179. self.assertEqual(response.status_code, 400)
  180. response_json = response.json()
  181. self.assertEqual(
  182. response_json['detail'][0], "This category is closed. You can't change threads weights in it."
  183. )
  184. @patch_category_acl({'can_pin_threads': 2, 'can_close_threads': 0})
  185. def test_pin_thread_closed_no_permission(self):
  186. """api checks if thread is closed"""
  187. self.thread.is_closed = True
  188. self.thread.save()
  189. response = self.patch(
  190. self.api_link, [
  191. {
  192. 'op': 'replace',
  193. 'path': 'weight',
  194. 'value': 2,
  195. },
  196. ]
  197. )
  198. self.assertEqual(response.status_code, 400)
  199. response_json = response.json()
  200. self.assertEqual(
  201. response_json['detail'][0], "This thread is closed. You can't change its weight."
  202. )
  203. @patch_category_acl({'can_pin_threads': 2})
  204. def test_unpin_thread(self):
  205. """api makes it possible to unpin thread"""
  206. self.thread.weight = 2
  207. self.thread.save()
  208. thread_json = self.get_thread_json()
  209. self.assertEqual(thread_json['weight'], 2)
  210. response = self.patch(
  211. self.api_link, [
  212. {
  213. 'op': 'replace',
  214. 'path': 'weight',
  215. 'value': 0,
  216. },
  217. ]
  218. )
  219. self.assertEqual(response.status_code, 200)
  220. response_json = response.json()
  221. self.assertEqual(response_json['weight'], 0)
  222. thread_json = self.get_thread_json()
  223. self.assertEqual(thread_json['weight'], 0)
  224. @patch_category_acl({'can_pin_threads': 1})
  225. def test_pin_thread_no_permission(self):
  226. """api pin thread globally with no permission fails"""
  227. response = self.patch(
  228. self.api_link, [
  229. {
  230. 'op': 'replace',
  231. 'path': 'weight',
  232. 'value': 2,
  233. },
  234. ]
  235. )
  236. self.assertEqual(response.status_code, 400)
  237. response_json = response.json()
  238. self.assertEqual(
  239. response_json['detail'][0], "You can't pin threads globally in this category."
  240. )
  241. thread_json = self.get_thread_json()
  242. self.assertEqual(thread_json['weight'], 0)
  243. @patch_category_acl({'can_pin_threads': 1})
  244. def test_unpin_thread_no_permission(self):
  245. """api unpin thread with no permission fails"""
  246. self.thread.weight = 2
  247. self.thread.save()
  248. thread_json = self.get_thread_json()
  249. self.assertEqual(thread_json['weight'], 2)
  250. response = self.patch(
  251. self.api_link, [
  252. {
  253. 'op': 'replace',
  254. 'path': 'weight',
  255. 'value': 1,
  256. },
  257. ]
  258. )
  259. self.assertEqual(response.status_code, 400)
  260. response_json = response.json()
  261. self.assertEqual(
  262. response_json['detail'][0], "You can't change globally pinned threads weights in this category."
  263. )
  264. thread_json = self.get_thread_json()
  265. self.assertEqual(thread_json['weight'], 2)
  266. class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
  267. @patch_category_acl({'can_pin_threads': 1})
  268. def test_pin_thread(self):
  269. """api makes it possible to pin locally thread"""
  270. response = self.patch(
  271. self.api_link, [
  272. {
  273. 'op': 'replace',
  274. 'path': 'weight',
  275. 'value': 1,
  276. },
  277. ]
  278. )
  279. self.assertEqual(response.status_code, 200)
  280. response_json = response.json()
  281. self.assertEqual(response_json['weight'], 1)
  282. thread_json = self.get_thread_json()
  283. self.assertEqual(thread_json['weight'], 1)
  284. @patch_category_acl({'can_pin_threads': 1})
  285. def test_unpin_thread(self):
  286. """api makes it possible to unpin thread"""
  287. self.thread.weight = 1
  288. self.thread.save()
  289. thread_json = self.get_thread_json()
  290. self.assertEqual(thread_json['weight'], 1)
  291. response = self.patch(
  292. self.api_link, [
  293. {
  294. 'op': 'replace',
  295. 'path': 'weight',
  296. 'value': 0,
  297. },
  298. ]
  299. )
  300. self.assertEqual(response.status_code, 200)
  301. response_json = response.json()
  302. self.assertEqual(response_json['weight'], 0)
  303. thread_json = self.get_thread_json()
  304. self.assertEqual(thread_json['weight'], 0)
  305. @patch_category_acl({'can_pin_threads': 0})
  306. def test_pin_thread_no_permission(self):
  307. """api pin thread locally with no permission fails"""
  308. response = self.patch(
  309. self.api_link, [
  310. {
  311. 'op': 'replace',
  312. 'path': 'weight',
  313. 'value': 1,
  314. },
  315. ]
  316. )
  317. self.assertEqual(response.status_code, 400)
  318. response_json = response.json()
  319. self.assertEqual(
  320. response_json['detail'][0], "You can't change threads weights in this category."
  321. )
  322. thread_json = self.get_thread_json()
  323. self.assertEqual(thread_json['weight'], 0)
  324. @patch_category_acl({'can_pin_threads': 0})
  325. def test_unpin_thread_no_permission(self):
  326. """api unpin thread with no permission fails"""
  327. self.thread.weight = 1
  328. self.thread.save()
  329. thread_json = self.get_thread_json()
  330. self.assertEqual(thread_json['weight'], 1)
  331. response = self.patch(
  332. self.api_link, [
  333. {
  334. 'op': 'replace',
  335. 'path': 'weight',
  336. 'value': 0,
  337. },
  338. ]
  339. )
  340. self.assertEqual(response.status_code, 400)
  341. response_json = response.json()
  342. self.assertEqual(
  343. response_json['detail'][0], "You can't change threads weights in this category."
  344. )
  345. thread_json = self.get_thread_json()
  346. self.assertEqual(thread_json['weight'], 1)
  347. class ThreadMoveApiTests(ThreadPatchApiTestCase):
  348. def setUp(self):
  349. super().setUp()
  350. Category(
  351. name='Other category',
  352. slug='other-category',
  353. ).insert_at(
  354. self.category,
  355. position='last-child',
  356. save=True,
  357. )
  358. self.dst_category = Category.objects.get(slug='other-category')
  359. @patch_other_category_acl({'can_start_threads': 2})
  360. @patch_category_acl({'can_move_threads': True})
  361. def test_move_thread_no_top(self):
  362. """api moves thread to other category, sets no top category"""
  363. response = self.patch(
  364. self.api_link, [
  365. {
  366. 'op': 'replace',
  367. 'path': 'category',
  368. 'value': self.dst_category.pk,
  369. },
  370. {
  371. 'op': 'add',
  372. 'path': 'top-category',
  373. 'value': self.dst_category.pk,
  374. },
  375. {
  376. 'op': 'replace',
  377. 'path': 'flatten-categories',
  378. 'value': None,
  379. },
  380. ]
  381. )
  382. self.assertEqual(response.status_code, 200)
  383. reponse_json = response.json()
  384. self.assertEqual(reponse_json['category'], self.dst_category.pk)
  385. thread_json = self.get_thread_json()
  386. self.assertEqual(thread_json['category']['id'], self.dst_category.pk)
  387. @patch_other_category_acl({'can_start_threads': 2})
  388. @patch_category_acl({'can_move_threads': True})
  389. def test_move_thread_with_top(self):
  390. """api moves thread to other category, sets top"""
  391. response = self.patch(
  392. self.api_link, [
  393. {
  394. 'op': 'replace',
  395. 'path': 'category',
  396. 'value': self.dst_category.pk,
  397. },
  398. {
  399. 'op': 'add',
  400. 'path': 'top-category',
  401. 'value': Category.objects.root_category().pk,
  402. },
  403. {
  404. 'op': 'replace',
  405. 'path': 'flatten-categories',
  406. 'value': None,
  407. },
  408. ]
  409. )
  410. self.assertEqual(response.status_code, 200)
  411. reponse_json = response.json()
  412. self.assertEqual(reponse_json['category'], self.dst_category.pk)
  413. thread_json = self.get_thread_json()
  414. self.assertEqual(thread_json['category']['id'], self.dst_category.pk)
  415. @patch_other_category_acl({'can_start_threads': 2})
  416. @patch_category_acl({'can_move_threads': True})
  417. def test_move_thread_reads(self):
  418. """api moves thread reads together with thread"""
  419. poststracker.save_read(self.user, self.thread.first_post)
  420. self.assertEqual(self.user.postread_set.count(), 1)
  421. self.user.postread_set.get(category=self.category)
  422. response = self.patch(
  423. self.api_link, [
  424. {
  425. 'op': 'replace',
  426. 'path': 'category',
  427. 'value': self.dst_category.pk,
  428. },
  429. {
  430. 'op': 'add',
  431. 'path': 'top-category',
  432. 'value': self.dst_category.pk,
  433. },
  434. {
  435. 'op': 'replace',
  436. 'path': 'flatten-categories',
  437. 'value': None,
  438. },
  439. ]
  440. )
  441. self.assertEqual(response.status_code, 200)
  442. # thread read was moved to new category
  443. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  444. self.assertEqual(postreads.count(), 1)
  445. postreads.get(category=self.dst_category)
  446. @patch_other_category_acl({'can_start_threads': 2})
  447. @patch_category_acl({'can_move_threads': True})
  448. def test_move_thread_subscriptions(self):
  449. """api moves thread subscriptions together with thread"""
  450. self.user.subscription_set.create(
  451. thread=self.thread,
  452. category=self.thread.category,
  453. last_read_on=self.thread.last_post_on,
  454. send_email=False,
  455. )
  456. self.assertEqual(self.user.subscription_set.count(), 1)
  457. self.user.subscription_set.get(category=self.category)
  458. response = self.patch(
  459. self.api_link, [
  460. {
  461. 'op': 'replace',
  462. 'path': 'category',
  463. 'value': self.dst_category.pk,
  464. },
  465. {
  466. 'op': 'add',
  467. 'path': 'top-category',
  468. 'value': self.dst_category.pk,
  469. },
  470. {
  471. 'op': 'replace',
  472. 'path': 'flatten-categories',
  473. 'value': None,
  474. },
  475. ]
  476. )
  477. self.assertEqual(response.status_code, 200)
  478. # thread read was moved to new category
  479. self.assertEqual(self.user.subscription_set.count(), 1)
  480. self.user.subscription_set.get(category=self.dst_category)
  481. @patch_category_acl({'can_move_threads': False})
  482. def test_move_thread_no_permission(self):
  483. """api move thread to other category with no permission fails"""
  484. response = self.patch(
  485. self.api_link, [
  486. {
  487. 'op': 'replace',
  488. 'path': 'category',
  489. 'value': self.dst_category.pk,
  490. },
  491. ]
  492. )
  493. self.assertEqual(response.status_code, 400)
  494. response_json = response.json()
  495. self.assertEqual(
  496. response_json['detail'][0], "You can't move threads in this category."
  497. )
  498. thread_json = self.get_thread_json()
  499. self.assertEqual(thread_json['category']['id'], self.category.pk)
  500. @patch_other_category_acl({'can_close_threads': False})
  501. @patch_category_acl({'can_move_threads': True})
  502. def test_move_thread_closed_category_no_permission(self):
  503. """api move thread from closed category with no permission fails"""
  504. self.category.is_closed = True
  505. self.category.save()
  506. response = self.patch(
  507. self.api_link, [
  508. {
  509. 'op': 'replace',
  510. 'path': 'category',
  511. 'value': self.dst_category.pk,
  512. },
  513. ]
  514. )
  515. self.assertEqual(response.status_code, 400)
  516. response_json = response.json()
  517. self.assertEqual(
  518. response_json['detail'][0], "This category is closed. You can't move it's threads."
  519. )
  520. @patch_other_category_acl({'can_close_threads': False})
  521. @patch_category_acl({'can_move_threads': True})
  522. def test_move_closed_thread_no_permission(self):
  523. """api move closed thread with no permission fails"""
  524. self.thread.is_closed = True
  525. self.thread.save()
  526. response = self.patch(
  527. self.api_link, [
  528. {
  529. 'op': 'replace',
  530. 'path': 'category',
  531. 'value': self.dst_category.pk,
  532. },
  533. ]
  534. )
  535. self.assertEqual(response.status_code, 400)
  536. response_json = response.json()
  537. self.assertEqual(
  538. response_json['detail'][0], "This thread is closed. You can't move it."
  539. )
  540. @patch_other_category_acl({'can_see': False})
  541. @patch_category_acl({'can_move_threads': True})
  542. def test_move_thread_no_category_access(self):
  543. """api move thread to category with no access fails"""
  544. response = self.patch(
  545. self.api_link, [
  546. {
  547. 'op': 'replace',
  548. 'path': 'category',
  549. 'value': self.dst_category.pk,
  550. },
  551. ]
  552. )
  553. self.assertEqual(response.status_code, 400)
  554. response_json = response.json()
  555. self.assertEqual(response_json['detail'][0], 'NOT FOUND')
  556. thread_json = self.get_thread_json()
  557. self.assertEqual(thread_json['category']['id'], self.category.pk)
  558. @patch_other_category_acl({'can_browse': False})
  559. @patch_category_acl({'can_move_threads': True})
  560. def test_move_thread_no_category_browse(self):
  561. """api move thread to category with no browsing access fails"""
  562. response = self.patch(
  563. self.api_link, [
  564. {
  565. 'op': 'replace',
  566. 'path': 'category',
  567. 'value': self.dst_category.pk,
  568. },
  569. ]
  570. )
  571. self.assertEqual(response.status_code, 400)
  572. response_json = response.json()
  573. self.assertEqual(
  574. response_json['detail'][0],
  575. 'You don\'t have permission to browse "Other category" contents.'
  576. )
  577. thread_json = self.get_thread_json()
  578. self.assertEqual(thread_json['category']['id'], self.category.pk)
  579. @patch_other_category_acl({'can_start_threads': False})
  580. @patch_category_acl({'can_move_threads': True})
  581. def test_move_thread_no_category_start_threads(self):
  582. """api move thread to category with no posting access fails"""
  583. response = self.patch(
  584. self.api_link, [
  585. {
  586. 'op': 'replace',
  587. 'path': 'category',
  588. 'value': self.dst_category.pk,
  589. },
  590. ]
  591. )
  592. self.assertEqual(response.status_code, 400)
  593. response_json = response.json()
  594. self.assertEqual(
  595. response_json['detail'][0],
  596. "You don't have permission to start new threads in this category."
  597. )
  598. thread_json = self.get_thread_json()
  599. self.assertEqual(thread_json['category']['id'], self.category.pk)
  600. @patch_other_category_acl({'can_start_threads': 2})
  601. @patch_category_acl({'can_move_threads': True})
  602. def test_move_thread_same_category(self):
  603. """api move thread to category it's already in fails"""
  604. response = self.patch(
  605. self.api_link, [
  606. {
  607. 'op': 'replace',
  608. 'path': 'category',
  609. 'value': self.thread.category_id,
  610. },
  611. ]
  612. )
  613. self.assertEqual(response.status_code, 400)
  614. response_json = response.json()
  615. self.assertEqual(
  616. response_json['detail'][0], "You can't move thread to the category it's already in."
  617. )
  618. thread_json = self.get_thread_json()
  619. self.assertEqual(thread_json['category']['id'], self.category.pk)
  620. def test_thread_flatten_categories(self):
  621. """api flatten thread categories"""
  622. response = self.patch(
  623. self.api_link, [
  624. {
  625. 'op': 'replace',
  626. 'path': 'flatten-categories',
  627. 'value': None,
  628. },
  629. ]
  630. )
  631. self.assertEqual(response.status_code, 200)
  632. response_json = response.json()
  633. self.assertEqual(response_json['category'], self.category.pk)
  634. class ThreadCloseApiTests(ThreadPatchApiTestCase):
  635. @patch_category_acl({'can_close_threads': True})
  636. def test_close_thread(self):
  637. """api makes it possible to close thread"""
  638. response = self.patch(
  639. self.api_link, [
  640. {
  641. 'op': 'replace',
  642. 'path': 'is-closed',
  643. 'value': True,
  644. },
  645. ]
  646. )
  647. self.assertEqual(response.status_code, 200)
  648. response_json = response.json()
  649. self.assertTrue(response_json['is_closed'])
  650. thread_json = self.get_thread_json()
  651. self.assertTrue(thread_json['is_closed'])
  652. @patch_category_acl({'can_close_threads': True})
  653. def test_open_thread(self):
  654. """api makes it possible to open thread"""
  655. self.thread.is_closed = True
  656. self.thread.save()
  657. thread_json = self.get_thread_json()
  658. self.assertTrue(thread_json['is_closed'])
  659. response = self.patch(
  660. self.api_link, [
  661. {
  662. 'op': 'replace',
  663. 'path': 'is-closed',
  664. 'value': False,
  665. },
  666. ]
  667. )
  668. self.assertEqual(response.status_code, 200)
  669. response_json = response.json()
  670. self.assertFalse(response_json['is_closed'])
  671. thread_json = self.get_thread_json()
  672. self.assertFalse(thread_json['is_closed'])
  673. @patch_category_acl({'can_close_threads': False})
  674. def test_close_thread_no_permission(self):
  675. """api close thread with no permission fails"""
  676. response = self.patch(
  677. self.api_link, [
  678. {
  679. 'op': 'replace',
  680. 'path': 'is-closed',
  681. 'value': True,
  682. },
  683. ]
  684. )
  685. self.assertEqual(response.status_code, 400)
  686. response_json = response.json()
  687. self.assertEqual(
  688. response_json['detail'][0], "You don't have permission to close this thread."
  689. )
  690. thread_json = self.get_thread_json()
  691. self.assertFalse(thread_json['is_closed'])
  692. @patch_category_acl({'can_close_threads': False})
  693. def test_open_thread_no_permission(self):
  694. """api open thread with no permission fails"""
  695. self.thread.is_closed = True
  696. self.thread.save()
  697. thread_json = self.get_thread_json()
  698. self.assertTrue(thread_json['is_closed'])
  699. response = self.patch(
  700. self.api_link, [
  701. {
  702. 'op': 'replace',
  703. 'path': 'is-closed',
  704. 'value': False,
  705. },
  706. ]
  707. )
  708. self.assertEqual(response.status_code, 400)
  709. response_json = response.json()
  710. self.assertEqual(
  711. response_json['detail'][0], "You don't have permission to open this thread."
  712. )
  713. thread_json = self.get_thread_json()
  714. self.assertTrue(thread_json['is_closed'])
  715. class ThreadApproveApiTests(ThreadPatchApiTestCase):
  716. @patch_category_acl({'can_approve_content': True})
  717. def test_approve_thread(self):
  718. """api makes it possible to approve thread"""
  719. self.thread.first_post.is_unapproved = True
  720. self.thread.first_post.save()
  721. self.thread.synchronize()
  722. self.thread.save()
  723. self.assertTrue(self.thread.is_unapproved)
  724. self.assertTrue(self.thread.has_unapproved_posts)
  725. response = self.patch(
  726. self.api_link, [
  727. {
  728. 'op': 'replace',
  729. 'path': 'is-unapproved',
  730. 'value': False,
  731. },
  732. ]
  733. )
  734. self.assertEqual(response.status_code, 200)
  735. response_json = response.json()
  736. self.assertFalse(response_json['is_unapproved'])
  737. self.assertFalse(response_json['has_unapproved_posts'])
  738. thread_json = self.get_thread_json()
  739. self.assertFalse(thread_json['is_unapproved'])
  740. self.assertFalse(thread_json['has_unapproved_posts'])
  741. thread = Thread.objects.get(pk=self.thread.pk)
  742. self.assertFalse(thread.is_unapproved)
  743. self.assertFalse(thread.has_unapproved_posts)
  744. @patch_category_acl({'can_approve_content': True, 'can_close_threads': False})
  745. def test_approve_thread_category_closed_no_permission(self):
  746. """api checks permission for approving threads in closed categories"""
  747. self.thread.first_post.is_unapproved = True
  748. self.thread.first_post.save()
  749. self.thread.synchronize()
  750. self.thread.save()
  751. self.assertTrue(self.thread.is_unapproved)
  752. self.assertTrue(self.thread.has_unapproved_posts)
  753. self.category.is_closed = True
  754. self.category.save()
  755. response = self.patch(
  756. self.api_link, [
  757. {
  758. 'op': 'replace',
  759. 'path': 'is-unapproved',
  760. 'value': False,
  761. },
  762. ]
  763. )
  764. self.assertEqual(response.status_code, 400)
  765. response_json = response.json()
  766. self.assertEqual(response_json['detail'][0], "This category is closed. You can't approve threads in it.")
  767. @patch_category_acl({'can_approve_content': True, 'can_close_threads': False})
  768. def test_approve_thread_closed_no_permission(self):
  769. """api checks permission for approving posts in closed categories"""
  770. self.thread.first_post.is_unapproved = True
  771. self.thread.first_post.save()
  772. self.thread.synchronize()
  773. self.thread.save()
  774. self.assertTrue(self.thread.is_unapproved)
  775. self.assertTrue(self.thread.has_unapproved_posts)
  776. self.thread.is_closed = True
  777. self.thread.save()
  778. response = self.patch(
  779. self.api_link, [
  780. {
  781. 'op': 'replace',
  782. 'path': 'is-unapproved',
  783. 'value': False,
  784. },
  785. ]
  786. )
  787. self.assertEqual(response.status_code, 400)
  788. response_json = response.json()
  789. self.assertEqual(response_json['detail'][0], "This thread is closed. You can't approve it.")
  790. @patch_category_acl({'can_approve_content': True})
  791. def test_unapprove_thread(self):
  792. """api returns permission error on approval removal"""
  793. response = self.patch(
  794. self.api_link, [
  795. {
  796. 'op': 'replace',
  797. 'path': 'is-unapproved',
  798. 'value': True,
  799. },
  800. ]
  801. )
  802. self.assertEqual(response.status_code, 400)
  803. response_json = response.json()
  804. self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
  805. class ThreadHideApiTests(ThreadPatchApiTestCase):
  806. @patch_category_acl({'can_hide_threads': True})
  807. def test_hide_thread(self):
  808. """api makes it possible to hide thread"""
  809. response = self.patch(
  810. self.api_link, [
  811. {
  812. 'op': 'replace',
  813. 'path': 'is-hidden',
  814. 'value': True,
  815. },
  816. ]
  817. )
  818. self.assertEqual(response.status_code, 200)
  819. reponse_json = response.json()
  820. self.assertTrue(reponse_json['is_hidden'])
  821. thread_json = self.get_thread_json()
  822. self.assertTrue(thread_json['is_hidden'])
  823. @patch_category_acl({'can_hide_threads': False})
  824. def test_hide_thread_no_permission(self):
  825. """api hide thread with no permission fails"""
  826. response = self.patch(
  827. self.api_link, [
  828. {
  829. 'op': 'replace',
  830. 'path': 'is-hidden',
  831. 'value': True,
  832. },
  833. ]
  834. )
  835. self.assertEqual(response.status_code, 400)
  836. response_json = response.json()
  837. self.assertEqual(
  838. response_json['detail'][0], "You can't hide threads in this category."
  839. )
  840. thread_json = self.get_thread_json()
  841. self.assertFalse(thread_json['is_hidden'])
  842. @patch_category_acl({'can_hide_threads': False, 'can_hide_own_threads': True})
  843. def test_hide_non_owned_thread(self):
  844. """api forbids non-moderator from hiding other users threads"""
  845. response = self.patch(
  846. self.api_link, [
  847. {
  848. 'op': 'replace',
  849. 'path': 'is-hidden',
  850. 'value': True,
  851. },
  852. ]
  853. )
  854. self.assertEqual(response.status_code, 400)
  855. response_json = response.json()
  856. self.assertEqual(
  857. response_json['detail'][0], "You can't hide other users theads in this category."
  858. )
  859. @patch_category_acl({
  860. 'can_hide_threads': False,
  861. 'can_hide_own_threads': True,
  862. 'thread_edit_time': 1,
  863. })
  864. def test_hide_owned_thread_no_time(self):
  865. """api forbids non-moderator from hiding other users threads"""
  866. self.thread.started_on = timezone.now() - timedelta(minutes=5)
  867. self.thread.starter = self.user
  868. self.thread.save()
  869. response = self.patch(
  870. self.api_link, [
  871. {
  872. 'op': 'replace',
  873. 'path': 'is-hidden',
  874. 'value': True,
  875. },
  876. ]
  877. )
  878. self.assertEqual(response.status_code, 400)
  879. response_json = response.json()
  880. self.assertEqual(
  881. response_json['detail'][0], "You can't hide threads that are older than 1 minute."
  882. )
  883. @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False})
  884. def test_hide_closed_category_no_permission(self):
  885. """api test permission to hide thread in closed category"""
  886. self.category.is_closed = True
  887. self.category.save()
  888. response = self.patch(
  889. self.api_link, [
  890. {
  891. 'op': 'replace',
  892. 'path': 'is-hidden',
  893. 'value': True,
  894. },
  895. ]
  896. )
  897. self.assertEqual(response.status_code, 400)
  898. response_json = response.json()
  899. self.assertEqual(
  900. response_json['detail'][0], "This category is closed. You can't hide threads in it."
  901. )
  902. @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False})
  903. def test_hide_closed_thread_no_permission(self):
  904. """api test permission to hide closed thread"""
  905. self.thread.is_closed = True
  906. self.thread.save()
  907. response = self.patch(
  908. self.api_link, [
  909. {
  910. 'op': 'replace',
  911. 'path': 'is-hidden',
  912. 'value': True,
  913. },
  914. ]
  915. )
  916. self.assertEqual(response.status_code, 400)
  917. response_json = response.json()
  918. self.assertEqual(
  919. response_json['detail'][0], "This thread is closed. You can't hide it."
  920. )
  921. class ThreadUnhideApiTests(ThreadPatchApiTestCase):
  922. def setUp(self):
  923. super().setUp()
  924. self.thread.is_hidden = True
  925. self.thread.save()
  926. @patch_category_acl({'can_hide_threads': True})
  927. def test_unhide_thread(self):
  928. """api makes it possible to unhide thread"""
  929. response = self.patch(
  930. self.api_link, [
  931. {
  932. 'op': 'replace',
  933. 'path': 'is-hidden',
  934. 'value': False,
  935. },
  936. ]
  937. )
  938. self.assertEqual(response.status_code, 200)
  939. reponse_json = response.json()
  940. self.assertFalse(reponse_json['is_hidden'])
  941. thread_json = self.get_thread_json()
  942. self.assertFalse(thread_json['is_hidden'])
  943. @patch_category_acl({'can_hide_threads': False})
  944. def test_unhide_thread_no_permission(self):
  945. """api unhide thread with no permission fails as thread is invisible"""
  946. response = self.patch(
  947. self.api_link, [
  948. {
  949. 'op': 'replace',
  950. 'path': 'is-hidden',
  951. 'value': True,
  952. },
  953. ]
  954. )
  955. self.assertEqual(response.status_code, 404)
  956. @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False})
  957. def test_unhide_closed_category_no_permission(self):
  958. """api test permission to unhide thread in closed category"""
  959. self.category.is_closed = True
  960. self.category.save()
  961. response = self.patch(
  962. self.api_link, [
  963. {
  964. 'op': 'replace',
  965. 'path': 'is-hidden',
  966. 'value': False,
  967. },
  968. ]
  969. )
  970. self.assertEqual(response.status_code, 400)
  971. response_json = response.json()
  972. self.assertEqual(
  973. response_json['detail'][0], "This category is closed. You can't reveal threads in it."
  974. )
  975. @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False})
  976. def test_unhide_closed_thread_no_permission(self):
  977. """api test permission to unhide closed thread"""
  978. self.thread.is_closed = True
  979. self.thread.save()
  980. response = self.patch(
  981. self.api_link, [
  982. {
  983. 'op': 'replace',
  984. 'path': 'is-hidden',
  985. 'value': False,
  986. },
  987. ]
  988. )
  989. self.assertEqual(response.status_code, 400)
  990. response_json = response.json()
  991. self.assertEqual(
  992. response_json['detail'][0], "This thread is closed. You can't reveal it."
  993. )
  994. class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
  995. def test_subscribe_thread(self):
  996. """api makes it possible to subscribe thread"""
  997. response = self.patch(
  998. self.api_link, [
  999. {
  1000. 'op': 'replace',
  1001. 'path': 'subscription',
  1002. 'value': 'notify',
  1003. },
  1004. ]
  1005. )
  1006. self.assertEqual(response.status_code, 200)
  1007. reponse_json = response.json()
  1008. self.assertFalse(reponse_json['subscription'])
  1009. thread_json = self.get_thread_json()
  1010. self.assertFalse(thread_json['subscription'])
  1011. subscription = self.user.subscription_set.get(thread=self.thread)
  1012. self.assertFalse(subscription.send_email)
  1013. def test_subscribe_thread_with_email(self):
  1014. """api makes it possible to subscribe thread with emails"""
  1015. response = self.patch(
  1016. self.api_link, [
  1017. {
  1018. 'op': 'replace',
  1019. 'path': 'subscription',
  1020. 'value': 'email',
  1021. },
  1022. ]
  1023. )
  1024. self.assertEqual(response.status_code, 200)
  1025. reponse_json = response.json()
  1026. self.assertTrue(reponse_json['subscription'])
  1027. thread_json = self.get_thread_json()
  1028. self.assertTrue(thread_json['subscription'])
  1029. subscription = self.user.subscription_set.get(thread=self.thread)
  1030. self.assertTrue(subscription.send_email)
  1031. def test_unsubscribe_thread(self):
  1032. """api makes it possible to unsubscribe thread"""
  1033. response = self.patch(
  1034. self.api_link, [
  1035. {
  1036. 'op': 'replace',
  1037. 'path': 'subscription',
  1038. 'value': 'remove',
  1039. },
  1040. ]
  1041. )
  1042. self.assertEqual(response.status_code, 200)
  1043. reponse_json = response.json()
  1044. self.assertIsNone(reponse_json['subscription'])
  1045. thread_json = self.get_thread_json()
  1046. self.assertIsNone(thread_json['subscription'])
  1047. self.assertEqual(self.user.subscription_set.count(), 0)
  1048. def test_subscribe_as_guest(self):
  1049. """api makes it impossible to subscribe thread"""
  1050. self.logout_user()
  1051. response = self.patch(
  1052. self.api_link, [
  1053. {
  1054. 'op': 'replace',
  1055. 'path': 'subscription',
  1056. 'value': 'email',
  1057. },
  1058. ]
  1059. )
  1060. self.assertEqual(response.status_code, 403)
  1061. def test_subscribe_nonexistant_thread(self):
  1062. """api makes it impossible to subscribe nonexistant thread"""
  1063. bad_api_link = self.api_link.replace(
  1064. str(self.thread.pk), str(self.thread.pk + 9)
  1065. )
  1066. response = self.patch(
  1067. bad_api_link, [
  1068. {
  1069. 'op': 'replace',
  1070. 'path': 'subscription',
  1071. 'value': 'email',
  1072. },
  1073. ]
  1074. )
  1075. self.assertEqual(response.status_code, 404)
  1076. class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
  1077. @patch_category_acl({'can_mark_best_answers': 2})
  1078. def test_mark_best_answer(self):
  1079. """api makes it possible to mark best answer"""
  1080. best_answer = testutils.reply_thread(self.thread)
  1081. response = self.patch(
  1082. self.api_link, [
  1083. {
  1084. 'op': 'replace',
  1085. 'path': 'best-answer',
  1086. 'value': best_answer.id,
  1087. },
  1088. ]
  1089. )
  1090. self.assertEqual(response.status_code, 200)
  1091. self.assertEqual(response.json(), {
  1092. 'id': self.thread.id,
  1093. 'detail': ['ok'],
  1094. 'best_answer': best_answer.id,
  1095. 'best_answer_is_protected': False,
  1096. 'best_answer_marked_on': response.json()['best_answer_marked_on'],
  1097. 'best_answer_marked_by': self.user.id,
  1098. 'best_answer_marked_by_name': self.user.username,
  1099. 'best_answer_marked_by_slug': self.user.slug,
  1100. })
  1101. thread_json = self.get_thread_json()
  1102. self.assertEqual(thread_json['best_answer'], best_answer.id)
  1103. self.assertEqual(thread_json['best_answer_is_protected'], False)
  1104. self.assertEqual(
  1105. thread_json['best_answer_marked_on'], response.json()['best_answer_marked_on'])
  1106. self.assertEqual(thread_json['best_answer_marked_by'], self.user.id)
  1107. self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username)
  1108. self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug)
  1109. @patch_category_acl({'can_mark_best_answers': 2})
  1110. def test_mark_best_answer_anonymous(self):
  1111. """api validates that user is authenticated before marking best answer"""
  1112. self.logout_user()
  1113. best_answer = testutils.reply_thread(self.thread)
  1114. response = self.patch(
  1115. self.api_link, [
  1116. {
  1117. 'op': 'replace',
  1118. 'path': 'best-answer',
  1119. 'value': best_answer.id,
  1120. },
  1121. ]
  1122. )
  1123. self.assertEqual(response.status_code, 403)
  1124. self.assertEqual(response.json(), {
  1125. 'detail': "This action is not available to guests.",
  1126. })
  1127. thread_json = self.get_thread_json()
  1128. self.assertIsNone(thread_json['best_answer'])
  1129. @patch_category_acl({'can_mark_best_answers': 0})
  1130. def test_mark_best_answer_no_permission(self):
  1131. """api validates permission to mark best answers"""
  1132. best_answer = testutils.reply_thread(self.thread)
  1133. response = self.patch(
  1134. self.api_link, [
  1135. {
  1136. 'op': 'replace',
  1137. 'path': 'best-answer',
  1138. 'value': best_answer.id,
  1139. },
  1140. ]
  1141. )
  1142. self.assertEqual(response.status_code, 400)
  1143. self.assertEqual(response.json(), {
  1144. 'id': self.thread.id,
  1145. 'detail': [
  1146. 'You don\'t have permission to mark best answers in the "First category" category.'
  1147. ],
  1148. })
  1149. thread_json = self.get_thread_json()
  1150. self.assertIsNone(thread_json['best_answer'])
  1151. @patch_category_acl({'can_mark_best_answers': 1})
  1152. def test_mark_best_answer_not_thread_starter(self):
  1153. """api validates permission to mark best answers in owned thread"""
  1154. best_answer = testutils.reply_thread(self.thread)
  1155. response = self.patch(
  1156. self.api_link, [
  1157. {
  1158. 'op': 'replace',
  1159. 'path': 'best-answer',
  1160. 'value': best_answer.id,
  1161. },
  1162. ]
  1163. )
  1164. self.assertEqual(response.status_code, 400)
  1165. self.assertEqual(response.json(), {
  1166. 'id': self.thread.id,
  1167. 'detail': [
  1168. "You don't have permission to mark best answer in this thread because you didn't "
  1169. "start it."
  1170. ],
  1171. })
  1172. thread_json = self.get_thread_json()
  1173. self.assertIsNone(thread_json['best_answer'])
  1174. # passing scenario is possible
  1175. self.thread.starter = self.user
  1176. self.thread.save()
  1177. response = self.patch(
  1178. self.api_link, [
  1179. {
  1180. 'op': 'replace',
  1181. 'path': 'best-answer',
  1182. 'value': best_answer.id,
  1183. },
  1184. ]
  1185. )
  1186. self.assertEqual(response.status_code, 200)
  1187. @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': False})
  1188. def test_mark_best_answer_category_closed_no_permission(self):
  1189. """api validates permission to mark best answers in closed category"""
  1190. best_answer = testutils.reply_thread(self.thread)
  1191. self.category.is_closed = True
  1192. self.category.save()
  1193. response = self.patch(
  1194. self.api_link, [
  1195. {
  1196. 'op': 'replace',
  1197. 'path': 'best-answer',
  1198. 'value': best_answer.id,
  1199. },
  1200. ]
  1201. )
  1202. self.assertEqual(response.status_code, 400)
  1203. self.assertEqual(response.json(), {
  1204. 'id': self.thread.id,
  1205. 'detail': [
  1206. 'You don\'t have permission to mark best answer in this thread because its '
  1207. 'category "First category" is closed.'
  1208. ],
  1209. })
  1210. thread_json = self.get_thread_json()
  1211. self.assertIsNone(thread_json['best_answer'])
  1212. @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': True})
  1213. def test_mark_best_answer_category_closed(self):
  1214. """api validates permission to mark best answers in closed category"""
  1215. best_answer = testutils.reply_thread(self.thread)
  1216. self.category.is_closed = True
  1217. self.category.save()
  1218. response = self.patch(
  1219. self.api_link, [
  1220. {
  1221. 'op': 'replace',
  1222. 'path': 'best-answer',
  1223. 'value': best_answer.id,
  1224. },
  1225. ]
  1226. )
  1227. self.assertEqual(response.status_code, 200)
  1228. @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': False})
  1229. def test_mark_best_answer_thread_closed_no_permission(self):
  1230. """api validates permission to mark best answers in closed thread"""
  1231. best_answer = testutils.reply_thread(self.thread)
  1232. self.thread.is_closed = True
  1233. self.thread.save()
  1234. response = self.patch(
  1235. self.api_link, [
  1236. {
  1237. 'op': 'replace',
  1238. 'path': 'best-answer',
  1239. 'value': best_answer.id,
  1240. },
  1241. ]
  1242. )
  1243. self.assertEqual(response.status_code, 400)
  1244. self.assertEqual(response.json(), {
  1245. 'id': self.thread.id,
  1246. 'detail': [
  1247. "You can't mark best answer in this thread because it's closed and you don't have "
  1248. "permission to open it."
  1249. ],
  1250. })
  1251. thread_json = self.get_thread_json()
  1252. self.assertIsNone(thread_json['best_answer'])
  1253. @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': True})
  1254. def test_mark_best_answer_thread_closed(self):
  1255. """api validates permission to mark best answers in closed thread"""
  1256. best_answer = testutils.reply_thread(self.thread)
  1257. self.thread.is_closed = True
  1258. self.thread.save()
  1259. response = self.patch(
  1260. self.api_link, [
  1261. {
  1262. 'op': 'replace',
  1263. 'path': 'best-answer',
  1264. 'value': best_answer.id,
  1265. },
  1266. ]
  1267. )
  1268. self.assertEqual(response.status_code, 200)
  1269. @patch_category_acl({'can_mark_best_answers': 2})
  1270. def test_mark_best_answer_invalid_post_id(self):
  1271. """api validates that post id is int"""
  1272. response = self.patch(
  1273. self.api_link, [
  1274. {
  1275. 'op': 'replace',
  1276. 'path': 'best-answer',
  1277. 'value': 'd7sd89a7d98sa',
  1278. },
  1279. ]
  1280. )
  1281. self.assertEqual(response.status_code, 400)
  1282. self.assertEqual(response.json(), {
  1283. 'id': self.thread.id,
  1284. 'detail': ["A valid integer is required."],
  1285. })
  1286. thread_json = self.get_thread_json()
  1287. self.assertIsNone(thread_json['best_answer'])
  1288. @patch_category_acl({'can_mark_best_answers': 2})
  1289. def test_mark_best_answer_post_not_found(self):
  1290. """api validates that post exists"""
  1291. response = self.patch(
  1292. self.api_link, [
  1293. {
  1294. 'op': 'replace',
  1295. 'path': 'best-answer',
  1296. 'value': self.thread.last_post_id + 1,
  1297. },
  1298. ]
  1299. )
  1300. self.assertEqual(response.status_code, 400)
  1301. self.assertEqual(response.json(), {
  1302. 'id': self.thread.id,
  1303. 'detail': ["NOT FOUND"],
  1304. })
  1305. thread_json = self.get_thread_json()
  1306. self.assertIsNone(thread_json['best_answer'])
  1307. @patch_category_acl({'can_mark_best_answers': 2})
  1308. def test_mark_best_answer_post_invisible(self):
  1309. """api validates post visibility to action author"""
  1310. unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True)
  1311. response = self.patch(
  1312. self.api_link, [
  1313. {
  1314. 'op': 'replace',
  1315. 'path': 'best-answer',
  1316. 'value': unapproved_post.id,
  1317. },
  1318. ]
  1319. )
  1320. self.assertEqual(response.status_code, 400)
  1321. self.assertEqual(response.json(), {
  1322. 'id': self.thread.id,
  1323. 'detail': ["NOT FOUND"],
  1324. })
  1325. thread_json = self.get_thread_json()
  1326. self.assertIsNone(thread_json['best_answer'])
  1327. @patch_category_acl({'can_mark_best_answers': 2})
  1328. def test_mark_best_answer_post_other_thread(self):
  1329. """api validates post belongs to same thread"""
  1330. other_thread = testutils.post_thread(self.category)
  1331. response = self.patch(
  1332. self.api_link, [
  1333. {
  1334. 'op': 'replace',
  1335. 'path': 'best-answer',
  1336. 'value': other_thread.first_post_id,
  1337. },
  1338. ]
  1339. )
  1340. self.assertEqual(response.status_code, 400)
  1341. self.assertEqual(response.json(), {
  1342. 'id': self.thread.id,
  1343. 'detail': ["NOT FOUND"],
  1344. })
  1345. thread_json = self.get_thread_json()
  1346. self.assertIsNone(thread_json['best_answer'])
  1347. @patch_category_acl({'can_mark_best_answers': 2})
  1348. def test_mark_best_answer_event_id(self):
  1349. """api validates that post is not an event"""
  1350. best_answer = testutils.reply_thread(self.thread)
  1351. best_answer.is_event = True
  1352. best_answer.save()
  1353. response = self.patch(
  1354. self.api_link, [
  1355. {
  1356. 'op': 'replace',
  1357. 'path': 'best-answer',
  1358. 'value': best_answer.id,
  1359. },
  1360. ]
  1361. )
  1362. self.assertEqual(response.status_code, 400)
  1363. self.assertEqual(response.json(), {
  1364. 'id': self.thread.id,
  1365. 'detail': ["Events can't be marked as best answers."],
  1366. })
  1367. thread_json = self.get_thread_json()
  1368. self.assertIsNone(thread_json['best_answer'])
  1369. @patch_category_acl({'can_mark_best_answers': 2})
  1370. def test_mark_best_answer_first_post(self):
  1371. """api validates that post is not a first post in thread"""
  1372. response = self.patch(
  1373. self.api_link, [
  1374. {
  1375. 'op': 'replace',
  1376. 'path': 'best-answer',
  1377. 'value': self.thread.first_post_id,
  1378. },
  1379. ]
  1380. )
  1381. self.assertEqual(response.status_code, 400)
  1382. self.assertEqual(response.json(), {
  1383. 'id': self.thread.id,
  1384. 'detail': ["First post in a thread can't be marked as best answer."],
  1385. })
  1386. thread_json = self.get_thread_json()
  1387. self.assertIsNone(thread_json['best_answer'])
  1388. @patch_category_acl({'can_mark_best_answers': 2})
  1389. def test_mark_best_answer_hidden_post(self):
  1390. """api validates that post is not hidden"""
  1391. best_answer = testutils.reply_thread(self.thread, is_hidden=True)
  1392. response = self.patch(
  1393. self.api_link, [
  1394. {
  1395. 'op': 'replace',
  1396. 'path': 'best-answer',
  1397. 'value': best_answer.id,
  1398. },
  1399. ]
  1400. )
  1401. self.assertEqual(response.status_code, 400)
  1402. self.assertEqual(response.json(), {
  1403. 'id': self.thread.id,
  1404. 'detail': ["Hidden posts can't be marked as best answers."],
  1405. })
  1406. thread_json = self.get_thread_json()
  1407. self.assertIsNone(thread_json['best_answer'])
  1408. @patch_category_acl({'can_mark_best_answers': 2})
  1409. def test_mark_best_answer_unapproved_post(self):
  1410. """api validates that post is not unapproved"""
  1411. best_answer = testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True)
  1412. response = self.patch(
  1413. self.api_link, [
  1414. {
  1415. 'op': 'replace',
  1416. 'path': 'best-answer',
  1417. 'value': best_answer.id,
  1418. },
  1419. ]
  1420. )
  1421. self.assertEqual(response.status_code, 400)
  1422. self.assertEqual(response.json(), {
  1423. 'id': self.thread.id,
  1424. 'detail': ["Unapproved posts can't be marked as best answers."],
  1425. })
  1426. thread_json = self.get_thread_json()
  1427. self.assertIsNone(thread_json['best_answer'])
  1428. @patch_category_acl({'can_mark_best_answers': 2, 'can_protect_posts': False})
  1429. def test_mark_best_answer_protected_post_no_permission(self):
  1430. """api respects post protection"""
  1431. best_answer = testutils.reply_thread(self.thread, is_protected=True)
  1432. response = self.patch(
  1433. self.api_link, [
  1434. {
  1435. 'op': 'replace',
  1436. 'path': 'best-answer',
  1437. 'value': best_answer.id,
  1438. },
  1439. ]
  1440. )
  1441. self.assertEqual(response.status_code, 400)
  1442. self.assertEqual(response.json(), {
  1443. 'id': self.thread.id,
  1444. 'detail': [
  1445. "You don't have permission to mark this post as best answer because a moderator "
  1446. "has protected it."
  1447. ],
  1448. })
  1449. thread_json = self.get_thread_json()
  1450. self.assertIsNone(thread_json['best_answer'])
  1451. @patch_category_acl({'can_mark_best_answers': 2, 'can_protect_posts': True})
  1452. def test_mark_best_answer_protected_post(self):
  1453. """api respects post protection"""
  1454. best_answer = testutils.reply_thread(self.thread, is_protected=True)
  1455. response = self.patch(
  1456. self.api_link, [
  1457. {
  1458. 'op': 'replace',
  1459. 'path': 'best-answer',
  1460. 'value': best_answer.id,
  1461. },
  1462. ]
  1463. )
  1464. self.assertEqual(response.status_code, 200)
  1465. class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
  1466. def setUp(self):
  1467. super().setUp()
  1468. self.best_answer = testutils.reply_thread(self.thread)
  1469. self.thread.set_best_answer(self.user, self.best_answer)
  1470. self.thread.save()
  1471. @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2})
  1472. def test_change_best_answer(self):
  1473. """api makes it possible to change best answer"""
  1474. best_answer = testutils.reply_thread(self.thread)
  1475. response = self.patch(
  1476. self.api_link, [
  1477. {
  1478. 'op': 'replace',
  1479. 'path': 'best-answer',
  1480. 'value': best_answer.id,
  1481. },
  1482. ]
  1483. )
  1484. self.assertEqual(response.status_code, 200)
  1485. self.assertEqual(response.json(), {
  1486. 'id': self.thread.id,
  1487. 'detail': ['ok'],
  1488. 'best_answer': best_answer.id,
  1489. 'best_answer_is_protected': False,
  1490. 'best_answer_marked_on': response.json()['best_answer_marked_on'],
  1491. 'best_answer_marked_by': self.user.id,
  1492. 'best_answer_marked_by_name': self.user.username,
  1493. 'best_answer_marked_by_slug': self.user.slug,
  1494. })
  1495. thread_json = self.get_thread_json()
  1496. self.assertEqual(thread_json['best_answer'], best_answer.id)
  1497. self.assertEqual(thread_json['best_answer_is_protected'], False)
  1498. self.assertEqual(
  1499. thread_json['best_answer_marked_on'], response.json()['best_answer_marked_on'])
  1500. self.assertEqual(thread_json['best_answer_marked_by'], self.user.id)
  1501. self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username)
  1502. self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug)
  1503. @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2})
  1504. def test_change_best_answer_same_post(self):
  1505. """api validates if new best answer is same as current one"""
  1506. response = self.patch(
  1507. self.api_link, [
  1508. {
  1509. 'op': 'replace',
  1510. 'path': 'best-answer',
  1511. 'value': self.best_answer.id,
  1512. },
  1513. ]
  1514. )
  1515. self.assertEqual(response.status_code, 400)
  1516. self.assertEqual(response.json(), {
  1517. 'id': self.thread.id,
  1518. 'detail': ["This post is already marked as thread's best answer."],
  1519. })
  1520. thread_json = self.get_thread_json()
  1521. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1522. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1523. def test_change_best_answer_no_permission_to_mark(self):
  1524. """api validates permission to mark best answers before allowing answer change"""
  1525. best_answer = testutils.reply_thread(self.thread)
  1526. response = self.patch(
  1527. self.api_link, [
  1528. {
  1529. 'op': 'replace',
  1530. 'path': 'best-answer',
  1531. 'value': best_answer.id,
  1532. },
  1533. ]
  1534. )
  1535. self.assertEqual(response.status_code, 400)
  1536. self.assertEqual(response.json(), {
  1537. 'id': self.thread.id,
  1538. 'detail': [
  1539. 'You don\'t have permission to mark best answers in the "First category" category.'
  1540. ],
  1541. })
  1542. thread_json = self.get_thread_json()
  1543. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1544. @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 0})
  1545. def test_change_best_answer_no_permission(self):
  1546. """api validates permission to change best answers"""
  1547. best_answer = testutils.reply_thread(self.thread)
  1548. response = self.patch(
  1549. self.api_link, [
  1550. {
  1551. 'op': 'replace',
  1552. 'path': 'best-answer',
  1553. 'value': best_answer.id,
  1554. },
  1555. ]
  1556. )
  1557. self.assertEqual(response.status_code, 400)
  1558. self.assertEqual(response.json(), {
  1559. 'id': self.thread.id,
  1560. 'detail': [
  1561. 'You don\'t have permission to change this thread\'s marked answer because it\'s '
  1562. 'in the "First category" category.'
  1563. ],
  1564. })
  1565. thread_json = self.get_thread_json()
  1566. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1567. @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1})
  1568. def test_change_best_answer_not_starter(self):
  1569. """api validates permission to change best answers"""
  1570. best_answer = testutils.reply_thread(self.thread)
  1571. response = self.patch(
  1572. self.api_link, [
  1573. {
  1574. 'op': 'replace',
  1575. 'path': 'best-answer',
  1576. 'value': best_answer.id,
  1577. },
  1578. ]
  1579. )
  1580. self.assertEqual(response.status_code, 400)
  1581. self.assertEqual(response.json(), {
  1582. 'id': self.thread.id,
  1583. 'detail': [
  1584. "You don't have permission to change this thread's marked answer because you are "
  1585. "not a thread starter."
  1586. ],
  1587. })
  1588. thread_json = self.get_thread_json()
  1589. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1590. # passing scenario is possible
  1591. self.thread.starter = self.user
  1592. self.thread.save()
  1593. response = self.patch(
  1594. self.api_link, [
  1595. {
  1596. 'op': 'replace',
  1597. 'path': 'best-answer',
  1598. 'value': best_answer.id,
  1599. },
  1600. ]
  1601. )
  1602. self.assertEqual(response.status_code, 200)
  1603. @patch_category_acl({
  1604. 'can_mark_best_answers': 2,
  1605. 'can_change_marked_answers': 1,
  1606. 'best_answer_change_time': 5,
  1607. })
  1608. def test_change_best_answer_timelimit_out_of_time(self):
  1609. """api validates permission for starter to change best answers within timelimit"""
  1610. best_answer = testutils.reply_thread(self.thread)
  1611. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
  1612. self.thread.starter = self.user
  1613. self.thread.save()
  1614. response = self.patch(
  1615. self.api_link, [
  1616. {
  1617. 'op': 'replace',
  1618. 'path': 'best-answer',
  1619. 'value': best_answer.id,
  1620. },
  1621. ]
  1622. )
  1623. self.assertEqual(response.status_code, 400)
  1624. self.assertEqual(response.json(), {
  1625. 'id': self.thread.id,
  1626. 'detail': [
  1627. "You don't have permission to change best answer that was marked for more than "
  1628. "5 minutes."
  1629. ],
  1630. })
  1631. thread_json = self.get_thread_json()
  1632. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1633. @patch_category_acl({
  1634. 'can_mark_best_answers': 2,
  1635. 'can_change_marked_answers': 1,
  1636. 'best_answer_change_time': 5,
  1637. })
  1638. def test_change_best_answer_timelimit(self):
  1639. """api validates permission for starter to change best answers within timelimit"""
  1640. best_answer = testutils.reply_thread(self.thread)
  1641. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=1)
  1642. self.thread.starter = self.user
  1643. self.thread.save()
  1644. response = self.patch(
  1645. self.api_link, [
  1646. {
  1647. 'op': 'replace',
  1648. 'path': 'best-answer',
  1649. 'value': best_answer.id,
  1650. },
  1651. ]
  1652. )
  1653. self.assertEqual(response.status_code, 200)
  1654. @patch_category_acl({
  1655. 'can_mark_best_answers': 2,
  1656. 'can_change_marked_answers': 2,
  1657. 'can_protect_posts': False,
  1658. })
  1659. def test_change_best_answer_protected_no_permission(self):
  1660. """api validates permission to change protected best answers"""
  1661. best_answer = testutils.reply_thread(self.thread)
  1662. self.thread.best_answer_is_protected = True
  1663. self.thread.save()
  1664. response = self.patch(
  1665. self.api_link, [
  1666. {
  1667. 'op': 'replace',
  1668. 'path': 'best-answer',
  1669. 'value': best_answer.id,
  1670. },
  1671. ]
  1672. )
  1673. self.assertEqual(response.status_code, 400)
  1674. self.assertEqual(response.json(), {
  1675. 'id': self.thread.id,
  1676. 'detail': [
  1677. "You don't have permission to change this thread's best answer because a "
  1678. "moderator has protected it."
  1679. ],
  1680. })
  1681. thread_json = self.get_thread_json()
  1682. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1683. @patch_category_acl({
  1684. 'can_mark_best_answers': 2,
  1685. 'can_change_marked_answers': 2,
  1686. 'can_protect_posts': True,
  1687. })
  1688. def test_change_best_answer_protected(self):
  1689. """api validates permission to change protected best answers"""
  1690. best_answer = testutils.reply_thread(self.thread)
  1691. self.thread.best_answer_is_protected = True
  1692. self.thread.save()
  1693. response = self.patch(
  1694. self.api_link, [
  1695. {
  1696. 'op': 'replace',
  1697. 'path': 'best-answer',
  1698. 'value': best_answer.id,
  1699. },
  1700. ]
  1701. )
  1702. self.assertEqual(response.status_code, 200)
  1703. @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2})
  1704. def test_change_best_answer_post_validation(self):
  1705. """api validates new post'"""
  1706. best_answer = testutils.reply_thread(self.thread, is_hidden=True)
  1707. response = self.patch(
  1708. self.api_link, [
  1709. {
  1710. 'op': 'replace',
  1711. 'path': 'best-answer',
  1712. 'value': best_answer.id,
  1713. },
  1714. ]
  1715. )
  1716. self.assertEqual(response.status_code, 400)
  1717. thread_json = self.get_thread_json()
  1718. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1719. class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
  1720. def setUp(self):
  1721. super().setUp()
  1722. self.best_answer = testutils.reply_thread(self.thread)
  1723. self.thread.set_best_answer(self.user, self.best_answer)
  1724. self.thread.save()
  1725. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1726. def test_unmark_best_answer(self):
  1727. """api makes it possible to unmark best answer"""
  1728. response = self.patch(
  1729. self.api_link, [
  1730. {
  1731. 'op': 'remove',
  1732. 'path': 'best-answer',
  1733. 'value': self.best_answer.id,
  1734. },
  1735. ]
  1736. )
  1737. self.assertEqual(response.status_code, 200)
  1738. self.assertEqual(response.json(), {
  1739. 'id': self.thread.id,
  1740. 'detail': ['ok'],
  1741. 'best_answer': None,
  1742. 'best_answer_is_protected': False,
  1743. 'best_answer_marked_on': None,
  1744. 'best_answer_marked_by': None,
  1745. 'best_answer_marked_by_name': None,
  1746. 'best_answer_marked_by_slug': None,
  1747. })
  1748. thread_json = self.get_thread_json()
  1749. self.assertIsNone(thread_json['best_answer'])
  1750. self.assertFalse(thread_json['best_answer_is_protected'])
  1751. self.assertIsNone(thread_json['best_answer_marked_on'])
  1752. self.assertIsNone(thread_json['best_answer_marked_by'])
  1753. self.assertIsNone(thread_json['best_answer_marked_by_name'])
  1754. self.assertIsNone(thread_json['best_answer_marked_by_slug'])
  1755. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1756. def test_unmark_best_answer_invalid_post_id(self):
  1757. """api validates that post id is int"""
  1758. response = self.patch(
  1759. self.api_link, [
  1760. {
  1761. 'op': 'remove',
  1762. 'path': 'best-answer',
  1763. 'value': 'd7sd89a7d98sa',
  1764. },
  1765. ]
  1766. )
  1767. self.assertEqual(response.status_code, 400)
  1768. self.assertEqual(response.json(), {
  1769. 'id': self.thread.id,
  1770. 'detail': ["A valid integer is required."],
  1771. })
  1772. thread_json = self.get_thread_json()
  1773. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1774. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1775. def test_unmark_best_answer_post_not_found(self):
  1776. """api validates that post to unmark exists"""
  1777. response = self.patch(
  1778. self.api_link, [
  1779. {
  1780. 'op': 'remove',
  1781. 'path': 'best-answer',
  1782. 'value': self.best_answer.id + 1,
  1783. },
  1784. ]
  1785. )
  1786. self.assertEqual(response.status_code, 400)
  1787. self.assertEqual(response.json(), {
  1788. 'id': self.thread.id,
  1789. 'detail': ["NOT FOUND"],
  1790. })
  1791. thread_json = self.get_thread_json()
  1792. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1793. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1794. def test_unmark_best_answer_wrong_post(self):
  1795. """api validates if post given to unmark is best answer"""
  1796. best_answer = testutils.reply_thread(self.thread)
  1797. response = self.patch(
  1798. self.api_link, [
  1799. {
  1800. 'op': 'remove',
  1801. 'path': 'best-answer',
  1802. 'value': best_answer.id,
  1803. },
  1804. ]
  1805. )
  1806. self.assertEqual(response.status_code, 400)
  1807. self.assertEqual(response.json(), {
  1808. 'id': self.thread.id,
  1809. 'detail': [
  1810. "This post can't be unmarked because it's not currently marked as best answer."
  1811. ],
  1812. })
  1813. thread_json = self.get_thread_json()
  1814. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1815. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 0})
  1816. def test_unmark_best_answer_no_permission(self):
  1817. """api validates if user has permission to unmark best answers"""
  1818. response = self.patch(
  1819. self.api_link, [
  1820. {
  1821. 'op': 'remove',
  1822. 'path': 'best-answer',
  1823. 'value': self.best_answer.id,
  1824. },
  1825. ]
  1826. )
  1827. self.assertEqual(response.status_code, 400)
  1828. self.assertEqual(response.json(), {
  1829. 'id': self.thread.id,
  1830. 'detail': [
  1831. 'You don\'t have permission to unmark threads answers in the "First category" '
  1832. 'category.'
  1833. ],
  1834. })
  1835. thread_json = self.get_thread_json()
  1836. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1837. @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1})
  1838. def test_unmark_best_answer_not_starter(self):
  1839. """api validates if starter has permission to unmark best answers"""
  1840. response = self.patch(
  1841. self.api_link, [
  1842. {
  1843. 'op': 'remove',
  1844. 'path': 'best-answer',
  1845. 'value': self.best_answer.id,
  1846. },
  1847. ]
  1848. )
  1849. self.assertEqual(response.status_code, 400)
  1850. self.assertEqual(response.json(), {
  1851. 'id': self.thread.id,
  1852. 'detail': [
  1853. "You don't have permission to unmark this best answer because you are not a "
  1854. "thread starter."
  1855. ],
  1856. })
  1857. thread_json = self.get_thread_json()
  1858. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1859. # passing scenario is possible
  1860. self.thread.starter = self.user
  1861. self.thread.save()
  1862. response = self.patch(
  1863. self.api_link, [
  1864. {
  1865. 'op': 'remove',
  1866. 'path': 'best-answer',
  1867. 'value': self.best_answer.id,
  1868. },
  1869. ]
  1870. )
  1871. self.assertEqual(response.status_code, 200)
  1872. @patch_category_acl({
  1873. 'can_mark_best_answers': 0,
  1874. 'can_change_marked_answers': 1,
  1875. 'best_answer_change_time': 5,
  1876. })
  1877. def test_unmark_best_answer_timelimit(self):
  1878. """api validates if starter has permission to unmark best answer within time limit"""
  1879. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
  1880. self.thread.starter = self.user
  1881. self.thread.save()
  1882. response = self.patch(
  1883. self.api_link, [
  1884. {
  1885. 'op': 'remove',
  1886. 'path': 'best-answer',
  1887. 'value': self.best_answer.id,
  1888. },
  1889. ]
  1890. )
  1891. self.assertEqual(response.status_code, 400)
  1892. self.assertEqual(response.json(), {
  1893. 'id': self.thread.id,
  1894. 'detail': [
  1895. "You don't have permission to unmark best answer that was marked for more than "
  1896. "5 minutes."
  1897. ],
  1898. })
  1899. thread_json = self.get_thread_json()
  1900. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1901. # passing scenario is possible
  1902. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=2)
  1903. self.thread.save()
  1904. response = self.patch(
  1905. self.api_link, [
  1906. {
  1907. 'op': 'remove',
  1908. 'path': 'best-answer',
  1909. 'value': self.best_answer.id,
  1910. },
  1911. ]
  1912. )
  1913. self.assertEqual(response.status_code, 200)
  1914. @patch_category_acl({
  1915. 'can_mark_best_answers': 0,
  1916. 'can_change_marked_answers': 2,
  1917. 'can_close_threads': False,
  1918. })
  1919. def test_unmark_best_answer_closed_category_no_permission(self):
  1920. """api validates if user has permission to unmark best answer in closed category"""
  1921. self.category.is_closed = True
  1922. self.category.save()
  1923. response = self.patch(
  1924. self.api_link, [
  1925. {
  1926. 'op': 'remove',
  1927. 'path': 'best-answer',
  1928. 'value': self.best_answer.id,
  1929. },
  1930. ]
  1931. )
  1932. self.assertEqual(response.status_code, 400)
  1933. self.assertEqual(response.json(), {
  1934. 'id': self.thread.id,
  1935. 'detail': [
  1936. 'You don\'t have permission to unmark this best answer because its category '
  1937. '"First category" is closed.'
  1938. ],
  1939. })
  1940. thread_json = self.get_thread_json()
  1941. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1942. @patch_category_acl({
  1943. 'can_mark_best_answers': 0,
  1944. 'can_change_marked_answers': 2,
  1945. 'can_close_threads': True,
  1946. })
  1947. def test_unmark_best_answer_closed_category(self):
  1948. """api validates if user has permission to unmark best answer in closed category"""
  1949. self.category.is_closed = True
  1950. self.category.save()
  1951. response = self.patch(
  1952. self.api_link, [
  1953. {
  1954. 'op': 'remove',
  1955. 'path': 'best-answer',
  1956. 'value': self.best_answer.id,
  1957. },
  1958. ]
  1959. )
  1960. self.assertEqual(response.status_code, 200)
  1961. @patch_category_acl({
  1962. 'can_mark_best_answers': 0,
  1963. 'can_change_marked_answers': 2,
  1964. 'can_close_threads': False,
  1965. })
  1966. def test_unmark_best_answer_closed_thread_no_permission(self):
  1967. """api validates if user has permission to unmark best answer in closed thread"""
  1968. self.thread.is_closed = True
  1969. self.thread.save()
  1970. response = self.patch(
  1971. self.api_link, [
  1972. {
  1973. 'op': 'remove',
  1974. 'path': 'best-answer',
  1975. 'value': self.best_answer.id,
  1976. },
  1977. ]
  1978. )
  1979. self.assertEqual(response.status_code, 400)
  1980. self.assertEqual(response.json(), {
  1981. 'id': self.thread.id,
  1982. 'detail': [
  1983. "You can't unmark this thread's best answer because it's closed and you don't "
  1984. "have permission to open it."
  1985. ],
  1986. })
  1987. thread_json = self.get_thread_json()
  1988. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1989. @patch_category_acl({
  1990. 'can_mark_best_answers': 0,
  1991. 'can_change_marked_answers': 2,
  1992. 'can_close_threads': True,
  1993. })
  1994. def test_unmark_best_answer_closed_thread(self):
  1995. """api validates if user has permission to unmark best answer in closed thread"""
  1996. self.thread.is_closed = True
  1997. self.thread.save()
  1998. response = self.patch(
  1999. self.api_link, [
  2000. {
  2001. 'op': 'remove',
  2002. 'path': 'best-answer',
  2003. 'value': self.best_answer.id,
  2004. },
  2005. ]
  2006. )
  2007. self.assertEqual(response.status_code, 200)
  2008. @patch_category_acl({
  2009. 'can_mark_best_answers': 0,
  2010. 'can_change_marked_answers': 2,
  2011. 'can_protect_posts': 0,
  2012. })
  2013. def test_unmark_best_answer_protected_no_permission(self):
  2014. """api validates permission to unmark protected best answers"""
  2015. self.thread.best_answer_is_protected = True
  2016. self.thread.save()
  2017. response = self.patch(
  2018. self.api_link, [
  2019. {
  2020. 'op': 'remove',
  2021. 'path': 'best-answer',
  2022. 'value': self.best_answer.id,
  2023. },
  2024. ]
  2025. )
  2026. self.assertEqual(response.status_code, 400)
  2027. self.assertEqual(response.json(), {
  2028. 'id': self.thread.id,
  2029. 'detail': [
  2030. "You don't have permission to unmark this thread's best answer because a "
  2031. "moderator has protected it."
  2032. ],
  2033. })
  2034. thread_json = self.get_thread_json()
  2035. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  2036. @patch_category_acl({
  2037. 'can_mark_best_answers': 0,
  2038. 'can_change_marked_answers': 2,
  2039. 'can_protect_posts': 1,
  2040. })
  2041. def test_unmark_best_answer_protected(self):
  2042. """api validates permission to unmark protected best answers"""
  2043. self.thread.best_answer_is_protected = True
  2044. self.thread.save()
  2045. response = self.patch(
  2046. self.api_link, [
  2047. {
  2048. 'op': 'remove',
  2049. 'path': 'best-answer',
  2050. 'value': self.best_answer.id,
  2051. },
  2052. ]
  2053. )
  2054. self.assertEqual(response.status_code, 200)