test_thread_patch_api.py 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436
  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. response_json = response.json()
  408. self.assertEqual(response_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. response_json = response.json()
  437. self.assertEqual(response_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(), {'detail': 'NOT FOUND'})
  586. self.override_other_acl({})
  587. thread_json = self.get_thread_json()
  588. self.assertEqual(thread_json['category']['id'], self.category.pk)
  589. def test_move_thread_no_category_browse(self):
  590. """api move thread to category with no browsing access fails"""
  591. self.override_acl({'can_move_threads': True})
  592. self.override_other_acl({'can_browse': False})
  593. response = self.patch(
  594. self.api_link, [
  595. {
  596. 'op': 'replace',
  597. 'path': 'category',
  598. 'value': self.category_b.pk,
  599. },
  600. ]
  601. )
  602. self.assertEqual(response.status_code, 403)
  603. self.assertEqual(response.json(), {
  604. 'detail': 'You don\'t have permission to browse "Category B" contents.'
  605. })
  606. self.override_other_acl({})
  607. thread_json = self.get_thread_json()
  608. self.assertEqual(thread_json['category']['id'], self.category.pk)
  609. def test_move_thread_no_category_start_threads(self):
  610. """api move thread to category with no posting access fails"""
  611. self.override_acl({'can_move_threads': True})
  612. self.override_other_acl({'can_start_threads': False})
  613. response = self.patch(
  614. self.api_link, [
  615. {
  616. 'op': 'replace',
  617. 'path': 'category',
  618. 'value': self.category_b.pk,
  619. },
  620. ]
  621. )
  622. self.assertEqual(response.status_code, 403)
  623. self.assertEqual(response.json(), {
  624. 'detail': "You don't have permission to start new threads in this category."
  625. })
  626. self.override_other_acl({})
  627. thread_json = self.get_thread_json()
  628. self.assertEqual(thread_json['category']['id'], self.category.pk)
  629. def test_move_thread_same_category(self):
  630. """api move thread to category it's already in fails"""
  631. self.override_acl({'can_move_threads': True})
  632. self.override_other_acl({'can_start_threads': 2})
  633. response = self.patch(
  634. self.api_link, [
  635. {
  636. 'op': 'replace',
  637. 'path': 'category',
  638. 'value': self.thread.category_id,
  639. },
  640. ]
  641. )
  642. self.assertEqual(response.status_code, 400)
  643. self.assertEqual(response.json(), {
  644. 'detail': ["You can't move thread to the category it's already in."]
  645. })
  646. self.override_other_acl({})
  647. thread_json = self.get_thread_json()
  648. self.assertEqual(thread_json['category']['id'], self.category.pk)
  649. def test_thread_flatten_categories(self):
  650. """api flatten thread categories"""
  651. response = self.patch(
  652. self.api_link, [
  653. {
  654. 'op': 'replace',
  655. 'path': 'flatten-categories',
  656. 'value': None,
  657. },
  658. ]
  659. )
  660. self.assertEqual(response.status_code, 200)
  661. response_json = response.json()
  662. self.assertEqual(response_json['category'], self.category.pk)
  663. class ThreadCloseApiTests(ThreadPatchApiTestCase):
  664. def test_close_thread(self):
  665. """api makes it possible to close thread"""
  666. self.override_acl({'can_close_threads': True})
  667. response = self.patch(
  668. self.api_link, [
  669. {
  670. 'op': 'replace',
  671. 'path': 'is-closed',
  672. 'value': True,
  673. },
  674. ]
  675. )
  676. self.assertEqual(response.status_code, 200)
  677. response_json = response.json()
  678. self.assertTrue(response_json['is_closed'])
  679. thread_json = self.get_thread_json()
  680. self.assertTrue(thread_json['is_closed'])
  681. def test_open_thread(self):
  682. """api makes it possible to open thread"""
  683. self.thread.is_closed = True
  684. self.thread.save()
  685. thread_json = self.get_thread_json()
  686. self.assertTrue(thread_json['is_closed'])
  687. self.override_acl({'can_close_threads': True})
  688. response = self.patch(
  689. self.api_link, [
  690. {
  691. 'op': 'replace',
  692. 'path': 'is-closed',
  693. 'value': False,
  694. },
  695. ]
  696. )
  697. self.assertEqual(response.status_code, 200)
  698. response_json = response.json()
  699. self.assertFalse(response_json['is_closed'])
  700. thread_json = self.get_thread_json()
  701. self.assertFalse(thread_json['is_closed'])
  702. def test_close_thread_no_permission(self):
  703. """api close thread with no permission fails"""
  704. self.override_acl({'can_close_threads': False})
  705. response = self.patch(
  706. self.api_link, [
  707. {
  708. 'op': 'replace',
  709. 'path': 'is-closed',
  710. 'value': True,
  711. },
  712. ]
  713. )
  714. self.assertEqual(response.status_code, 403)
  715. self.assertEqual(response.json(), {
  716. 'detail': "You don't have permission to close this thread."
  717. })
  718. thread_json = self.get_thread_json()
  719. self.assertFalse(thread_json['is_closed'])
  720. def test_open_thread_no_permission(self):
  721. """api open thread with no permission fails"""
  722. self.thread.is_closed = True
  723. self.thread.save()
  724. thread_json = self.get_thread_json()
  725. self.assertTrue(thread_json['is_closed'])
  726. self.override_acl({'can_close_threads': False})
  727. response = self.patch(
  728. self.api_link, [
  729. {
  730. 'op': 'replace',
  731. 'path': 'is-closed',
  732. 'value': False,
  733. },
  734. ]
  735. )
  736. self.assertEqual(response.status_code, 403)
  737. self.assertEqual(response.json(), {
  738. 'detail': "You don't have permission to open this thread."
  739. })
  740. thread_json = self.get_thread_json()
  741. self.assertTrue(thread_json['is_closed'])
  742. class ThreadApproveApiTests(ThreadPatchApiTestCase):
  743. def test_approve_thread(self):
  744. """api makes it possible to approve thread"""
  745. self.thread.first_post.is_unapproved = True
  746. self.thread.first_post.save()
  747. self.thread.synchronize()
  748. self.thread.save()
  749. self.assertTrue(self.thread.is_unapproved)
  750. self.assertTrue(self.thread.has_unapproved_posts)
  751. self.override_acl({'can_approve_content': 1})
  752. response = self.patch(
  753. self.api_link, [
  754. {
  755. 'op': 'replace',
  756. 'path': 'is-unapproved',
  757. 'value': False,
  758. },
  759. ]
  760. )
  761. self.assertEqual(response.status_code, 200)
  762. response_json = response.json()
  763. self.assertFalse(response_json['is_unapproved'])
  764. self.assertFalse(response_json['has_unapproved_posts'])
  765. thread_json = self.get_thread_json()
  766. self.assertFalse(thread_json['is_unapproved'])
  767. self.assertFalse(thread_json['has_unapproved_posts'])
  768. thread = Thread.objects.get(pk=self.thread.pk)
  769. self.assertFalse(thread.is_unapproved)
  770. self.assertFalse(thread.has_unapproved_posts)
  771. def test_approve_thread_category_closed_no_permission(self):
  772. """api checks permission for approving threads in closed categories"""
  773. self.thread.first_post.is_unapproved = True
  774. self.thread.first_post.save()
  775. self.thread.synchronize()
  776. self.thread.save()
  777. self.assertTrue(self.thread.is_unapproved)
  778. self.assertTrue(self.thread.has_unapproved_posts)
  779. self.category.is_closed = True
  780. self.category.save()
  781. self.override_acl({
  782. 'can_approve_content': 1,
  783. 'can_close_threads': 0,
  784. })
  785. response = self.patch(
  786. self.api_link, [
  787. {
  788. 'op': 'replace',
  789. 'path': 'is-unapproved',
  790. 'value': False,
  791. },
  792. ]
  793. )
  794. self.assertEqual(response.status_code, 403)
  795. self.assertEqual(response.json(), {
  796. 'detail': "This category is closed. You can't approve threads in it."
  797. })
  798. def test_approve_thread_closed_no_permission(self):
  799. """api checks permission for approving posts in closed categories"""
  800. self.thread.first_post.is_unapproved = True
  801. self.thread.first_post.save()
  802. self.thread.synchronize()
  803. self.thread.save()
  804. self.assertTrue(self.thread.is_unapproved)
  805. self.assertTrue(self.thread.has_unapproved_posts)
  806. self.thread.is_closed = True
  807. self.thread.save()
  808. self.override_acl({
  809. 'can_approve_content': 1,
  810. 'can_close_threads': 0,
  811. })
  812. response = self.patch(
  813. self.api_link, [
  814. {
  815. 'op': 'replace',
  816. 'path': 'is-unapproved',
  817. 'value': False,
  818. },
  819. ]
  820. )
  821. self.assertEqual(response.status_code, 403)
  822. self.assertEqual(response.json(), {
  823. 'detail': "This thread is closed. You can't approve it."
  824. })
  825. def test_unapprove_thread(self):
  826. """api returns permission error on approval removal"""
  827. self.override_acl({'can_approve_content': 1})
  828. response = self.patch(
  829. self.api_link, [
  830. {
  831. 'op': 'replace',
  832. 'path': 'is-unapproved',
  833. 'value': True,
  834. },
  835. ]
  836. )
  837. self.assertEqual(response.status_code, 403)
  838. self.assertEqual(response.json(), {
  839. 'detail': "Content approval can't be reversed."
  840. })
  841. class ThreadHideApiTests(ThreadPatchApiTestCase):
  842. def test_hide_thread(self):
  843. """api makes it possible to hide thread"""
  844. self.override_acl({'can_hide_threads': 1})
  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, 200)
  855. response_json = response.json()
  856. self.assertTrue(response_json['is_hidden'])
  857. self.override_acl({'can_hide_threads': 1})
  858. thread_json = self.get_thread_json()
  859. self.assertTrue(thread_json['is_hidden'])
  860. def test_hide_thread_no_permission(self):
  861. """api hide thread with no permission fails"""
  862. self.override_acl({'can_hide_threads': 0})
  863. response = self.patch(
  864. self.api_link, [
  865. {
  866. 'op': 'replace',
  867. 'path': 'is-hidden',
  868. 'value': True,
  869. },
  870. ]
  871. )
  872. self.assertEqual(response.status_code, 403)
  873. self.assertEqual(response.json(), {
  874. 'detail': "You can't hide threads in this category."
  875. })
  876. thread_json = self.get_thread_json()
  877. self.assertFalse(thread_json['is_hidden'])
  878. def test_hide_non_owned_thread(self):
  879. """api forbids non-moderator from hiding other users threads"""
  880. self.override_acl({
  881. 'can_hide_own_threads': 1,
  882. 'can_hide_threads': 0
  883. })
  884. response = self.patch(
  885. self.api_link, [
  886. {
  887. 'op': 'replace',
  888. 'path': 'is-hidden',
  889. 'value': True,
  890. },
  891. ]
  892. )
  893. self.assertEqual(response.status_code, 403)
  894. self.assertEqual(response.json(), {
  895. 'detail': "You can't hide other users theads in this category."
  896. })
  897. def test_hide_owned_thread_no_time(self):
  898. """api forbids non-moderator from hiding other users threads"""
  899. self.override_acl({
  900. 'can_hide_own_threads': 1,
  901. 'can_hide_threads': 0,
  902. 'thread_edit_time': 1,
  903. })
  904. self.thread.started_on = timezone.now() - timedelta(minutes=5)
  905. self.thread.starter = self.user
  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, 403)
  917. self.assertEqual(response.json(), {
  918. 'detail': "You can't hide threads that are older than 1 minute."
  919. })
  920. def test_hide_closed_category_no_permission(self):
  921. """api test permission to hide thread in closed category"""
  922. self.override_acl({
  923. 'can_hide_threads': 1,
  924. 'can_close_threads': 0
  925. })
  926. self.category.is_closed = True
  927. self.category.save()
  928. response = self.patch(
  929. self.api_link, [
  930. {
  931. 'op': 'replace',
  932. 'path': 'is-hidden',
  933. 'value': True,
  934. },
  935. ]
  936. )
  937. self.assertEqual(response.status_code, 403)
  938. self.assertEqual(response.json(), {
  939. 'detail': "This category is closed. You can't hide threads in it."
  940. })
  941. def test_hide_closed_thread_no_permission(self):
  942. """api test permission to hide closed thread"""
  943. self.override_acl({
  944. 'can_hide_threads': 1,
  945. 'can_close_threads': 0
  946. })
  947. self.thread.is_closed = True
  948. self.thread.save()
  949. response = self.patch(
  950. self.api_link, [
  951. {
  952. 'op': 'replace',
  953. 'path': 'is-hidden',
  954. 'value': True,
  955. },
  956. ]
  957. )
  958. self.assertEqual(response.status_code, 403)
  959. self.assertEqual(response.json(), {
  960. 'detail': "This thread is closed. You can't hide it."
  961. })
  962. class ThreadUnhideApiTests(ThreadPatchApiTestCase):
  963. def setUp(self):
  964. super(ThreadUnhideApiTests, self).setUp()
  965. self.thread.is_hidden = True
  966. self.thread.save()
  967. def test_unhide_thread(self):
  968. """api makes it possible to unhide thread"""
  969. self.override_acl({'can_hide_threads': 1})
  970. response = self.patch(
  971. self.api_link, [
  972. {
  973. 'op': 'replace',
  974. 'path': 'is-hidden',
  975. 'value': False,
  976. },
  977. ]
  978. )
  979. self.assertEqual(response.status_code, 200)
  980. response_json = response.json()
  981. self.assertFalse(response_json['is_hidden'])
  982. self.override_acl({'can_hide_threads': 1})
  983. thread_json = self.get_thread_json()
  984. self.assertFalse(thread_json['is_hidden'])
  985. def test_unhide_thread_no_permission(self):
  986. """api unhide thread with no permission fails as thread is invisible"""
  987. self.override_acl({'can_hide_threads': 0})
  988. response = self.patch(
  989. self.api_link, [
  990. {
  991. 'op': 'replace',
  992. 'path': 'is-hidden',
  993. 'value': True,
  994. },
  995. ]
  996. )
  997. self.assertEqual(response.status_code, 404)
  998. def test_unhide_closed_category_no_permission(self):
  999. """api test permission to unhide thread in closed category"""
  1000. self.override_acl({
  1001. 'can_hide_threads': 1,
  1002. 'can_close_threads': 0
  1003. })
  1004. self.category.is_closed = True
  1005. self.category.save()
  1006. response = self.patch(
  1007. self.api_link, [
  1008. {
  1009. 'op': 'replace',
  1010. 'path': 'is-hidden',
  1011. 'value': False,
  1012. },
  1013. ]
  1014. )
  1015. self.assertEqual(response.status_code, 403)
  1016. self.assertEqual(response.json(), {
  1017. 'detail': "This category is closed. You can't reveal threads in it."
  1018. })
  1019. def test_unhide_closed_thread_no_permission(self):
  1020. """api test permission to unhide closed thread"""
  1021. self.override_acl({
  1022. 'can_hide_threads': 1,
  1023. 'can_close_threads': 0
  1024. })
  1025. self.thread.is_closed = True
  1026. self.thread.save()
  1027. response = self.patch(
  1028. self.api_link, [
  1029. {
  1030. 'op': 'replace',
  1031. 'path': 'is-hidden',
  1032. 'value': False,
  1033. },
  1034. ]
  1035. )
  1036. self.assertEqual(response.status_code, 403)
  1037. self.assertEqual(response.json(), {
  1038. 'detail': "This thread is closed. You can't reveal it."
  1039. })
  1040. class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
  1041. def test_subscribe_thread(self):
  1042. """api makes it possible to subscribe thread"""
  1043. response = self.patch(
  1044. self.api_link, [
  1045. {
  1046. 'op': 'replace',
  1047. 'path': 'subscription',
  1048. 'value': 'notify',
  1049. },
  1050. ]
  1051. )
  1052. self.assertEqual(response.status_code, 200)
  1053. response_json = response.json()
  1054. self.assertFalse(response_json['subscription'])
  1055. thread_json = self.get_thread_json()
  1056. self.assertFalse(thread_json['subscription'])
  1057. subscription = self.user.subscription_set.get(thread=self.thread)
  1058. self.assertFalse(subscription.send_email)
  1059. def test_subscribe_thread_with_email(self):
  1060. """api makes it possible to subscribe thread with emails"""
  1061. response = self.patch(
  1062. self.api_link, [
  1063. {
  1064. 'op': 'replace',
  1065. 'path': 'subscription',
  1066. 'value': 'email',
  1067. },
  1068. ]
  1069. )
  1070. self.assertEqual(response.status_code, 200)
  1071. response_json = response.json()
  1072. self.assertTrue(response_json['subscription'])
  1073. thread_json = self.get_thread_json()
  1074. self.assertTrue(thread_json['subscription'])
  1075. subscription = self.user.subscription_set.get(thread=self.thread)
  1076. self.assertTrue(subscription.send_email)
  1077. def test_unsubscribe_thread(self):
  1078. """api makes it possible to unsubscribe thread"""
  1079. response = self.patch(
  1080. self.api_link, [
  1081. {
  1082. 'op': 'replace',
  1083. 'path': 'subscription',
  1084. 'value': 'remove',
  1085. },
  1086. ]
  1087. )
  1088. self.assertEqual(response.status_code, 200)
  1089. response_json = response.json()
  1090. self.assertIsNone(response_json['subscription'])
  1091. thread_json = self.get_thread_json()
  1092. self.assertIsNone(thread_json['subscription'])
  1093. self.assertEqual(self.user.subscription_set.count(), 0)
  1094. def test_subscribe_as_guest(self):
  1095. """api makes it impossible to subscribe thread"""
  1096. self.logout_user()
  1097. response = self.patch(
  1098. self.api_link, [
  1099. {
  1100. 'op': 'replace',
  1101. 'path': 'subscription',
  1102. 'value': 'email',
  1103. },
  1104. ]
  1105. )
  1106. self.assertEqual(response.status_code, 403)
  1107. def test_subscribe_nonexistant_thread(self):
  1108. """api makes it impossible to subscribe nonexistant thread"""
  1109. bad_api_link = self.api_link.replace(
  1110. six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9)
  1111. )
  1112. response = self.patch(
  1113. bad_api_link, [
  1114. {
  1115. 'op': 'replace',
  1116. 'path': 'subscription',
  1117. 'value': 'email',
  1118. },
  1119. ]
  1120. )
  1121. self.assertEqual(response.status_code, 404)
  1122. class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
  1123. def test_mark_best_answer(self):
  1124. """api makes it possible to mark best answer"""
  1125. self.override_acl({'can_mark_best_answers': 2})
  1126. best_answer = testutils.reply_thread(self.thread)
  1127. response = self.patch(
  1128. self.api_link, [
  1129. {
  1130. 'op': 'replace',
  1131. 'path': 'best-answer',
  1132. 'value': best_answer.id,
  1133. },
  1134. ]
  1135. )
  1136. self.assertEqual(response.status_code, 200)
  1137. self.assertEqual(response.json(), {
  1138. 'id': self.thread.id,
  1139. 'best_answer': best_answer.id,
  1140. 'best_answer_is_protected': False,
  1141. 'best_answer_marked_on': response.json()['best_answer_marked_on'],
  1142. 'best_answer_marked_by': self.user.id,
  1143. 'best_answer_marked_by_name': self.user.username,
  1144. 'best_answer_marked_by_slug': self.user.slug,
  1145. })
  1146. thread_json = self.get_thread_json()
  1147. self.assertEqual(thread_json['best_answer'], best_answer.id)
  1148. self.assertEqual(thread_json['best_answer_is_protected'], False)
  1149. self.assertEqual(
  1150. thread_json['best_answer_marked_on'], response.json()['best_answer_marked_on'])
  1151. self.assertEqual(thread_json['best_answer_marked_by'], self.user.id)
  1152. self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username)
  1153. self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug)
  1154. def test_mark_best_answer_anonymous(self):
  1155. """api validates that user is authenticated before marking best answer"""
  1156. self.logout_user()
  1157. self.override_acl({'can_mark_best_answers': 2})
  1158. best_answer = testutils.reply_thread(self.thread)
  1159. response = self.patch(
  1160. self.api_link, [
  1161. {
  1162. 'op': 'replace',
  1163. 'path': 'best-answer',
  1164. 'value': best_answer.id,
  1165. },
  1166. ]
  1167. )
  1168. self.assertEqual(response.status_code, 403)
  1169. self.assertEqual(response.json(), {
  1170. 'detail': "This action is not available to guests.",
  1171. })
  1172. thread_json = self.get_thread_json()
  1173. self.assertIsNone(thread_json['best_answer'])
  1174. def test_mark_best_answer_no_permission(self):
  1175. """api validates permission to mark best answers"""
  1176. self.override_acl({'can_mark_best_answers': 0})
  1177. best_answer = testutils.reply_thread(self.thread)
  1178. response = self.patch(
  1179. self.api_link, [
  1180. {
  1181. 'op': 'replace',
  1182. 'path': 'best-answer',
  1183. 'value': best_answer.id,
  1184. },
  1185. ]
  1186. )
  1187. self.assertEqual(response.status_code, 403)
  1188. self.assertEqual(response.json(), {
  1189. 'detail': (
  1190. 'You don\'t have permission to mark best answers in the "First category" category.'
  1191. ),
  1192. })
  1193. thread_json = self.get_thread_json()
  1194. self.assertIsNone(thread_json['best_answer'])
  1195. def test_mark_best_answer_not_thread_starter(self):
  1196. """api validates permission to mark best answers in owned thread"""
  1197. self.override_acl({'can_mark_best_answers': 1})
  1198. best_answer = testutils.reply_thread(self.thread)
  1199. response = self.patch(
  1200. self.api_link, [
  1201. {
  1202. 'op': 'replace',
  1203. 'path': 'best-answer',
  1204. 'value': best_answer.id,
  1205. },
  1206. ]
  1207. )
  1208. self.assertEqual(response.status_code, 403)
  1209. self.assertEqual(response.json(), {
  1210. 'detail': (
  1211. "You don't have permission to mark best answer in this thread because you didn't "
  1212. "start it."
  1213. ),
  1214. })
  1215. thread_json = self.get_thread_json()
  1216. self.assertIsNone(thread_json['best_answer'])
  1217. # passing scenario is possible
  1218. self.thread.starter = self.user
  1219. self.thread.save()
  1220. self.override_acl({'can_mark_best_answers': 1})
  1221. response = self.patch(
  1222. self.api_link, [
  1223. {
  1224. 'op': 'replace',
  1225. 'path': 'best-answer',
  1226. 'value': best_answer.id,
  1227. },
  1228. ]
  1229. )
  1230. self.assertEqual(response.status_code, 200)
  1231. def test_mark_best_answer_category_closed(self):
  1232. """api validates permission to mark best answers in closed category"""
  1233. self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 0})
  1234. best_answer = testutils.reply_thread(self.thread)
  1235. self.category.is_closed = True
  1236. self.category.save()
  1237. response = self.patch(
  1238. self.api_link, [
  1239. {
  1240. 'op': 'replace',
  1241. 'path': 'best-answer',
  1242. 'value': best_answer.id,
  1243. },
  1244. ]
  1245. )
  1246. self.assertEqual(response.status_code, 403)
  1247. self.assertEqual(response.json(), {
  1248. 'detail': (
  1249. 'You don\'t have permission to mark best answer in this thread because its '
  1250. 'category "First category" is closed.'
  1251. ),
  1252. })
  1253. thread_json = self.get_thread_json()
  1254. self.assertIsNone(thread_json['best_answer'])
  1255. # passing scenario is possible
  1256. self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 1})
  1257. response = self.patch(
  1258. self.api_link, [
  1259. {
  1260. 'op': 'replace',
  1261. 'path': 'best-answer',
  1262. 'value': best_answer.id,
  1263. },
  1264. ]
  1265. )
  1266. self.assertEqual(response.status_code, 200)
  1267. def test_mark_best_answer_thread_closed(self):
  1268. """api validates permission to mark best answers in closed thread"""
  1269. self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 0})
  1270. best_answer = testutils.reply_thread(self.thread)
  1271. self.thread.is_closed = True
  1272. self.thread.save()
  1273. response = self.patch(
  1274. self.api_link, [
  1275. {
  1276. 'op': 'replace',
  1277. 'path': 'best-answer',
  1278. 'value': best_answer.id,
  1279. },
  1280. ]
  1281. )
  1282. self.assertEqual(response.status_code, 403)
  1283. self.assertEqual(response.json(), {
  1284. 'detail': (
  1285. "You can't mark best answer in this thread because it's closed and you don't have "
  1286. "permission to open it."
  1287. ),
  1288. })
  1289. thread_json = self.get_thread_json()
  1290. self.assertIsNone(thread_json['best_answer'])
  1291. # passing scenario is possible
  1292. self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 1})
  1293. response = self.patch(
  1294. self.api_link, [
  1295. {
  1296. 'op': 'replace',
  1297. 'path': 'best-answer',
  1298. 'value': best_answer.id,
  1299. },
  1300. ]
  1301. )
  1302. self.assertEqual(response.status_code, 200)
  1303. def test_mark_best_answer_invalid_post_id(self):
  1304. """api validates that post id is int"""
  1305. self.override_acl({'can_mark_best_answers': 2})
  1306. response = self.patch(
  1307. self.api_link, [
  1308. {
  1309. 'op': 'replace',
  1310. 'path': 'best-answer',
  1311. 'value': 'd7sd89a7d98sa',
  1312. },
  1313. ]
  1314. )
  1315. self.assertEqual(response.status_code, 400)
  1316. self.assertEqual(response.json(), {
  1317. 'detail': ["A valid integer is required."],
  1318. })
  1319. thread_json = self.get_thread_json()
  1320. self.assertIsNone(thread_json['best_answer'])
  1321. def test_mark_best_answer_post_not_found(self):
  1322. """api validates that post exists"""
  1323. self.override_acl({'can_mark_best_answers': 2})
  1324. response = self.patch(
  1325. self.api_link, [
  1326. {
  1327. 'op': 'replace',
  1328. 'path': 'best-answer',
  1329. 'value': self.thread.last_post_id + 1,
  1330. },
  1331. ]
  1332. )
  1333. self.assertEqual(response.status_code, 404)
  1334. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  1335. thread_json = self.get_thread_json()
  1336. self.assertIsNone(thread_json['best_answer'])
  1337. def test_mark_best_answer_post_invisible(self):
  1338. """api validates post visibility to action author"""
  1339. self.override_acl({'can_mark_best_answers': 2})
  1340. unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True)
  1341. response = self.patch(
  1342. self.api_link, [
  1343. {
  1344. 'op': 'replace',
  1345. 'path': 'best-answer',
  1346. 'value': unapproved_post.id,
  1347. },
  1348. ]
  1349. )
  1350. self.assertEqual(response.status_code, 404)
  1351. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  1352. thread_json = self.get_thread_json()
  1353. self.assertIsNone(thread_json['best_answer'])
  1354. def test_mark_best_answer_post_other_thread(self):
  1355. """api validates post belongs to same thread"""
  1356. self.override_acl({'can_mark_best_answers': 2})
  1357. other_thread = testutils.post_thread(self.category)
  1358. response = self.patch(
  1359. self.api_link, [
  1360. {
  1361. 'op': 'replace',
  1362. 'path': 'best-answer',
  1363. 'value': other_thread.first_post_id,
  1364. },
  1365. ]
  1366. )
  1367. self.assertEqual(response.status_code, 404)
  1368. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  1369. thread_json = self.get_thread_json()
  1370. self.assertIsNone(thread_json['best_answer'])
  1371. def test_mark_best_answer_event_id(self):
  1372. """api validates that post is not an event"""
  1373. self.override_acl({'can_mark_best_answers': 2})
  1374. best_answer = testutils.reply_thread(self.thread)
  1375. best_answer.is_event = True
  1376. best_answer.save()
  1377. response = self.patch(
  1378. self.api_link, [
  1379. {
  1380. 'op': 'replace',
  1381. 'path': 'best-answer',
  1382. 'value': best_answer.id,
  1383. },
  1384. ]
  1385. )
  1386. self.assertEqual(response.status_code, 403)
  1387. self.assertEqual(response.json(), {
  1388. 'detail': "Events can't be marked as best answers.",
  1389. })
  1390. thread_json = self.get_thread_json()
  1391. self.assertIsNone(thread_json['best_answer'])
  1392. def test_mark_best_answer_first_post(self):
  1393. """api validates that post is not a first post in thread"""
  1394. self.override_acl({'can_mark_best_answers': 2})
  1395. response = self.patch(
  1396. self.api_link, [
  1397. {
  1398. 'op': 'replace',
  1399. 'path': 'best-answer',
  1400. 'value': self.thread.first_post_id,
  1401. },
  1402. ]
  1403. )
  1404. self.assertEqual(response.status_code, 403)
  1405. self.assertEqual(response.json(), {
  1406. 'detail': "First post in a thread can't be marked as best answer.",
  1407. })
  1408. thread_json = self.get_thread_json()
  1409. self.assertIsNone(thread_json['best_answer'])
  1410. def test_mark_best_answer_hidden_post(self):
  1411. """api validates that post is not hidden"""
  1412. self.override_acl({'can_mark_best_answers': 2})
  1413. best_answer = testutils.reply_thread(self.thread, is_hidden=True)
  1414. response = self.patch(
  1415. self.api_link, [
  1416. {
  1417. 'op': 'replace',
  1418. 'path': 'best-answer',
  1419. 'value': best_answer.id,
  1420. },
  1421. ]
  1422. )
  1423. self.assertEqual(response.status_code, 403)
  1424. self.assertEqual(response.json(), {
  1425. 'detail': "Hidden posts can't be marked as best answers.",
  1426. })
  1427. thread_json = self.get_thread_json()
  1428. self.assertIsNone(thread_json['best_answer'])
  1429. def test_mark_best_answer_unapproved_post(self):
  1430. """api validates that post is not unapproved"""
  1431. self.override_acl({'can_mark_best_answers': 2})
  1432. best_answer = testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True)
  1433. response = self.patch(
  1434. self.api_link, [
  1435. {
  1436. 'op': 'replace',
  1437. 'path': 'best-answer',
  1438. 'value': best_answer.id,
  1439. },
  1440. ]
  1441. )
  1442. self.assertEqual(response.status_code, 403)
  1443. self.assertEqual(response.json(), {
  1444. 'detail': "Unapproved posts can't be marked as best answers.",
  1445. })
  1446. thread_json = self.get_thread_json()
  1447. self.assertIsNone(thread_json['best_answer'])
  1448. def test_mark_best_answer_protected_post(self):
  1449. """api respects post protection"""
  1450. self.override_acl({'can_mark_best_answers': 2, 'can_protect_posts': 0})
  1451. best_answer = testutils.reply_thread(self.thread, is_protected=True)
  1452. response = self.patch(
  1453. self.api_link, [
  1454. {
  1455. 'op': 'replace',
  1456. 'path': 'best-answer',
  1457. 'value': best_answer.id,
  1458. },
  1459. ]
  1460. )
  1461. self.assertEqual(response.status_code, 403)
  1462. self.assertEqual(response.json(), {
  1463. 'detail': (
  1464. "You don't have permission to mark this post as best answer because a moderator "
  1465. "has protected it."
  1466. ),
  1467. })
  1468. thread_json = self.get_thread_json()
  1469. self.assertIsNone(thread_json['best_answer'])
  1470. # passing scenario is possible
  1471. self.override_acl({'can_mark_best_answers': 2, 'can_protect_posts': 1})
  1472. response = self.patch(
  1473. self.api_link, [
  1474. {
  1475. 'op': 'replace',
  1476. 'path': 'best-answer',
  1477. 'value': best_answer.id,
  1478. },
  1479. ]
  1480. )
  1481. self.assertEqual(response.status_code, 200)
  1482. class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
  1483. def setUp(self):
  1484. super(ThreadChangeBestAnswerApiTests, self).setUp()
  1485. self.best_answer = testutils.reply_thread(self.thread)
  1486. self.thread.set_best_answer(self.user, self.best_answer)
  1487. self.thread.save()
  1488. def test_change_best_answer(self):
  1489. """api makes it possible to change best answer"""
  1490. self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2})
  1491. best_answer = testutils.reply_thread(self.thread)
  1492. response = self.patch(
  1493. self.api_link, [
  1494. {
  1495. 'op': 'replace',
  1496. 'path': 'best-answer',
  1497. 'value': best_answer.id,
  1498. },
  1499. ]
  1500. )
  1501. self.assertEqual(response.status_code, 200)
  1502. self.assertEqual(response.json(), {
  1503. 'id': self.thread.id,
  1504. 'best_answer': best_answer.id,
  1505. 'best_answer_is_protected': False,
  1506. 'best_answer_marked_on': response.json()['best_answer_marked_on'],
  1507. 'best_answer_marked_by': self.user.id,
  1508. 'best_answer_marked_by_name': self.user.username,
  1509. 'best_answer_marked_by_slug': self.user.slug,
  1510. })
  1511. thread_json = self.get_thread_json()
  1512. self.assertEqual(thread_json['best_answer'], best_answer.id)
  1513. self.assertEqual(thread_json['best_answer_is_protected'], False)
  1514. self.assertEqual(
  1515. thread_json['best_answer_marked_on'], response.json()['best_answer_marked_on'])
  1516. self.assertEqual(thread_json['best_answer_marked_by'], self.user.id)
  1517. self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username)
  1518. self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug)
  1519. def test_change_best_answer_same_post(self):
  1520. """api validates if new best answer is same as current one"""
  1521. self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2})
  1522. response = self.patch(
  1523. self.api_link, [
  1524. {
  1525. 'op': 'replace',
  1526. 'path': 'best-answer',
  1527. 'value': self.best_answer.id,
  1528. },
  1529. ]
  1530. )
  1531. self.assertEqual(response.status_code, 403)
  1532. self.assertEqual(response.json(), {
  1533. 'detail': "This post is already marked as thread's best answer.",
  1534. })
  1535. thread_json = self.get_thread_json()
  1536. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1537. def test_change_best_answer_no_permission_to_mark(self):
  1538. """api validates permission to mark best answers before allowing answer change"""
  1539. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1540. best_answer = testutils.reply_thread(self.thread)
  1541. response = self.patch(
  1542. self.api_link, [
  1543. {
  1544. 'op': 'replace',
  1545. 'path': 'best-answer',
  1546. 'value': best_answer.id,
  1547. },
  1548. ]
  1549. )
  1550. self.assertEqual(response.status_code, 403)
  1551. self.assertEqual(response.json(), {
  1552. 'detail': (
  1553. 'You don\'t have permission to mark best answers in the "First category" category.'
  1554. ),
  1555. })
  1556. thread_json = self.get_thread_json()
  1557. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1558. def test_change_best_answer_no_permission(self):
  1559. """api validates permission to change best answers"""
  1560. self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 0})
  1561. best_answer = testutils.reply_thread(self.thread)
  1562. response = self.patch(
  1563. self.api_link, [
  1564. {
  1565. 'op': 'replace',
  1566. 'path': 'best-answer',
  1567. 'value': best_answer.id,
  1568. },
  1569. ]
  1570. )
  1571. self.assertEqual(response.status_code, 403)
  1572. self.assertEqual(response.json(), {
  1573. 'detail': (
  1574. 'You don\'t have permission to change this thread\'s marked answer because it\'s '
  1575. 'in the "First category" category.'
  1576. ),
  1577. })
  1578. thread_json = self.get_thread_json()
  1579. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1580. def test_change_best_answer_not_starter(self):
  1581. """api validates permission to change best answers"""
  1582. self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1})
  1583. best_answer = testutils.reply_thread(self.thread)
  1584. response = self.patch(
  1585. self.api_link, [
  1586. {
  1587. 'op': 'replace',
  1588. 'path': 'best-answer',
  1589. 'value': best_answer.id,
  1590. },
  1591. ]
  1592. )
  1593. self.assertEqual(response.status_code, 403)
  1594. self.assertEqual(response.json(), {
  1595. 'detail': (
  1596. "You don't have permission to change this thread's marked answer because you are "
  1597. "not a thread starter."
  1598. ),
  1599. })
  1600. thread_json = self.get_thread_json()
  1601. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1602. # passing scenario is possible
  1603. self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1})
  1604. self.thread.starter = self.user
  1605. self.thread.save()
  1606. response = self.patch(
  1607. self.api_link, [
  1608. {
  1609. 'op': 'replace',
  1610. 'path': 'best-answer',
  1611. 'value': best_answer.id,
  1612. },
  1613. ]
  1614. )
  1615. self.assertEqual(response.status_code, 200)
  1616. def test_change_best_answer_timelimit(self):
  1617. """api validates permission for starter to change best answers within timelimit"""
  1618. self.override_acl({
  1619. 'can_mark_best_answers': 2,
  1620. 'can_change_marked_answers': 1,
  1621. 'best_answer_change_time': 5,
  1622. })
  1623. best_answer = testutils.reply_thread(self.thread)
  1624. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
  1625. self.thread.starter = self.user
  1626. self.thread.save()
  1627. response = self.patch(
  1628. self.api_link, [
  1629. {
  1630. 'op': 'replace',
  1631. 'path': 'best-answer',
  1632. 'value': best_answer.id,
  1633. },
  1634. ]
  1635. )
  1636. self.assertEqual(response.status_code, 403)
  1637. self.assertEqual(response.json(), {
  1638. 'detail': (
  1639. "You don't have permission to change best answer that was marked for more than "
  1640. "5 minutes."
  1641. ),
  1642. })
  1643. thread_json = self.get_thread_json()
  1644. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1645. # passing scenario is possible
  1646. self.override_acl({
  1647. 'can_mark_best_answers': 2,
  1648. 'can_change_marked_answers': 1,
  1649. 'best_answer_change_time': 10,
  1650. })
  1651. response = self.patch(
  1652. self.api_link, [
  1653. {
  1654. 'op': 'replace',
  1655. 'path': 'best-answer',
  1656. 'value': best_answer.id,
  1657. },
  1658. ]
  1659. )
  1660. self.assertEqual(response.status_code, 200)
  1661. def test_change_best_answer_protected(self):
  1662. """api validates permission to change protected best answers"""
  1663. self.override_acl({
  1664. 'can_mark_best_answers': 2,
  1665. 'can_change_marked_answers': 2,
  1666. 'can_protect_posts': 0,
  1667. })
  1668. best_answer = testutils.reply_thread(self.thread)
  1669. self.thread.best_answer_is_protected = True
  1670. self.thread.save()
  1671. response = self.patch(
  1672. self.api_link, [
  1673. {
  1674. 'op': 'replace',
  1675. 'path': 'best-answer',
  1676. 'value': best_answer.id,
  1677. },
  1678. ]
  1679. )
  1680. self.assertEqual(response.status_code, 403)
  1681. self.assertEqual(response.json(), {
  1682. 'detail': (
  1683. "You don't have permission to change this thread's best answer because a "
  1684. "moderator has protected it."
  1685. ),
  1686. })
  1687. thread_json = self.get_thread_json()
  1688. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1689. # passing scenario is possible
  1690. self.override_acl({
  1691. 'can_mark_best_answers': 2,
  1692. 'can_change_marked_answers': 2,
  1693. 'can_protect_posts': 1,
  1694. })
  1695. response = self.patch(
  1696. self.api_link, [
  1697. {
  1698. 'op': 'replace',
  1699. 'path': 'best-answer',
  1700. 'value': best_answer.id,
  1701. },
  1702. ]
  1703. )
  1704. self.assertEqual(response.status_code, 200)
  1705. def test_change_best_answer_post_validation(self):
  1706. """api validates new post'"""
  1707. self.override_acl({
  1708. 'can_mark_best_answers': 2,
  1709. 'can_change_marked_answers': 2,
  1710. })
  1711. best_answer = testutils.reply_thread(self.thread, is_hidden=True)
  1712. response = self.patch(
  1713. self.api_link, [
  1714. {
  1715. 'op': 'replace',
  1716. 'path': 'best-answer',
  1717. 'value': best_answer.id,
  1718. },
  1719. ]
  1720. )
  1721. self.assertEqual(response.status_code, 403)
  1722. self.assertEqual(response.json(), {
  1723. 'detail': "Hidden posts can't be marked as best answers.",
  1724. })
  1725. thread_json = self.get_thread_json()
  1726. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1727. class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
  1728. def setUp(self):
  1729. super(ThreadUnmarkBestAnswerApiTests, self).setUp()
  1730. self.best_answer = testutils.reply_thread(self.thread)
  1731. self.thread.set_best_answer(self.user, self.best_answer)
  1732. self.thread.save()
  1733. def test_unmark_best_answer(self):
  1734. """api makes it possible to unmark best answer"""
  1735. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1736. response = self.patch(
  1737. self.api_link, [
  1738. {
  1739. 'op': 'remove',
  1740. 'path': 'best-answer',
  1741. 'value': self.best_answer.id,
  1742. },
  1743. ]
  1744. )
  1745. self.assertEqual(response.status_code, 200)
  1746. self.assertEqual(response.json(), {
  1747. 'id': self.thread.id,
  1748. 'best_answer': None,
  1749. 'best_answer_is_protected': False,
  1750. 'best_answer_marked_on': None,
  1751. 'best_answer_marked_by': None,
  1752. 'best_answer_marked_by_name': None,
  1753. 'best_answer_marked_by_slug': None,
  1754. })
  1755. thread_json = self.get_thread_json()
  1756. self.assertIsNone(thread_json['best_answer'])
  1757. self.assertFalse(thread_json['best_answer_is_protected'])
  1758. self.assertIsNone(thread_json['best_answer_marked_on'])
  1759. self.assertIsNone(thread_json['best_answer_marked_by'])
  1760. self.assertIsNone(thread_json['best_answer_marked_by_name'])
  1761. self.assertIsNone(thread_json['best_answer_marked_by_slug'])
  1762. def test_unmark_best_answer_invalid_post_id(self):
  1763. """api validates that post id is int"""
  1764. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1765. response = self.patch(
  1766. self.api_link, [
  1767. {
  1768. 'op': 'remove',
  1769. 'path': 'best-answer',
  1770. 'value': 'd7sd89a7d98sa',
  1771. },
  1772. ]
  1773. )
  1774. self.assertEqual(response.status_code, 400)
  1775. self.assertEqual(response.json(), {
  1776. 'detail': ["A valid integer is required."],
  1777. })
  1778. thread_json = self.get_thread_json()
  1779. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1780. def test_unmark_best_answer_post_not_found(self):
  1781. """api validates that post to unmark exists"""
  1782. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1783. response = self.patch(
  1784. self.api_link, [
  1785. {
  1786. 'op': 'remove',
  1787. 'path': 'best-answer',
  1788. 'value': self.best_answer.id + 1,
  1789. },
  1790. ]
  1791. )
  1792. self.assertEqual(response.status_code, 404)
  1793. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  1794. thread_json = self.get_thread_json()
  1795. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1796. def test_unmark_best_answer_wrong_post(self):
  1797. """api validates if post given to unmark is best answer"""
  1798. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2})
  1799. best_answer = testutils.reply_thread(self.thread)
  1800. response = self.patch(
  1801. self.api_link, [
  1802. {
  1803. 'op': 'remove',
  1804. 'path': 'best-answer',
  1805. 'value': best_answer.id,
  1806. },
  1807. ]
  1808. )
  1809. self.assertEqual(response.status_code, 403)
  1810. self.assertEqual(response.json(), {
  1811. 'detail': (
  1812. "This post can't be unmarked because it's not currently marked as best answer."
  1813. ),
  1814. })
  1815. thread_json = self.get_thread_json()
  1816. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1817. def test_unmark_best_answer_no_permission(self):
  1818. """api validates if user has permission to unmark best answers"""
  1819. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 0})
  1820. response = self.patch(
  1821. self.api_link, [
  1822. {
  1823. 'op': 'remove',
  1824. 'path': 'best-answer',
  1825. 'value': self.best_answer.id,
  1826. },
  1827. ]
  1828. )
  1829. self.assertEqual(response.status_code, 403)
  1830. self.assertEqual(response.json(), {
  1831. 'detail': (
  1832. 'You don\'t have permission to unmark threads answers in the "First category" '
  1833. 'category.'
  1834. ),
  1835. })
  1836. thread_json = self.get_thread_json()
  1837. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1838. def test_unmark_best_answer_not_starter(self):
  1839. """api validates if starter has permission to unmark best answers"""
  1840. self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1})
  1841. response = self.patch(
  1842. self.api_link, [
  1843. {
  1844. 'op': 'remove',
  1845. 'path': 'best-answer',
  1846. 'value': self.best_answer.id,
  1847. },
  1848. ]
  1849. )
  1850. self.assertEqual(response.status_code, 403)
  1851. self.assertEqual(response.json(), {
  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.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1})
  1861. self.thread.starter = self.user
  1862. self.thread.save()
  1863. response = self.patch(
  1864. self.api_link, [
  1865. {
  1866. 'op': 'remove',
  1867. 'path': 'best-answer',
  1868. 'value': self.best_answer.id,
  1869. },
  1870. ]
  1871. )
  1872. self.assertEqual(response.status_code, 200)
  1873. def test_unmark_best_answer_timelimit(self):
  1874. """api validates if starter has permission to unmark best answer within time limit"""
  1875. self.override_acl({
  1876. 'can_mark_best_answers': 0,
  1877. 'can_change_marked_answers': 1,
  1878. 'best_answer_change_time': 5,
  1879. })
  1880. self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
  1881. self.thread.starter = self.user
  1882. self.thread.save()
  1883. response = self.patch(
  1884. self.api_link, [
  1885. {
  1886. 'op': 'remove',
  1887. 'path': 'best-answer',
  1888. 'value': self.best_answer.id,
  1889. },
  1890. ]
  1891. )
  1892. self.assertEqual(response.status_code, 403)
  1893. self.assertEqual(response.json(), {
  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.override_acl({
  1903. 'can_mark_best_answers': 0,
  1904. 'can_change_marked_answers': 1,
  1905. 'best_answer_change_time': 10,
  1906. })
  1907. response = self.patch(
  1908. self.api_link, [
  1909. {
  1910. 'op': 'remove',
  1911. 'path': 'best-answer',
  1912. 'value': self.best_answer.id,
  1913. },
  1914. ]
  1915. )
  1916. self.assertEqual(response.status_code, 200)
  1917. def test_unmark_best_answer_closed_category(self):
  1918. """api validates if user has permission to unmark best answer in closed category"""
  1919. self.override_acl({
  1920. 'can_mark_best_answers': 0,
  1921. 'can_change_marked_answers': 2,
  1922. 'can_close_threads': 0,
  1923. })
  1924. self.category.is_closed = True
  1925. self.category.save()
  1926. response = self.patch(
  1927. self.api_link, [
  1928. {
  1929. 'op': 'remove',
  1930. 'path': 'best-answer',
  1931. 'value': self.best_answer.id,
  1932. },
  1933. ]
  1934. )
  1935. self.assertEqual(response.status_code, 403)
  1936. self.assertEqual(response.json(), {
  1937. 'detail': (
  1938. 'You don\'t have permission to unmark this best answer because its category '
  1939. '"First category" is closed.'
  1940. ),
  1941. })
  1942. thread_json = self.get_thread_json()
  1943. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1944. # passing scenario is possible
  1945. self.override_acl({
  1946. 'can_mark_best_answers': 0,
  1947. 'can_change_marked_answers': 2,
  1948. 'can_close_threads': 1,
  1949. })
  1950. response = self.patch(
  1951. self.api_link, [
  1952. {
  1953. 'op': 'remove',
  1954. 'path': 'best-answer',
  1955. 'value': self.best_answer.id,
  1956. },
  1957. ]
  1958. )
  1959. self.assertEqual(response.status_code, 200)
  1960. def test_unmark_best_answer_closed_thread(self):
  1961. """api validates if user has permission to unmark best answer in closed thread"""
  1962. self.override_acl({
  1963. 'can_mark_best_answers': 0,
  1964. 'can_change_marked_answers': 2,
  1965. 'can_close_threads': 0,
  1966. })
  1967. self.thread.is_closed = True
  1968. self.thread.save()
  1969. response = self.patch(
  1970. self.api_link, [
  1971. {
  1972. 'op': 'remove',
  1973. 'path': 'best-answer',
  1974. 'value': self.best_answer.id,
  1975. },
  1976. ]
  1977. )
  1978. self.assertEqual(response.status_code, 403)
  1979. self.assertEqual(response.json(), {
  1980. 'detail': (
  1981. "You can't unmark this thread's best answer because it's closed and you don't "
  1982. "have permission to open it."
  1983. ),
  1984. })
  1985. thread_json = self.get_thread_json()
  1986. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  1987. # passing scenario is possible
  1988. self.override_acl({
  1989. 'can_mark_best_answers': 0,
  1990. 'can_change_marked_answers': 2,
  1991. 'can_close_threads': 1,
  1992. })
  1993. response = self.patch(
  1994. self.api_link, [
  1995. {
  1996. 'op': 'remove',
  1997. 'path': 'best-answer',
  1998. 'value': self.best_answer.id,
  1999. },
  2000. ]
  2001. )
  2002. self.assertEqual(response.status_code, 200)
  2003. def test_unmark_best_answer_protected(self):
  2004. """api validates permission to unmark protected best answers"""
  2005. self.override_acl({
  2006. 'can_mark_best_answers': 0,
  2007. 'can_change_marked_answers': 2,
  2008. 'can_protect_posts': 0,
  2009. })
  2010. self.thread.best_answer_is_protected = True
  2011. self.thread.save()
  2012. response = self.patch(
  2013. self.api_link, [
  2014. {
  2015. 'op': 'remove',
  2016. 'path': 'best-answer',
  2017. 'value': self.best_answer.id,
  2018. },
  2019. ]
  2020. )
  2021. self.assertEqual(response.status_code, 403)
  2022. self.assertEqual(response.json(), {
  2023. 'detail': (
  2024. "You don't have permission to unmark this thread's best answer because a "
  2025. "moderator has protected it."
  2026. ),
  2027. })
  2028. thread_json = self.get_thread_json()
  2029. self.assertEqual(thread_json['best_answer'], self.best_answer.id)
  2030. # passing scenario is possible
  2031. self.override_acl({
  2032. 'can_mark_best_answers': 0,
  2033. 'can_change_marked_answers': 2,
  2034. 'can_protect_posts': 1,
  2035. })
  2036. response = self.patch(
  2037. self.api_link, [
  2038. {
  2039. 'op': 'remove',
  2040. 'path': 'best-answer',
  2041. 'value': self.best_answer.id,
  2042. },
  2043. ]
  2044. )
  2045. self.assertEqual(response.status_code, 200)