test_thread_patch_api.py 77 KB

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