test_thread_patch_api.py 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  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.threads.models import Thread
  7. from .test_threads_api import ThreadsApiTestCase
  8. class ThreadPatchApiTestCase(ThreadsApiTestCase):
  9. def patch(self, api_link, ops):
  10. return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
  11. class ThreadAddAclApiTests(ThreadPatchApiTestCase):
  12. def test_add_acl_true(self):
  13. """api adds current thread's acl to response"""
  14. response = self.patch(self.api_link, [
  15. {
  16. 'op': 'add',
  17. 'path': 'acl',
  18. 'value': True,
  19. },
  20. ])
  21. self.assertEqual(response.status_code, 200)
  22. response_json = response.json()
  23. self.assertTrue(response_json['acl'])
  24. def test_add_acl_false(self):
  25. """if value is false, api won't add acl to the response, but will set empty key"""
  26. response = self.patch(self.api_link, [
  27. {
  28. 'op': 'add',
  29. 'path': 'acl',
  30. 'value': False,
  31. },
  32. ])
  33. self.assertEqual(response.status_code, 200)
  34. response_json = response.json()
  35. self.assertIsNone(response_json['acl'])
  36. class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
  37. def test_change_thread_title(self):
  38. """api makes it possible to change thread title"""
  39. self.override_acl({'can_edit_threads': 2})
  40. response = self.patch(
  41. self.api_link, [
  42. {
  43. 'op': 'replace',
  44. 'path': 'title',
  45. 'value': "Lorem ipsum change!",
  46. },
  47. ]
  48. )
  49. self.assertEqual(response.status_code, 200)
  50. response_json = response.json()
  51. self.assertEqual(response_json['title'], "Lorem ipsum change!")
  52. thread_json = self.get_thread_json()
  53. self.assertEqual(thread_json['title'], "Lorem ipsum change!")
  54. def test_change_thread_title_no_permission(self):
  55. """api validates permission to change title"""
  56. self.override_acl({'can_edit_threads': 0})
  57. response = self.patch(
  58. self.api_link, [
  59. {
  60. 'op': 'replace',
  61. 'path': 'title',
  62. 'value': "Lorem ipsum change!",
  63. },
  64. ]
  65. )
  66. self.assertEqual(response.status_code, 400)
  67. response_json = response.json()
  68. self.assertEqual(response_json['detail'][0], "You can't edit threads in this category.")
  69. def test_change_thread_title_after_edit_time(self):
  70. """api cleans, validates and rejects too short title"""
  71. self.override_acl({'thread_edit_time': 1, 'can_edit_threads': 1})
  72. self.thread.starter = self.user
  73. self.thread.started_on = timezone.now() - timedelta(minutes=10)
  74. self.thread.save()
  75. response = self.patch(
  76. self.api_link, [
  77. {
  78. 'op': 'replace',
  79. 'path': 'title',
  80. 'value': "Lorem ipsum change!",
  81. },
  82. ]
  83. )
  84. self.assertEqual(response.status_code, 400)
  85. response_json = response.json()
  86. self.assertEqual(
  87. response_json['detail'][0], "You can't edit threads that are older than 1 minute."
  88. )
  89. def test_change_thread_title_invalid(self):
  90. """api cleans, validates and rejects too short title"""
  91. self.override_acl({'can_edit_threads': 2})
  92. response = self.patch(
  93. self.api_link, [
  94. {
  95. 'op': 'replace',
  96. 'path': 'title',
  97. 'value': 12,
  98. },
  99. ]
  100. )
  101. self.assertEqual(response.status_code, 400)
  102. response_json = response.json()
  103. self.assertEqual(
  104. response_json['detail'][0],
  105. "Thread title should be at least 5 characters long (it has 2)."
  106. )
  107. class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
  108. def test_pin_thread(self):
  109. """api makes it possible to pin globally thread"""
  110. self.override_acl({'can_pin_threads': 2})
  111. response = self.patch(
  112. self.api_link, [
  113. {
  114. 'op': 'replace',
  115. 'path': 'weight',
  116. 'value': 2,
  117. },
  118. ]
  119. )
  120. self.assertEqual(response.status_code, 200)
  121. response_json = response.json()
  122. self.assertEqual(response_json['weight'], 2)
  123. thread_json = self.get_thread_json()
  124. self.assertEqual(thread_json['weight'], 2)
  125. def test_unpin_thread(self):
  126. """api makes it possible to unpin thread"""
  127. self.thread.weight = 2
  128. self.thread.save()
  129. thread_json = self.get_thread_json()
  130. self.assertEqual(thread_json['weight'], 2)
  131. self.override_acl({'can_pin_threads': 2})
  132. response = self.patch(
  133. self.api_link, [
  134. {
  135. 'op': 'replace',
  136. 'path': 'weight',
  137. 'value': 0,
  138. },
  139. ]
  140. )
  141. self.assertEqual(response.status_code, 200)
  142. response_json = response.json()
  143. self.assertEqual(response_json['weight'], 0)
  144. thread_json = self.get_thread_json()
  145. self.assertEqual(thread_json['weight'], 0)
  146. def test_pin_thread_no_permission(self):
  147. """api pin thread globally with no permission fails"""
  148. self.override_acl({'can_pin_threads': 1})
  149. response = self.patch(
  150. self.api_link, [
  151. {
  152. 'op': 'replace',
  153. 'path': 'weight',
  154. 'value': 2,
  155. },
  156. ]
  157. )
  158. self.assertEqual(response.status_code, 400)
  159. response_json = response.json()
  160. self.assertEqual(
  161. response_json['detail'][0], "You don't have permission to pin this thread globally."
  162. )
  163. thread_json = self.get_thread_json()
  164. self.assertEqual(thread_json['weight'], 0)
  165. def test_unpin_thread_no_permission(self):
  166. """api unpin thread with no permission fails"""
  167. self.thread.weight = 2
  168. self.thread.save()
  169. thread_json = self.get_thread_json()
  170. self.assertEqual(thread_json['weight'], 2)
  171. self.override_acl({'can_pin_threads': 1})
  172. response = self.patch(
  173. self.api_link, [
  174. {
  175. 'op': 'replace',
  176. 'path': 'weight',
  177. 'value': 1,
  178. },
  179. ]
  180. )
  181. self.assertEqual(response.status_code, 400)
  182. response_json = response.json()
  183. self.assertEqual(
  184. response_json['detail'][0], "You don't have permission to change this thread's weight."
  185. )
  186. thread_json = self.get_thread_json()
  187. self.assertEqual(thread_json['weight'], 2)
  188. class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
  189. def test_pin_thread(self):
  190. """api makes it possible to pin locally thread"""
  191. self.override_acl({'can_pin_threads': 1})
  192. response = self.patch(
  193. self.api_link, [
  194. {
  195. 'op': 'replace',
  196. 'path': 'weight',
  197. 'value': 1,
  198. },
  199. ]
  200. )
  201. self.assertEqual(response.status_code, 200)
  202. response_json = response.json()
  203. self.assertEqual(response_json['weight'], 1)
  204. thread_json = self.get_thread_json()
  205. self.assertEqual(thread_json['weight'], 1)
  206. def test_unpin_thread(self):
  207. """api makes it possible to unpin thread"""
  208. self.thread.weight = 1
  209. self.thread.save()
  210. thread_json = self.get_thread_json()
  211. self.assertEqual(thread_json['weight'], 1)
  212. self.override_acl({'can_pin_threads': 1})
  213. response = self.patch(
  214. self.api_link, [
  215. {
  216. 'op': 'replace',
  217. 'path': 'weight',
  218. 'value': 0,
  219. },
  220. ]
  221. )
  222. self.assertEqual(response.status_code, 200)
  223. response_json = response.json()
  224. self.assertEqual(response_json['weight'], 0)
  225. thread_json = self.get_thread_json()
  226. self.assertEqual(thread_json['weight'], 0)
  227. def test_pin_thread_no_permission(self):
  228. """api pin thread locally with no permission fails"""
  229. self.override_acl({'can_pin_threads': 0})
  230. response = self.patch(
  231. self.api_link, [
  232. {
  233. 'op': 'replace',
  234. 'path': 'weight',
  235. 'value': 1,
  236. },
  237. ]
  238. )
  239. self.assertEqual(response.status_code, 400)
  240. response_json = response.json()
  241. self.assertEqual(
  242. response_json['detail'][0], "You don't have permission to change this thread's weight."
  243. )
  244. thread_json = self.get_thread_json()
  245. self.assertEqual(thread_json['weight'], 0)
  246. def test_unpin_thread_no_permission(self):
  247. """api unpin thread with no permission fails"""
  248. self.thread.weight = 1
  249. self.thread.save()
  250. thread_json = self.get_thread_json()
  251. self.assertEqual(thread_json['weight'], 1)
  252. self.override_acl({'can_pin_threads': 0})
  253. response = self.patch(
  254. self.api_link, [
  255. {
  256. 'op': 'replace',
  257. 'path': 'weight',
  258. 'value': 0,
  259. },
  260. ]
  261. )
  262. self.assertEqual(response.status_code, 400)
  263. response_json = response.json()
  264. self.assertEqual(
  265. response_json['detail'][0], "You don't have permission to change this thread's weight."
  266. )
  267. thread_json = self.get_thread_json()
  268. self.assertEqual(thread_json['weight'], 1)
  269. class ThreadMoveApiTests(ThreadPatchApiTestCase):
  270. def setUp(self):
  271. super(ThreadMoveApiTests, self).setUp()
  272. Category(
  273. name='Category B',
  274. slug='category-b',
  275. ).insert_at(
  276. self.category,
  277. position='last-child',
  278. save=True,
  279. )
  280. self.category_b = Category.objects.get(slug='category-b')
  281. def override_other_acl(self, acl):
  282. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  283. other_category_acl.update({
  284. 'can_see': 1,
  285. 'can_browse': 1,
  286. 'can_see_all_threads': 1,
  287. 'can_see_own_threads': 0,
  288. 'can_hide_threads': 0,
  289. 'can_approve_content': 0,
  290. })
  291. other_category_acl.update(acl)
  292. categories_acl = self.user.acl_cache['categories']
  293. categories_acl[self.category_b.pk] = other_category_acl
  294. visible_categories = [self.category.pk]
  295. if other_category_acl['can_see']:
  296. visible_categories.append(self.category_b.pk)
  297. override_acl(
  298. self.user, {
  299. 'visible_categories': visible_categories,
  300. 'categories': categories_acl,
  301. }
  302. )
  303. def test_move_thread_no_top(self):
  304. """api moves thread to other category, sets no top category"""
  305. self.override_acl({'can_move_threads': True})
  306. self.override_other_acl({'can_start_threads': 2})
  307. response = self.patch(
  308. self.api_link, [
  309. {
  310. 'op': 'replace',
  311. 'path': 'category',
  312. 'value': self.category_b.pk,
  313. },
  314. {
  315. 'op': 'add',
  316. 'path': 'top-category',
  317. 'value': self.category_b.pk,
  318. },
  319. {
  320. 'op': 'replace',
  321. 'path': 'flatten-categories',
  322. 'value': None,
  323. },
  324. ]
  325. )
  326. self.assertEqual(response.status_code, 200)
  327. reponse_json = response.json()
  328. self.assertEqual(reponse_json['category'], self.category_b.pk)
  329. self.override_other_acl({})
  330. thread_json = self.get_thread_json()
  331. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  332. def test_move_thread_with_top(self):
  333. """api moves thread to other category, sets top"""
  334. self.override_acl({'can_move_threads': True})
  335. self.override_other_acl({'can_start_threads': 2})
  336. response = self.patch(
  337. self.api_link, [
  338. {
  339. 'op': 'replace',
  340. 'path': 'category',
  341. 'value': self.category_b.pk,
  342. },
  343. {
  344. 'op': 'add',
  345. 'path': 'top-category',
  346. 'value': Category.objects.root_category().pk,
  347. },
  348. {
  349. 'op': 'replace',
  350. 'path': 'flatten-categories',
  351. 'value': None,
  352. },
  353. ]
  354. )
  355. self.assertEqual(response.status_code, 200)
  356. reponse_json = response.json()
  357. self.assertEqual(reponse_json['category'], self.category_b.pk)
  358. self.override_other_acl({})
  359. thread_json = self.get_thread_json()
  360. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  361. def test_move_thread_no_permission(self):
  362. """api move thread to other category with no permission fails"""
  363. self.override_acl({'can_move_threads': False})
  364. self.override_other_acl({})
  365. response = self.patch(
  366. self.api_link, [
  367. {
  368. 'op': 'replace',
  369. 'path': 'category',
  370. 'value': self.category_b.pk,
  371. },
  372. ]
  373. )
  374. self.assertEqual(response.status_code, 400)
  375. response_json = response.json()
  376. self.assertEqual(
  377. response_json['detail'][0], "You don't have permission to move this thread."
  378. )
  379. self.override_other_acl({})
  380. thread_json = self.get_thread_json()
  381. self.assertEqual(thread_json['category']['id'], self.category.pk)
  382. def test_move_thread_no_category_access(self):
  383. """api move thread to category with no access fails"""
  384. self.override_acl({'can_move_threads': True})
  385. self.override_other_acl({'can_see': False})
  386. response = self.patch(
  387. self.api_link, [
  388. {
  389. 'op': 'replace',
  390. 'path': 'category',
  391. 'value': self.category_b.pk,
  392. },
  393. ]
  394. )
  395. self.assertEqual(response.status_code, 400)
  396. response_json = response.json()
  397. self.assertEqual(response_json['detail'][0], 'NOT FOUND')
  398. self.override_other_acl({})
  399. thread_json = self.get_thread_json()
  400. self.assertEqual(thread_json['category']['id'], self.category.pk)
  401. def test_move_thread_no_category_browse(self):
  402. """api move thread to category with no browsing access fails"""
  403. self.override_acl({'can_move_threads': True})
  404. self.override_other_acl({'can_browse': False})
  405. response = self.patch(
  406. self.api_link, [
  407. {
  408. 'op': 'replace',
  409. 'path': 'category',
  410. 'value': self.category_b.pk,
  411. },
  412. ]
  413. )
  414. self.assertEqual(response.status_code, 400)
  415. response_json = response.json()
  416. self.assertEqual(
  417. response_json['detail'][0],
  418. 'You don\'t have permission to browse "Category B" contents.'
  419. )
  420. self.override_other_acl({})
  421. thread_json = self.get_thread_json()
  422. self.assertEqual(thread_json['category']['id'], self.category.pk)
  423. def test_move_thread_same_category(self):
  424. """api move thread to category it's already in fails"""
  425. self.override_acl({'can_move_threads': True})
  426. self.override_other_acl({'can_start_threads': 2})
  427. response = self.patch(
  428. self.api_link, [
  429. {
  430. 'op': 'replace',
  431. 'path': 'category',
  432. 'value': self.thread.category_id,
  433. },
  434. ]
  435. )
  436. self.assertEqual(response.status_code, 400)
  437. response_json = response.json()
  438. self.assertEqual(
  439. response_json['detail'][0], "You can't move thread to the category it's already in."
  440. )
  441. self.override_other_acl({})
  442. thread_json = self.get_thread_json()
  443. self.assertEqual(thread_json['category']['id'], self.category.pk)
  444. def test_thread_flatten_categories(self):
  445. """api flatten thread categories"""
  446. response = self.patch(
  447. self.api_link, [
  448. {
  449. 'op': 'replace',
  450. 'path': 'flatten-categories',
  451. 'value': None,
  452. },
  453. ]
  454. )
  455. self.assertEqual(response.status_code, 200)
  456. response_json = response.json()
  457. self.assertEqual(response_json['category'], self.category.pk)
  458. class ThreadCloseApiTests(ThreadPatchApiTestCase):
  459. def test_close_thread(self):
  460. """api makes it possible to close thread"""
  461. self.override_acl({'can_close_threads': True})
  462. response = self.patch(
  463. self.api_link, [
  464. {
  465. 'op': 'replace',
  466. 'path': 'is-closed',
  467. 'value': True,
  468. },
  469. ]
  470. )
  471. self.assertEqual(response.status_code, 200)
  472. response_json = response.json()
  473. self.assertTrue(response_json['is_closed'])
  474. thread_json = self.get_thread_json()
  475. self.assertTrue(thread_json['is_closed'])
  476. def test_open_thread(self):
  477. """api makes it possible to open thread"""
  478. self.thread.is_closed = True
  479. self.thread.save()
  480. thread_json = self.get_thread_json()
  481. self.assertTrue(thread_json['is_closed'])
  482. self.override_acl({'can_close_threads': True})
  483. response = self.patch(
  484. self.api_link, [
  485. {
  486. 'op': 'replace',
  487. 'path': 'is-closed',
  488. 'value': False,
  489. },
  490. ]
  491. )
  492. self.assertEqual(response.status_code, 200)
  493. response_json = response.json()
  494. self.assertFalse(response_json['is_closed'])
  495. thread_json = self.get_thread_json()
  496. self.assertFalse(thread_json['is_closed'])
  497. def test_close_thread_no_permission(self):
  498. """api close thread with no permission fails"""
  499. self.override_acl({'can_close_threads': False})
  500. response = self.patch(
  501. self.api_link, [
  502. {
  503. 'op': 'replace',
  504. 'path': 'is-closed',
  505. 'value': True,
  506. },
  507. ]
  508. )
  509. self.assertEqual(response.status_code, 400)
  510. response_json = response.json()
  511. self.assertEqual(
  512. response_json['detail'][0], "You don't have permission to close this thread."
  513. )
  514. thread_json = self.get_thread_json()
  515. self.assertFalse(thread_json['is_closed'])
  516. def test_open_thread_no_permission(self):
  517. """api open thread with no permission fails"""
  518. self.thread.is_closed = True
  519. self.thread.save()
  520. thread_json = self.get_thread_json()
  521. self.assertTrue(thread_json['is_closed'])
  522. self.override_acl({'can_close_threads': False})
  523. response = self.patch(
  524. self.api_link, [
  525. {
  526. 'op': 'replace',
  527. 'path': 'is-closed',
  528. 'value': False,
  529. },
  530. ]
  531. )
  532. self.assertEqual(response.status_code, 400)
  533. response_json = response.json()
  534. self.assertEqual(
  535. response_json['detail'][0], "You don't have permission to open this thread."
  536. )
  537. thread_json = self.get_thread_json()
  538. self.assertTrue(thread_json['is_closed'])
  539. class ThreadApproveApiTests(ThreadPatchApiTestCase):
  540. def test_approve_thread(self):
  541. """api makes it possible to approve thread"""
  542. self.thread.first_post.is_unapproved = True
  543. self.thread.first_post.save()
  544. self.thread.synchronize()
  545. self.thread.save()
  546. self.assertTrue(self.thread.is_unapproved)
  547. self.assertTrue(self.thread.has_unapproved_posts)
  548. self.override_acl({'can_approve_content': 1})
  549. response = self.patch(
  550. self.api_link, [
  551. {
  552. 'op': 'replace',
  553. 'path': 'is-unapproved',
  554. 'value': False,
  555. },
  556. ]
  557. )
  558. self.assertEqual(response.status_code, 200)
  559. response_json = response.json()
  560. self.assertFalse(response_json['is_unapproved'])
  561. self.assertFalse(response_json['has_unapproved_posts'])
  562. thread_json = self.get_thread_json()
  563. self.assertFalse(thread_json['is_unapproved'])
  564. self.assertFalse(thread_json['has_unapproved_posts'])
  565. thread = Thread.objects.get(pk=self.thread.pk)
  566. self.assertFalse(thread.is_unapproved)
  567. self.assertFalse(thread.has_unapproved_posts)
  568. def test_unapprove_thread(self):
  569. """api returns permission error on approval removal"""
  570. self.override_acl({'can_approve_content': 1})
  571. response = self.patch(
  572. self.api_link, [
  573. {
  574. 'op': 'replace',
  575. 'path': 'is-unapproved',
  576. 'value': True,
  577. },
  578. ]
  579. )
  580. self.assertEqual(response.status_code, 400)
  581. response_json = response.json()
  582. self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
  583. class ThreadHideApiTests(ThreadPatchApiTestCase):
  584. def test_hide_thread(self):
  585. """api makes it possible to hide thread"""
  586. self.override_acl({'can_hide_threads': 1})
  587. response = self.patch(
  588. self.api_link, [
  589. {
  590. 'op': 'replace',
  591. 'path': 'is-hidden',
  592. 'value': True,
  593. },
  594. ]
  595. )
  596. self.assertEqual(response.status_code, 200)
  597. reponse_json = response.json()
  598. self.assertTrue(reponse_json['is_hidden'])
  599. self.override_acl({'can_hide_threads': 1})
  600. thread_json = self.get_thread_json()
  601. self.assertTrue(thread_json['is_hidden'])
  602. def test_hide_thread_no_permission(self):
  603. """api hide thread with no permission fails"""
  604. self.override_acl({'can_hide_threads': 0})
  605. response = self.patch(
  606. self.api_link, [
  607. {
  608. 'op': 'replace',
  609. 'path': 'is-hidden',
  610. 'value': True,
  611. },
  612. ]
  613. )
  614. self.assertEqual(response.status_code, 400)
  615. response_json = response.json()
  616. self.assertEqual(
  617. response_json['detail'][0], "You can't hide threads in this category."
  618. )
  619. thread_json = self.get_thread_json()
  620. self.assertFalse(thread_json['is_hidden'])
  621. def test_hide_non_owned_thread(self):
  622. """api forbids non-moderator from hiding other users threads"""
  623. self.override_acl({
  624. 'can_hide_own_threads': 1,
  625. 'can_hide_threads': 0
  626. })
  627. response = self.patch(
  628. self.api_link, [
  629. {
  630. 'op': 'replace',
  631. 'path': 'is-hidden',
  632. 'value': True,
  633. },
  634. ]
  635. )
  636. self.assertEqual(response.status_code, 400)
  637. response_json = response.json()
  638. self.assertEqual(
  639. response_json['detail'][0], "You can't hide other users theads in this category."
  640. )
  641. def test_hide_owned_thread_no_time(self):
  642. """api forbids non-moderator from hiding other users threads"""
  643. self.override_acl({
  644. 'can_hide_own_threads': 1,
  645. 'can_hide_threads': 0,
  646. 'thread_edit_time': 1,
  647. })
  648. self.thread.starter = self.user
  649. self.thread.started_on = timezone.now() - timedelta(minutes=5)
  650. self.thread.save()
  651. response = self.patch(
  652. self.api_link, [
  653. {
  654. 'op': 'replace',
  655. 'path': 'is-hidden',
  656. 'value': True,
  657. },
  658. ]
  659. )
  660. self.assertEqual(response.status_code, 400)
  661. response_json = response.json()
  662. self.assertEqual(
  663. response_json['detail'][0], "You can't hide threads that are older than 1 minute."
  664. )
  665. def test_hide_closed_category_no_permission(self):
  666. """api test permission to hide thread in closed category"""
  667. self.override_acl({
  668. 'can_hide_threads': 1,
  669. 'can_close_threads': 0
  670. })
  671. self.category.is_closed = True
  672. self.category.save()
  673. response = self.patch(
  674. self.api_link, [
  675. {
  676. 'op': 'replace',
  677. 'path': 'is-hidden',
  678. 'value': True,
  679. },
  680. ]
  681. )
  682. self.assertEqual(response.status_code, 400)
  683. response_json = response.json()
  684. self.assertEqual(
  685. response_json['detail'][0], "This category is closed. You can't hide threads in it."
  686. )
  687. def test_hide_closed_thread_no_permission(self):
  688. """api test permission to hide closed thread"""
  689. self.override_acl({
  690. 'can_hide_threads': 1,
  691. 'can_close_threads': 0
  692. })
  693. self.thread.is_closed = True
  694. self.thread.save()
  695. response = self.patch(
  696. self.api_link, [
  697. {
  698. 'op': 'replace',
  699. 'path': 'is-hidden',
  700. 'value': True,
  701. },
  702. ]
  703. )
  704. self.assertEqual(response.status_code, 400)
  705. response_json = response.json()
  706. self.assertEqual(
  707. response_json['detail'][0], "This thread is closed. You can't hide it."
  708. )
  709. class ThreadUnhideApiTests(ThreadPatchApiTestCase):
  710. def setUp(self):
  711. super(ThreadUnhideApiTests, self).setUp()
  712. self.thread.is_hidden = True
  713. self.thread.save()
  714. def test_unhide_thread(self):
  715. """api makes it possible to unhide thread"""
  716. self.override_acl({'can_hide_threads': 1})
  717. response = self.patch(
  718. self.api_link, [
  719. {
  720. 'op': 'replace',
  721. 'path': 'is-hidden',
  722. 'value': False,
  723. },
  724. ]
  725. )
  726. self.assertEqual(response.status_code, 200)
  727. reponse_json = response.json()
  728. self.assertFalse(reponse_json['is_hidden'])
  729. self.override_acl({'can_hide_threads': 1})
  730. thread_json = self.get_thread_json()
  731. self.assertFalse(thread_json['is_hidden'])
  732. def test_unhide_thread_no_permission(self):
  733. """api unhide thread with no permission fails as thread is invisible"""
  734. self.override_acl({'can_hide_threads': 0})
  735. response = self.patch(
  736. self.api_link, [
  737. {
  738. 'op': 'replace',
  739. 'path': 'is-hidden',
  740. 'value': True,
  741. },
  742. ]
  743. )
  744. self.assertEqual(response.status_code, 404)
  745. def test_unhide_closed_category_no_permission(self):
  746. """api test permission to unhide thread in closed category"""
  747. self.override_acl({
  748. 'can_hide_threads': 1,
  749. 'can_close_threads': 0
  750. })
  751. self.category.is_closed = True
  752. self.category.save()
  753. response = self.patch(
  754. self.api_link, [
  755. {
  756. 'op': 'replace',
  757. 'path': 'is-hidden',
  758. 'value': False,
  759. },
  760. ]
  761. )
  762. self.assertEqual(response.status_code, 400)
  763. response_json = response.json()
  764. self.assertEqual(
  765. response_json['detail'][0], "This category is closed. You can't reveal threads in it."
  766. )
  767. def test_unhide_closed_thread_no_permission(self):
  768. """api test permission to unhide closed thread"""
  769. self.override_acl({
  770. 'can_hide_threads': 1,
  771. 'can_close_threads': 0
  772. })
  773. self.thread.is_closed = True
  774. self.thread.save()
  775. response = self.patch(
  776. self.api_link, [
  777. {
  778. 'op': 'replace',
  779. 'path': 'is-hidden',
  780. 'value': False,
  781. },
  782. ]
  783. )
  784. self.assertEqual(response.status_code, 400)
  785. response_json = response.json()
  786. self.assertEqual(
  787. response_json['detail'][0], "This thread is closed. You can't reveal it."
  788. )
  789. class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
  790. def test_subscribe_thread(self):
  791. """api makes it possible to subscribe thread"""
  792. response = self.patch(
  793. self.api_link, [
  794. {
  795. 'op': 'replace',
  796. 'path': 'subscription',
  797. 'value': 'notify',
  798. },
  799. ]
  800. )
  801. self.assertEqual(response.status_code, 200)
  802. reponse_json = response.json()
  803. self.assertFalse(reponse_json['subscription'])
  804. thread_json = self.get_thread_json()
  805. self.assertFalse(thread_json['subscription'])
  806. subscription = self.user.subscription_set.get(thread=self.thread)
  807. self.assertFalse(subscription.send_email)
  808. def test_subscribe_thread_with_email(self):
  809. """api makes it possible to subscribe thread with emails"""
  810. response = self.patch(
  811. self.api_link, [
  812. {
  813. 'op': 'replace',
  814. 'path': 'subscription',
  815. 'value': 'email',
  816. },
  817. ]
  818. )
  819. self.assertEqual(response.status_code, 200)
  820. reponse_json = response.json()
  821. self.assertTrue(reponse_json['subscription'])
  822. thread_json = self.get_thread_json()
  823. self.assertTrue(thread_json['subscription'])
  824. subscription = self.user.subscription_set.get(thread=self.thread)
  825. self.assertTrue(subscription.send_email)
  826. def test_unsubscribe_thread(self):
  827. """api makes it possible to unsubscribe thread"""
  828. response = self.patch(
  829. self.api_link, [
  830. {
  831. 'op': 'replace',
  832. 'path': 'subscription',
  833. 'value': 'remove',
  834. },
  835. ]
  836. )
  837. self.assertEqual(response.status_code, 200)
  838. reponse_json = response.json()
  839. self.assertIsNone(reponse_json['subscription'])
  840. thread_json = self.get_thread_json()
  841. self.assertIsNone(thread_json['subscription'])
  842. self.assertEqual(self.user.subscription_set.count(), 0)
  843. def test_subscribe_as_guest(self):
  844. """api makes it impossible to subscribe thread"""
  845. self.logout_user()
  846. response = self.patch(
  847. self.api_link, [
  848. {
  849. 'op': 'replace',
  850. 'path': 'subscription',
  851. 'value': 'email',
  852. },
  853. ]
  854. )
  855. self.assertEqual(response.status_code, 403)
  856. def test_subscribe_nonexistant_thread(self):
  857. """api makes it impossible to subscribe nonexistant thread"""
  858. bad_api_link = self.api_link.replace(
  859. six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9)
  860. )
  861. response = self.patch(
  862. bad_api_link, [
  863. {
  864. 'op': 'replace',
  865. 'path': 'subscription',
  866. 'value': 'email',
  867. },
  868. ]
  869. )
  870. self.assertEqual(response.status_code, 404)