test_thread_patch_api.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351
  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.models import Thread
  8. from .test_threads_api import ThreadsApiTestCase
  9. class ThreadPatchApiTestCase(ThreadsApiTestCase):
  10. def patch(self, api_link, ops):
  11. return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
  12. class ThreadAddAclApiTests(ThreadPatchApiTestCase):
  13. def test_add_acl_true(self):
  14. """api adds current thread's acl to response"""
  15. response = self.patch(self.api_link, [
  16. {
  17. 'op': 'add',
  18. 'path': 'acl',
  19. 'value': True,
  20. },
  21. ])
  22. self.assertEqual(response.status_code, 200)
  23. response_json = response.json()
  24. self.assertTrue(response_json['acl'])
  25. def test_add_acl_false(self):
  26. """if value is false, api won't add acl to the response, but will set empty key"""
  27. response = self.patch(self.api_link, [
  28. {
  29. 'op': 'add',
  30. 'path': 'acl',
  31. 'value': False,
  32. },
  33. ])
  34. self.assertEqual(response.status_code, 200)
  35. response_json = response.json()
  36. self.assertIsNone(response_json['acl'])
  37. class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
  38. def test_change_thread_title(self):
  39. """api makes it possible to change thread title"""
  40. self.override_acl({'can_edit_threads': 2})
  41. response = self.patch(
  42. self.api_link, [
  43. {
  44. 'op': 'replace',
  45. 'path': 'title',
  46. 'value': "Lorem ipsum change!",
  47. },
  48. ]
  49. )
  50. self.assertEqual(response.status_code, 200)
  51. response_json = response.json()
  52. self.assertEqual(response_json['title'], "Lorem ipsum change!")
  53. thread_json = self.get_thread_json()
  54. self.assertEqual(thread_json['title'], "Lorem ipsum change!")
  55. def test_change_thread_title_no_permission(self):
  56. """api validates permission to change title"""
  57. self.override_acl({'can_edit_threads': 0})
  58. response = self.patch(
  59. self.api_link, [
  60. {
  61. 'op': 'replace',
  62. 'path': 'title',
  63. 'value': "Lorem ipsum change!",
  64. },
  65. ]
  66. )
  67. self.assertEqual(response.status_code, 403)
  68. self.assertEqual(response.json(), {
  69. 'detail': "You can't edit threads in this category."
  70. })
  71. def test_change_thread_title_closed_category_no_permission(self):
  72. """api test permission to edit thread title in closed category"""
  73. self.override_acl({
  74. 'can_edit_threads': 2,
  75. 'can_close_threads': 0
  76. })
  77. self.category.is_closed = True
  78. self.category.save()
  79. response = self.patch(
  80. self.api_link, [
  81. {
  82. 'op': 'replace',
  83. 'path': 'title',
  84. 'value': "Lorem ipsum change!",
  85. },
  86. ]
  87. )
  88. self.assertEqual(response.status_code, 403)
  89. self.assertEqual(response.json(), {
  90. 'detail': "This category is closed. You can't edit threads in it."
  91. })
  92. def test_change_thread_title_closed_thread_no_permission(self):
  93. """api test permission to edit closed thread title"""
  94. self.override_acl({
  95. 'can_edit_threads': 2,
  96. 'can_close_threads': 0
  97. })
  98. self.thread.is_closed = True
  99. self.thread.save()
  100. response = self.patch(
  101. self.api_link, [
  102. {
  103. 'op': 'replace',
  104. 'path': 'title',
  105. 'value': "Lorem ipsum change!",
  106. },
  107. ]
  108. )
  109. self.assertEqual(response.status_code, 403)
  110. self.assertEqual(response.json(), {
  111. 'detail': "This thread is closed. You can't edit it."
  112. })
  113. def test_change_thread_title_after_edit_time(self):
  114. """api cleans, validates and rejects too short title"""
  115. self.override_acl({'thread_edit_time': 1, 'can_edit_threads': 1})
  116. self.thread.starter = self.user
  117. self.thread.started_on = timezone.now() - timedelta(minutes=10)
  118. self.thread.save()
  119. response = self.patch(
  120. self.api_link, [
  121. {
  122. 'op': 'replace',
  123. 'path': 'title',
  124. 'value': "Lorem ipsum change!",
  125. },
  126. ]
  127. )
  128. self.assertEqual(response.status_code, 403)
  129. self.assertEqual(response.json(), {
  130. 'detail': "You can't edit threads that are older than 1 minute."
  131. })
  132. def test_change_thread_title_invalid(self):
  133. """api cleans, validates and rejects too short title"""
  134. self.override_acl({'can_edit_threads': 2})
  135. response = self.patch(
  136. self.api_link, [
  137. {
  138. 'op': 'replace',
  139. 'path': 'title',
  140. 'value': 12,
  141. },
  142. ]
  143. )
  144. self.assertEqual(response.status_code, 400)
  145. self.assertEqual(response.json(), {
  146. 'detail': ["Thread title should be at least 5 characters long (it has 2)."]
  147. })
  148. class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
  149. def test_pin_thread(self):
  150. """api makes it possible to pin globally thread"""
  151. self.override_acl({'can_pin_threads': 2})
  152. response = self.patch(
  153. self.api_link, [
  154. {
  155. 'op': 'replace',
  156. 'path': 'weight',
  157. 'value': 2,
  158. },
  159. ]
  160. )
  161. self.assertEqual(response.status_code, 200)
  162. response_json = response.json()
  163. self.assertEqual(response_json['weight'], 2)
  164. thread_json = self.get_thread_json()
  165. self.assertEqual(thread_json['weight'], 2)
  166. def test_pin_thread_closed_category_no_permission(self):
  167. """api checks if category is closed"""
  168. self.override_acl({
  169. 'can_pin_threads': 2,
  170. 'can_close_threads': 0,
  171. })
  172. self.category.is_closed = True
  173. self.category.save()
  174. response = self.patch(
  175. self.api_link, [
  176. {
  177. 'op': 'replace',
  178. 'path': 'weight',
  179. 'value': 2,
  180. },
  181. ]
  182. )
  183. self.assertEqual(response.status_code, 403)
  184. self.assertEqual(response.json(), {
  185. 'detail': "This category is closed. You can't change threads weights in it."
  186. })
  187. def test_pin_thread_closed_no_permission(self):
  188. """api checks if thread is closed"""
  189. self.override_acl({
  190. 'can_pin_threads': 2,
  191. 'can_close_threads': 0,
  192. })
  193. self.thread.is_closed = True
  194. self.thread.save()
  195. response = self.patch(
  196. self.api_link, [
  197. {
  198. 'op': 'replace',
  199. 'path': 'weight',
  200. 'value': 2,
  201. },
  202. ]
  203. )
  204. self.assertEqual(response.status_code, 403)
  205. self.assertEqual(response.json(), {
  206. 'detail': "This thread is closed. You can't change its weight."
  207. })
  208. def test_unpin_thread(self):
  209. """api makes it possible to unpin thread"""
  210. self.thread.weight = 2
  211. self.thread.save()
  212. thread_json = self.get_thread_json()
  213. self.assertEqual(thread_json['weight'], 2)
  214. self.override_acl({'can_pin_threads': 2})
  215. response = self.patch(
  216. self.api_link, [
  217. {
  218. 'op': 'replace',
  219. 'path': 'weight',
  220. 'value': 0,
  221. },
  222. ]
  223. )
  224. self.assertEqual(response.status_code, 200)
  225. response_json = response.json()
  226. self.assertEqual(response_json['weight'], 0)
  227. thread_json = self.get_thread_json()
  228. self.assertEqual(thread_json['weight'], 0)
  229. def test_pin_thread_no_permission(self):
  230. """api pin thread globally with no permission fails"""
  231. self.override_acl({'can_pin_threads': 1})
  232. response = self.patch(
  233. self.api_link, [
  234. {
  235. 'op': 'replace',
  236. 'path': 'weight',
  237. 'value': 2,
  238. },
  239. ]
  240. )
  241. self.assertEqual(response.status_code, 403)
  242. self.assertEqual(response.json(), {
  243. 'detail': "You can't pin threads globally in this category."
  244. })
  245. thread_json = self.get_thread_json()
  246. self.assertEqual(thread_json['weight'], 0)
  247. def test_unpin_thread_no_permission(self):
  248. """api unpin thread with no permission fails"""
  249. self.thread.weight = 2
  250. self.thread.save()
  251. thread_json = self.get_thread_json()
  252. self.assertEqual(thread_json['weight'], 2)
  253. self.override_acl({'can_pin_threads': 1})
  254. response = self.patch(
  255. self.api_link, [
  256. {
  257. 'op': 'replace',
  258. 'path': 'weight',
  259. 'value': 1,
  260. },
  261. ]
  262. )
  263. self.assertEqual(response.status_code, 403)
  264. self.assertEqual(response.json(), {
  265. 'detail': "You can't change globally pinned threads weights in this category."
  266. })
  267. thread_json = self.get_thread_json()
  268. self.assertEqual(thread_json['weight'], 2)
  269. class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
  270. def test_pin_thread(self):
  271. """api makes it possible to pin locally thread"""
  272. self.override_acl({'can_pin_threads': 1})
  273. response = self.patch(
  274. self.api_link, [
  275. {
  276. 'op': 'replace',
  277. 'path': 'weight',
  278. 'value': 1,
  279. },
  280. ]
  281. )
  282. self.assertEqual(response.status_code, 200)
  283. response_json = response.json()
  284. self.assertEqual(response_json['weight'], 1)
  285. thread_json = self.get_thread_json()
  286. self.assertEqual(thread_json['weight'], 1)
  287. def test_unpin_thread(self):
  288. """api makes it possible to unpin thread"""
  289. self.thread.weight = 1
  290. self.thread.save()
  291. thread_json = self.get_thread_json()
  292. self.assertEqual(thread_json['weight'], 1)
  293. self.override_acl({'can_pin_threads': 1})
  294. response = self.patch(
  295. self.api_link, [
  296. {
  297. 'op': 'replace',
  298. 'path': 'weight',
  299. 'value': 0,
  300. },
  301. ]
  302. )
  303. self.assertEqual(response.status_code, 200)
  304. response_json = response.json()
  305. self.assertEqual(response_json['weight'], 0)
  306. thread_json = self.get_thread_json()
  307. self.assertEqual(thread_json['weight'], 0)
  308. def test_pin_thread_no_permission(self):
  309. """api pin thread locally with no permission fails"""
  310. self.override_acl({'can_pin_threads': 0})
  311. response = self.patch(
  312. self.api_link, [
  313. {
  314. 'op': 'replace',
  315. 'path': 'weight',
  316. 'value': 1,
  317. },
  318. ]
  319. )
  320. self.assertEqual(response.status_code, 403)
  321. self.assertEqual(response.json(), {
  322. 'detail': "You can't change threads weights in this category."
  323. })
  324. thread_json = self.get_thread_json()
  325. self.assertEqual(thread_json['weight'], 0)
  326. def test_unpin_thread_no_permission(self):
  327. """api unpin thread with no permission fails"""
  328. self.thread.weight = 1
  329. self.thread.save()
  330. thread_json = self.get_thread_json()
  331. self.assertEqual(thread_json['weight'], 1)
  332. self.override_acl({'can_pin_threads': 0})
  333. response = self.patch(
  334. self.api_link, [
  335. {
  336. 'op': 'replace',
  337. 'path': 'weight',
  338. 'value': 0,
  339. },
  340. ]
  341. )
  342. self.assertEqual(response.status_code, 403)
  343. self.assertEqual(response.json(), {
  344. 'detail': "You can't change threads weights in this category."
  345. })
  346. thread_json = self.get_thread_json()
  347. self.assertEqual(thread_json['weight'], 1)
  348. class ThreadMoveApiTests(ThreadPatchApiTestCase):
  349. def setUp(self):
  350. super(ThreadMoveApiTests, self).setUp()
  351. Category(
  352. name='Category B',
  353. slug='category-b',
  354. ).insert_at(
  355. self.category,
  356. position='last-child',
  357. save=True,
  358. )
  359. self.category_b = Category.objects.get(slug='category-b')
  360. def override_other_acl(self, acl):
  361. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  362. other_category_acl.update({
  363. 'can_see': 1,
  364. 'can_browse': 1,
  365. 'can_see_all_threads': 1,
  366. 'can_see_own_threads': 0,
  367. 'can_hide_threads': 0,
  368. 'can_approve_content': 0,
  369. })
  370. other_category_acl.update(acl)
  371. categories_acl = self.user.acl_cache['categories']
  372. categories_acl[self.category_b.pk] = other_category_acl
  373. visible_categories = [self.category.pk]
  374. if other_category_acl['can_see']:
  375. visible_categories.append(self.category_b.pk)
  376. override_acl(
  377. self.user, {
  378. 'visible_categories': visible_categories,
  379. 'categories': categories_acl,
  380. }
  381. )
  382. def test_move_thread_no_top(self):
  383. """api moves thread to other category, sets no top category"""
  384. self.override_acl({'can_move_threads': True})
  385. self.override_other_acl({'can_start_threads': 2})
  386. response = self.patch(
  387. self.api_link, [
  388. {
  389. 'op': 'replace',
  390. 'path': 'category',
  391. 'value': self.category_b.pk,
  392. },
  393. {
  394. 'op': 'add',
  395. 'path': 'top-category',
  396. 'value': self.category_b.pk,
  397. },
  398. {
  399. 'op': 'replace',
  400. 'path': 'flatten-categories',
  401. 'value': None,
  402. },
  403. ]
  404. )
  405. self.assertEqual(response.status_code, 200)
  406. reponse_json = response.json()
  407. self.assertEqual(reponse_json['category'], self.category_b.pk)
  408. self.override_other_acl({})
  409. thread_json = self.get_thread_json()
  410. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  411. def test_move_thread_with_top(self):
  412. """api moves thread to other category, sets top"""
  413. self.override_acl({'can_move_threads': True})
  414. self.override_other_acl({'can_start_threads': 2})
  415. response = self.patch(
  416. self.api_link, [
  417. {
  418. 'op': 'replace',
  419. 'path': 'category',
  420. 'value': self.category_b.pk,
  421. },
  422. {
  423. 'op': 'add',
  424. 'path': 'top-category',
  425. 'value': Category.objects.root_category().pk,
  426. },
  427. {
  428. 'op': 'replace',
  429. 'path': 'flatten-categories',
  430. 'value': None,
  431. },
  432. ]
  433. )
  434. self.assertEqual(response.status_code, 200)
  435. reponse_json = response.json()
  436. self.assertEqual(reponse_json['category'], self.category_b.pk)
  437. self.override_other_acl({})
  438. thread_json = self.get_thread_json()
  439. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  440. def test_move_thread_reads(self):
  441. """api moves thread reads together with thread"""
  442. self.override_acl({'can_move_threads': True})
  443. self.override_other_acl({'can_start_threads': 2})
  444. poststracker.save_read(self.user, self.thread.first_post)
  445. self.assertEqual(self.user.postread_set.count(), 1)
  446. self.user.postread_set.get(category=self.category)
  447. response = self.patch(
  448. self.api_link, [
  449. {
  450. 'op': 'replace',
  451. 'path': 'category',
  452. 'value': self.category_b.pk,
  453. },
  454. {
  455. 'op': 'add',
  456. 'path': 'top-category',
  457. 'value': self.category_b.pk,
  458. },
  459. {
  460. 'op': 'replace',
  461. 'path': 'flatten-categories',
  462. 'value': None,
  463. },
  464. ]
  465. )
  466. self.assertEqual(response.status_code, 200)
  467. # thread read was moved to new category
  468. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  469. self.assertEqual(postreads.count(), 1)
  470. postreads.get(category=self.category_b)
  471. def test_move_thread_subscriptions(self):
  472. """api moves thread subscriptions together with thread"""
  473. self.override_acl({'can_move_threads': True})
  474. self.override_other_acl({'can_start_threads': 2})
  475. self.user.subscription_set.create(
  476. thread=self.thread,
  477. category=self.thread.category,
  478. last_read_on=self.thread.last_post_on,
  479. send_email=False,
  480. )
  481. self.assertEqual(self.user.subscription_set.count(), 1)
  482. self.user.subscription_set.get(category=self.category)
  483. response = self.patch(
  484. self.api_link, [
  485. {
  486. 'op': 'replace',
  487. 'path': 'category',
  488. 'value': self.category_b.pk,
  489. },
  490. {
  491. 'op': 'add',
  492. 'path': 'top-category',
  493. 'value': self.category_b.pk,
  494. },
  495. {
  496. 'op': 'replace',
  497. 'path': 'flatten-categories',
  498. 'value': None,
  499. },
  500. ]
  501. )
  502. self.assertEqual(response.status_code, 200)
  503. # thread read was moved to new category
  504. self.assertEqual(self.user.subscription_set.count(), 1)
  505. self.user.subscription_set.get(category=self.category_b)
  506. def test_move_thread_no_permission(self):
  507. """api move thread to other category with no permission fails"""
  508. self.override_acl({'can_move_threads': False})
  509. self.override_other_acl({})
  510. response = self.patch(
  511. self.api_link, [
  512. {
  513. 'op': 'replace',
  514. 'path': 'category',
  515. 'value': self.category_b.pk,
  516. },
  517. ]
  518. )
  519. self.assertEqual(response.status_code, 403)
  520. self.assertEqual(response.json(), {
  521. 'detail': "You can't move threads in this category."
  522. })
  523. self.override_other_acl({})
  524. thread_json = self.get_thread_json()
  525. self.assertEqual(thread_json['category']['id'], self.category.pk)
  526. def test_move_thread_closed_category_no_permission(self):
  527. """api move thread from closed category with no permission fails"""
  528. self.override_acl({
  529. 'can_move_threads': True,
  530. 'can_close_threads': False,
  531. })
  532. self.override_other_acl({})
  533. self.category.is_closed = True
  534. self.category.save()
  535. response = self.patch(
  536. self.api_link, [
  537. {
  538. 'op': 'replace',
  539. 'path': 'category',
  540. 'value': self.category_b.pk,
  541. },
  542. ]
  543. )
  544. self.assertEqual(response.status_code, 403)
  545. self.assertEqual(response.json(), {
  546. 'detail': "This category is closed. You can't move it's threads."
  547. })
  548. def test_move_closed_thread_no_permission(self):
  549. """api move closed thread with no permission fails"""
  550. self.override_acl({
  551. 'can_move_threads': True,
  552. 'can_close_threads': False,
  553. })
  554. self.override_other_acl({})
  555. self.thread.is_closed = True
  556. self.thread.save()
  557. response = self.patch(
  558. self.api_link, [
  559. {
  560. 'op': 'replace',
  561. 'path': 'category',
  562. 'value': self.category_b.pk,
  563. },
  564. ]
  565. )
  566. self.assertEqual(response.status_code, 403)
  567. self.assertEqual(response.json(), {
  568. 'detail': "This thread is closed. You can't move it."
  569. })
  570. def test_move_thread_no_category_access(self):
  571. """api move thread to category with no access fails"""
  572. self.override_acl({'can_move_threads': True})
  573. self.override_other_acl({'can_see': False})
  574. response = self.patch(
  575. self.api_link, [
  576. {
  577. 'op': 'replace',
  578. 'path': 'category',
  579. 'value': self.category_b.pk,
  580. },
  581. ]
  582. )
  583. self.assertEqual(response.status_code, 404)
  584. self.assertEqual(response.json(), {
  585. 'detail': "NOT FOUND"
  586. })
  587. self.override_other_acl({})
  588. thread_json = self.get_thread_json()
  589. self.assertEqual(thread_json['category']['id'], self.category.pk)
  590. def test_move_thread_no_category_browse(self):
  591. """api move thread to category with no browsing access fails"""
  592. self.override_acl({'can_move_threads': True})
  593. self.override_other_acl({'can_browse': False})
  594. response = self.patch(
  595. self.api_link, [
  596. {
  597. 'op': 'replace',
  598. 'path': 'category',
  599. 'value': self.category_b.pk,
  600. },
  601. ]
  602. )
  603. self.assertEqual(response.status_code, 403)
  604. self.assertEqual(response.json(), {
  605. 'detail': 'You don\'t have permission to browse "Category B" contents.'
  606. })
  607. self.override_other_acl({})
  608. thread_json = self.get_thread_json()
  609. self.assertEqual(thread_json['category']['id'], self.category.pk)
  610. def test_move_thread_no_category_start_threads(self):
  611. """api move thread to category with no posting access fails"""
  612. self.override_acl({'can_move_threads': True})
  613. self.override_other_acl({'can_start_threads': False})
  614. response = self.patch(
  615. self.api_link, [
  616. {
  617. 'op': 'replace',
  618. 'path': 'category',
  619. 'value': self.category_b.pk,
  620. },
  621. ]
  622. )
  623. self.assertEqual(response.status_code, 403)
  624. self.assertEqual(response.json(), {
  625. 'detail': "You don't have permission to start new threads in this category."
  626. })
  627. self.override_other_acl({})
  628. thread_json = self.get_thread_json()
  629. self.assertEqual(thread_json['category']['id'], self.category.pk)
  630. def test_move_thread_same_category(self):
  631. """api move thread to category it's already in fails"""
  632. self.override_acl({'can_move_threads': True})
  633. self.override_other_acl({'can_start_threads': 2})
  634. response = self.patch(
  635. self.api_link, [
  636. {
  637. 'op': 'replace',
  638. 'path': 'category',
  639. 'value': self.thread.category_id,
  640. },
  641. ]
  642. )
  643. self.assertEqual(response.status_code, 400)
  644. self.assertEqual(response.json(), {
  645. 'detail': ["You can't move thread to the category it's already in."]
  646. })
  647. self.override_other_acl({})
  648. thread_json = self.get_thread_json()
  649. self.assertEqual(thread_json['category']['id'], self.category.pk)
  650. def test_thread_flatten_categories(self):
  651. """api flatten thread categories"""
  652. response = self.patch(
  653. self.api_link, [
  654. {
  655. 'op': 'replace',
  656. 'path': 'flatten-categories',
  657. 'value': None,
  658. },
  659. ]
  660. )
  661. self.assertEqual(response.status_code, 200)
  662. response_json = response.json()
  663. self.assertEqual(response_json['category'], self.category.pk)
  664. class ThreadCloseApiTests(ThreadPatchApiTestCase):
  665. def test_close_thread(self):
  666. """api makes it possible to close thread"""
  667. self.override_acl({'can_close_threads': True})
  668. response = self.patch(
  669. self.api_link, [
  670. {
  671. 'op': 'replace',
  672. 'path': 'is-closed',
  673. 'value': True,
  674. },
  675. ]
  676. )
  677. self.assertEqual(response.status_code, 200)
  678. response_json = response.json()
  679. self.assertTrue(response_json['is_closed'])
  680. thread_json = self.get_thread_json()
  681. self.assertTrue(thread_json['is_closed'])
  682. def test_open_thread(self):
  683. """api makes it possible to open thread"""
  684. self.thread.is_closed = True
  685. self.thread.save()
  686. thread_json = self.get_thread_json()
  687. self.assertTrue(thread_json['is_closed'])
  688. self.override_acl({'can_close_threads': True})
  689. response = self.patch(
  690. self.api_link, [
  691. {
  692. 'op': 'replace',
  693. 'path': 'is-closed',
  694. 'value': False,
  695. },
  696. ]
  697. )
  698. self.assertEqual(response.status_code, 200)
  699. response_json = response.json()
  700. self.assertFalse(response_json['is_closed'])
  701. thread_json = self.get_thread_json()
  702. self.assertFalse(thread_json['is_closed'])
  703. def test_close_thread_no_permission(self):
  704. """api close thread with no permission fails"""
  705. self.override_acl({'can_close_threads': False})
  706. response = self.patch(
  707. self.api_link, [
  708. {
  709. 'op': 'replace',
  710. 'path': 'is-closed',
  711. 'value': True,
  712. },
  713. ]
  714. )
  715. self.assertEqual(response.status_code, 403)
  716. self.assertEqual(response.json(), {
  717. 'detail': "You don't have permission to close this thread."
  718. })
  719. thread_json = self.get_thread_json()
  720. self.assertFalse(thread_json['is_closed'])
  721. def test_open_thread_no_permission(self):
  722. """api open thread with no permission fails"""
  723. self.thread.is_closed = True
  724. self.thread.save()
  725. thread_json = self.get_thread_json()
  726. self.assertTrue(thread_json['is_closed'])
  727. self.override_acl({'can_close_threads': False})
  728. response = self.patch(
  729. self.api_link, [
  730. {
  731. 'op': 'replace',
  732. 'path': 'is-closed',
  733. 'value': False,
  734. },
  735. ]
  736. )
  737. self.assertEqual(response.status_code, 403)
  738. self.assertEqual(response.json(), {
  739. 'detail': "You don't have permission to open this thread."
  740. })
  741. thread_json = self.get_thread_json()
  742. self.assertTrue(thread_json['is_closed'])
  743. class ThreadApproveApiTests(ThreadPatchApiTestCase):
  744. def test_approve_thread(self):
  745. """api makes it possible to approve thread"""
  746. self.thread.first_post.is_unapproved = True
  747. self.thread.first_post.save()
  748. self.thread.synchronize()
  749. self.thread.save()
  750. self.assertTrue(self.thread.is_unapproved)
  751. self.assertTrue(self.thread.has_unapproved_posts)
  752. self.override_acl({'can_approve_content': 1})
  753. response = self.patch(
  754. self.api_link, [
  755. {
  756. 'op': 'replace',
  757. 'path': 'is-unapproved',
  758. 'value': False,
  759. },
  760. ]
  761. )
  762. self.assertEqual(response.status_code, 200)
  763. response_json = response.json()
  764. self.assertFalse(response_json['is_unapproved'])
  765. self.assertFalse(response_json['has_unapproved_posts'])
  766. thread_json = self.get_thread_json()
  767. self.assertFalse(thread_json['is_unapproved'])
  768. self.assertFalse(thread_json['has_unapproved_posts'])
  769. thread = Thread.objects.get(pk=self.thread.pk)
  770. self.assertFalse(thread.is_unapproved)
  771. self.assertFalse(thread.has_unapproved_posts)
  772. def test_approve_thread_category_closed_no_permission(self):
  773. """api checks permission for approving threads in closed categories"""
  774. self.thread.first_post.is_unapproved = True
  775. self.thread.first_post.save()
  776. self.thread.synchronize()
  777. self.thread.save()
  778. self.assertTrue(self.thread.is_unapproved)
  779. self.assertTrue(self.thread.has_unapproved_posts)
  780. self.category.is_closed = True
  781. self.category.save()
  782. self.override_acl({
  783. 'can_approve_content': 1,
  784. 'can_close_threads': 0,
  785. })
  786. response = self.patch(
  787. self.api_link, [
  788. {
  789. 'op': 'replace',
  790. 'path': 'is-unapproved',
  791. 'value': False,
  792. },
  793. ]
  794. )
  795. self.assertEqual(response.status_code, 403)
  796. self.assertEqual(response.json(), {
  797. 'detail': "This category is closed. You can't approve threads in it."
  798. })
  799. def test_approve_thread_closed_no_permission(self):
  800. """api checks permission for approving posts in closed categories"""
  801. self.thread.first_post.is_unapproved = True
  802. self.thread.first_post.save()
  803. self.thread.synchronize()
  804. self.thread.save()
  805. self.assertTrue(self.thread.is_unapproved)
  806. self.assertTrue(self.thread.has_unapproved_posts)
  807. self.thread.is_closed = True
  808. self.thread.save()
  809. self.override_acl({
  810. 'can_approve_content': 1,
  811. 'can_close_threads': 0,
  812. })
  813. response = self.patch(
  814. self.api_link, [
  815. {
  816. 'op': 'replace',
  817. 'path': 'is-unapproved',
  818. 'value': False,
  819. },
  820. ]
  821. )
  822. self.assertEqual(response.status_code, 403)
  823. self.assertEqual(response.json(), {
  824. 'detail': "This thread is closed. You can't approve it."
  825. })
  826. def test_unapprove_thread(self):
  827. """api returns permission error on approval removal"""
  828. self.override_acl({'can_approve_content': 1})
  829. response = self.patch(
  830. self.api_link, [
  831. {
  832. 'op': 'replace',
  833. 'path': 'is-unapproved',
  834. 'value': True,
  835. },
  836. ]
  837. )
  838. self.assertEqual(response.status_code, 403)
  839. self.assertEqual(response.json(), {
  840. 'detail': "Content approval can't be reversed."
  841. })
  842. class ThreadHideApiTests(ThreadPatchApiTestCase):
  843. def test_hide_thread(self):
  844. """api makes it possible to hide thread"""
  845. self.override_acl({'can_hide_threads': 1})
  846. response = self.patch(
  847. self.api_link, [
  848. {
  849. 'op': 'replace',
  850. 'path': 'is-hidden',
  851. 'value': True,
  852. },
  853. ]
  854. )
  855. self.assertEqual(response.status_code, 200)
  856. reponse_json = response.json()
  857. self.assertTrue(reponse_json['is_hidden'])
  858. self.override_acl({'can_hide_threads': 1})
  859. thread_json = self.get_thread_json()
  860. self.assertTrue(thread_json['is_hidden'])
  861. def test_hide_thread_no_permission(self):
  862. """api hide thread with no permission fails"""
  863. self.override_acl({'can_hide_threads': 0})
  864. response = self.patch(
  865. self.api_link, [
  866. {
  867. 'op': 'replace',
  868. 'path': 'is-hidden',
  869. 'value': True,
  870. },
  871. ]
  872. )
  873. self.assertEqual(response.status_code, 403)
  874. self.assertEqual(response.json(), {
  875. 'detail': "You can't hide threads in this category."
  876. })
  877. thread_json = self.get_thread_json()
  878. self.assertFalse(thread_json['is_hidden'])
  879. def test_hide_non_owned_thread(self):
  880. """api forbids non-moderator from hiding other users threads"""
  881. self.override_acl({
  882. 'can_hide_own_threads': 1,
  883. 'can_hide_threads': 0
  884. })
  885. response = self.patch(
  886. self.api_link, [
  887. {
  888. 'op': 'replace',
  889. 'path': 'is-hidden',
  890. 'value': True,
  891. },
  892. ]
  893. )
  894. self.assertEqual(response.status_code, 403)
  895. self.assertEqual(response.json(), {
  896. 'detail': "You can't hide other users theads in this category."
  897. })
  898. def test_hide_owned_thread_no_time(self):
  899. """api forbids non-moderator from hiding other users threads"""
  900. self.override_acl({
  901. 'can_hide_own_threads': 1,
  902. 'can_hide_threads': 0,
  903. 'thread_edit_time': 1,
  904. })
  905. self.thread.starter = self.user
  906. self.thread.started_on = timezone.now() - timedelta(minutes=5)
  907. self.thread.save()
  908. response = self.patch(
  909. self.api_link, [
  910. {
  911. 'op': 'replace',
  912. 'path': 'is-hidden',
  913. 'value': True,
  914. },
  915. ]
  916. )
  917. self.assertEqual(response.status_code, 403)
  918. self.assertEqual(response.json(), {
  919. 'detail': "You can't hide threads that are older than 1 minute."
  920. })
  921. def test_hide_closed_category_no_permission(self):
  922. """api test permission to hide thread in closed category"""
  923. self.override_acl({
  924. 'can_hide_threads': 1,
  925. 'can_close_threads': 0
  926. })
  927. self.category.is_closed = True
  928. self.category.save()
  929. response = self.patch(
  930. self.api_link, [
  931. {
  932. 'op': 'replace',
  933. 'path': 'is-hidden',
  934. 'value': True,
  935. },
  936. ]
  937. )
  938. self.assertEqual(response.status_code, 403)
  939. self.assertEqual(response.json(), {
  940. 'detail': "This category is closed. You can't hide threads in it."
  941. })
  942. def test_hide_closed_thread_no_permission(self):
  943. """api test permission to hide closed thread"""
  944. self.override_acl({
  945. 'can_hide_threads': 1,
  946. 'can_close_threads': 0
  947. })
  948. self.thread.is_closed = True
  949. self.thread.save()
  950. response = self.patch(
  951. self.api_link, [
  952. {
  953. 'op': 'replace',
  954. 'path': 'is-hidden',
  955. 'value': True,
  956. },
  957. ]
  958. )
  959. self.assertEqual(response.status_code, 403)
  960. self.assertEqual(response.json(), {
  961. 'detail': "This thread is closed. You can't hide it."
  962. })
  963. class ThreadUnhideApiTests(ThreadPatchApiTestCase):
  964. def setUp(self):
  965. super(ThreadUnhideApiTests, self).setUp()
  966. self.thread.is_hidden = True
  967. self.thread.save()
  968. def test_unhide_thread(self):
  969. """api makes it possible to unhide thread"""
  970. self.override_acl({'can_hide_threads': 1})
  971. response = self.patch(
  972. self.api_link, [
  973. {
  974. 'op': 'replace',
  975. 'path': 'is-hidden',
  976. 'value': False,
  977. },
  978. ]
  979. )
  980. self.assertEqual(response.status_code, 200)
  981. reponse_json = response.json()
  982. self.assertFalse(reponse_json['is_hidden'])
  983. self.override_acl({'can_hide_threads': 1})
  984. thread_json = self.get_thread_json()
  985. self.assertFalse(thread_json['is_hidden'])
  986. def test_unhide_thread_no_permission(self):
  987. """api unhide thread with no permission fails as thread is invisible"""
  988. self.override_acl({'can_hide_threads': 0})
  989. response = self.patch(
  990. self.api_link, [
  991. {
  992. 'op': 'replace',
  993. 'path': 'is-hidden',
  994. 'value': True,
  995. },
  996. ]
  997. )
  998. self.assertEqual(response.status_code, 404)
  999. def test_unhide_closed_category_no_permission(self):
  1000. """api test permission to unhide thread in closed category"""
  1001. self.override_acl({
  1002. 'can_hide_threads': 1,
  1003. 'can_close_threads': 0
  1004. })
  1005. self.category.is_closed = True
  1006. self.category.save()
  1007. response = self.patch(
  1008. self.api_link, [
  1009. {
  1010. 'op': 'replace',
  1011. 'path': 'is-hidden',
  1012. 'value': False,
  1013. },
  1014. ]
  1015. )
  1016. self.assertEqual(response.status_code, 403)
  1017. self.assertEqual(response.json(), {
  1018. 'detail': "This category is closed. You can't reveal threads in it."
  1019. })
  1020. def test_unhide_closed_thread_no_permission(self):
  1021. """api test permission to unhide closed thread"""
  1022. self.override_acl({
  1023. 'can_hide_threads': 1,
  1024. 'can_close_threads': 0
  1025. })
  1026. self.thread.is_closed = True
  1027. self.thread.save()
  1028. response = self.patch(
  1029. self.api_link, [
  1030. {
  1031. 'op': 'replace',
  1032. 'path': 'is-hidden',
  1033. 'value': False,
  1034. },
  1035. ]
  1036. )
  1037. self.assertEqual(response.status_code, 403)
  1038. self.assertEqual(response.json(), {
  1039. 'detail': "This thread is closed. You can't reveal it."
  1040. })
  1041. class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
  1042. def test_subscribe_thread(self):
  1043. """api makes it possible to subscribe thread"""
  1044. response = self.patch(
  1045. self.api_link, [
  1046. {
  1047. 'op': 'replace',
  1048. 'path': 'subscription',
  1049. 'value': 'notify',
  1050. },
  1051. ]
  1052. )
  1053. self.assertEqual(response.status_code, 200)
  1054. reponse_json = response.json()
  1055. self.assertFalse(reponse_json['subscription'])
  1056. thread_json = self.get_thread_json()
  1057. self.assertFalse(thread_json['subscription'])
  1058. subscription = self.user.subscription_set.get(thread=self.thread)
  1059. self.assertFalse(subscription.send_email)
  1060. def test_subscribe_thread_with_email(self):
  1061. """api makes it possible to subscribe thread with emails"""
  1062. response = self.patch(
  1063. self.api_link, [
  1064. {
  1065. 'op': 'replace',
  1066. 'path': 'subscription',
  1067. 'value': 'email',
  1068. },
  1069. ]
  1070. )
  1071. self.assertEqual(response.status_code, 200)
  1072. reponse_json = response.json()
  1073. self.assertTrue(reponse_json['subscription'])
  1074. thread_json = self.get_thread_json()
  1075. self.assertTrue(thread_json['subscription'])
  1076. subscription = self.user.subscription_set.get(thread=self.thread)
  1077. self.assertTrue(subscription.send_email)
  1078. def test_unsubscribe_thread(self):
  1079. """api makes it possible to unsubscribe thread"""
  1080. response = self.patch(
  1081. self.api_link, [
  1082. {
  1083. 'op': 'replace',
  1084. 'path': 'subscription',
  1085. 'value': 'remove',
  1086. },
  1087. ]
  1088. )
  1089. self.assertEqual(response.status_code, 200)
  1090. reponse_json = response.json()
  1091. self.assertIsNone(reponse_json['subscription'])
  1092. thread_json = self.get_thread_json()
  1093. self.assertIsNone(thread_json['subscription'])
  1094. self.assertEqual(self.user.subscription_set.count(), 0)
  1095. def test_subscribe_as_guest(self):
  1096. """api makes it impossible to subscribe thread"""
  1097. self.logout_user()
  1098. response = self.patch(
  1099. self.api_link, [
  1100. {
  1101. 'op': 'replace',
  1102. 'path': 'subscription',
  1103. 'value': 'email',
  1104. },
  1105. ]
  1106. )
  1107. self.assertEqual(response.status_code, 403)
  1108. def test_subscribe_nonexistant_thread(self):
  1109. """api makes it impossible to subscribe nonexistant thread"""
  1110. bad_api_link = self.api_link.replace(
  1111. six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9)
  1112. )
  1113. response = self.patch(
  1114. bad_api_link, [
  1115. {
  1116. 'op': 'replace',
  1117. 'path': 'subscription',
  1118. 'value': 'email',
  1119. },
  1120. ]
  1121. )
  1122. self.assertEqual(response.status_code, 404)