test_thread_patch_api.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. import json
  2. from django.utils import six
  3. from django.utils.encoding import smart_str
  4. from misago.acl.testutils import override_acl
  5. from misago.categories.models import Category
  6. from .test_threads_api import ThreadsApiTestCase
  7. class ThreadPinGloballyApiTests(ThreadsApiTestCase):
  8. def test_pin_thread(self):
  9. """api makes it possible to pin globally thread"""
  10. self.override_acl({
  11. 'can_pin_threads': 2
  12. })
  13. response = self.client.patch(self.api_link, json.dumps([
  14. {'op': 'replace', 'path': 'weight', 'value': 2}
  15. ]),
  16. content_type="application/json")
  17. self.assertEqual(response.status_code, 200)
  18. thread_json = self.get_thread_json()
  19. self.assertEqual(thread_json['weight'], 2)
  20. def test_unpin_thread(self):
  21. """api makes it possible to unpin thread"""
  22. self.thread.weight = 2
  23. self.thread.save()
  24. thread_json = self.get_thread_json()
  25. self.assertEqual(thread_json['weight'], 2)
  26. self.override_acl({
  27. 'can_pin_threads': 2
  28. })
  29. response = self.client.patch(self.api_link, json.dumps([
  30. {'op': 'replace', 'path': 'weight', 'value': 0}
  31. ]),
  32. content_type="application/json")
  33. self.assertEqual(response.status_code, 200)
  34. thread_json = self.get_thread_json()
  35. self.assertEqual(thread_json['weight'], 0)
  36. def test_pin_thread_no_permission(self):
  37. """api pin thread globally with no permission fails"""
  38. self.override_acl({
  39. 'can_pin_threads': 1
  40. })
  41. response = self.client.patch(self.api_link, json.dumps([
  42. {'op': 'replace', 'path': 'weight', 'value': 2}
  43. ]),
  44. content_type="application/json")
  45. self.assertEqual(response.status_code, 400)
  46. response_json = json.loads(smart_str(response.content))
  47. self.assertEqual(response_json['detail'][0],
  48. "You don't have permission to pin this thread globally.")
  49. thread_json = self.get_thread_json()
  50. self.assertEqual(thread_json['weight'], 0)
  51. def test_unpin_thread_no_permission(self):
  52. """api unpin thread with no permission fails"""
  53. self.thread.weight = 2
  54. self.thread.save()
  55. thread_json = self.get_thread_json()
  56. self.assertEqual(thread_json['weight'], 2)
  57. self.override_acl({
  58. 'can_pin_threads': 1
  59. })
  60. response = self.client.patch(self.api_link, json.dumps([
  61. {'op': 'replace', 'path': 'weight', 'value': 1}
  62. ]),
  63. content_type="application/json")
  64. self.assertEqual(response.status_code, 400)
  65. response_json = json.loads(smart_str(response.content))
  66. self.assertEqual(response_json['detail'][0],
  67. "You don't have permission to change this thread's weight.")
  68. thread_json = self.get_thread_json()
  69. self.assertEqual(thread_json['weight'], 2)
  70. class ThreadPinLocallyApiTests(ThreadsApiTestCase):
  71. def test_pin_thread(self):
  72. """api makes it possible to pin locally thread"""
  73. self.override_acl({
  74. 'can_pin_threads': 1
  75. })
  76. response = self.client.patch(self.api_link, json.dumps([
  77. {'op': 'replace', 'path': 'weight', 'value': 1}
  78. ]),
  79. content_type="application/json")
  80. self.assertEqual(response.status_code, 200)
  81. thread_json = self.get_thread_json()
  82. self.assertEqual(thread_json['weight'], 1)
  83. def test_unpin_thread(self):
  84. """api makes it possible to unpin thread"""
  85. self.thread.weight = 1
  86. self.thread.save()
  87. thread_json = self.get_thread_json()
  88. self.assertEqual(thread_json['weight'], 1)
  89. self.override_acl({
  90. 'can_pin_threads': 1
  91. })
  92. response = self.client.patch(self.api_link, json.dumps([
  93. {'op': 'replace', 'path': 'weight', 'value': 0}
  94. ]),
  95. content_type="application/json")
  96. self.assertEqual(response.status_code, 200)
  97. thread_json = self.get_thread_json()
  98. self.assertEqual(thread_json['weight'], 0)
  99. def test_pin_thread_no_permission(self):
  100. """api pin thread locally with no permission fails"""
  101. self.override_acl({
  102. 'can_pin_threads': 0
  103. })
  104. response = self.client.patch(self.api_link, json.dumps([
  105. {'op': 'replace', 'path': 'weight', 'value': 1}
  106. ]),
  107. content_type="application/json")
  108. self.assertEqual(response.status_code, 400)
  109. response_json = json.loads(smart_str(response.content))
  110. self.assertEqual(response_json['detail'][0],
  111. "You don't have permission to change this thread's weight.")
  112. thread_json = self.get_thread_json()
  113. self.assertEqual(thread_json['weight'], 0)
  114. def test_unpin_thread_no_permission(self):
  115. """api unpin thread with no permission fails"""
  116. self.thread.weight = 1
  117. self.thread.save()
  118. thread_json = self.get_thread_json()
  119. self.assertEqual(thread_json['weight'], 1)
  120. self.override_acl({
  121. 'can_pin_threads': 0
  122. })
  123. response = self.client.patch(self.api_link, json.dumps([
  124. {'op': 'replace', 'path': 'weight', 'value': 0}
  125. ]),
  126. content_type="application/json")
  127. self.assertEqual(response.status_code, 400)
  128. response_json = json.loads(smart_str(response.content))
  129. self.assertEqual(response_json['detail'][0],
  130. "You don't have permission to change this thread's weight.")
  131. thread_json = self.get_thread_json()
  132. self.assertEqual(thread_json['weight'], 1)
  133. class ThreadMoveApiTests(ThreadsApiTestCase):
  134. def setUp(self):
  135. super(ThreadMoveApiTests, self).setUp()
  136. Category(
  137. name='Category B',
  138. slug='category-b',
  139. ).insert_at(self.category, position='last-child', save=True)
  140. self.category_b = Category.objects.get(slug='category-b')
  141. def override_other_acl(self, acl):
  142. final_acl = {
  143. 'can_see': 1,
  144. 'can_browse': 1,
  145. 'can_see_all_threads': 1,
  146. 'can_see_own_threads': 0,
  147. 'can_hide_threads': 0,
  148. 'can_approve_content': 0,
  149. }
  150. final_acl.update(acl)
  151. categories_acl = self.user.acl['categories']
  152. categories_acl[self.category_b.pk] = final_acl
  153. visible_categories = [self.category.pk]
  154. if final_acl['can_see']:
  155. visible_categories.append(self.category_b.pk)
  156. override_acl(self.user, {
  157. 'visible_categories': visible_categories,
  158. 'categories': categories_acl,
  159. })
  160. def test_move_thread_no_top(self):
  161. """api moves thread to other category, sets no top category"""
  162. self.override_acl({
  163. 'can_move_threads': True
  164. })
  165. self.override_other_acl({
  166. 'can_start_threads': 2
  167. })
  168. response = self.client.patch(self.api_link, json.dumps([
  169. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
  170. {'op': 'add', 'path': 'top-category', 'value': self.category_b.pk},
  171. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  172. ]),
  173. content_type="application/json")
  174. self.assertEqual(response.status_code, 200)
  175. self.override_other_acl({})
  176. thread_json = self.get_thread_json()
  177. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  178. reponse_json = json.loads(smart_str(response.content))
  179. self.assertEqual(reponse_json['category'], self.category_b.pk)
  180. self.assertEqual(reponse_json['top_category'], None)
  181. def test_move_thread_with_top(self):
  182. """api moves thread to other category, sets top"""
  183. self.override_acl({
  184. 'can_move_threads': True
  185. })
  186. self.override_other_acl({
  187. 'can_start_threads': 2
  188. })
  189. response = self.client.patch(self.api_link, json.dumps([
  190. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
  191. {
  192. 'op': 'add',
  193. 'path': 'top-category',
  194. 'value': Category.objects.root_category().pk,
  195. },
  196. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  197. ]),
  198. content_type="application/json")
  199. self.assertEqual(response.status_code, 200)
  200. self.override_other_acl({})
  201. thread_json = self.get_thread_json()
  202. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  203. reponse_json = json.loads(smart_str(response.content))
  204. self.assertEqual(reponse_json['category'], self.category_b.pk)
  205. self.assertEqual(reponse_json['top_category'], self.category.pk)
  206. def test_move_thread_no_permission(self):
  207. """api move thread to other category with no permission fails"""
  208. self.override_acl({
  209. 'can_move_threads': False
  210. })
  211. self.override_other_acl({})
  212. response = self.client.patch(self.api_link, json.dumps([
  213. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  214. ]),
  215. content_type="application/json")
  216. self.assertEqual(response.status_code, 400)
  217. response_json = json.loads(smart_str(response.content))
  218. self.assertEqual(response_json['detail'][0],
  219. "You don't have permission to move this thread.")
  220. self.override_other_acl({})
  221. thread_json = self.get_thread_json()
  222. self.assertEqual(thread_json['category']['id'], self.category.pk)
  223. def test_move_thread_no_category_access(self):
  224. """api move thread to category with no access fails"""
  225. self.override_acl({
  226. 'can_move_threads': True
  227. })
  228. self.override_other_acl({
  229. 'can_see': False
  230. })
  231. response = self.client.patch(self.api_link, json.dumps([
  232. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  233. ]),
  234. content_type="application/json")
  235. self.assertEqual(response.status_code, 400)
  236. response_json = json.loads(smart_str(response.content))
  237. self.assertEqual(response_json['detail'][0], 'NOT FOUND')
  238. self.override_other_acl({})
  239. thread_json = self.get_thread_json()
  240. self.assertEqual(thread_json['category']['id'], self.category.pk)
  241. def test_move_thread_no_category_browse(self):
  242. """api move thread to category with no browsing access fails"""
  243. self.override_acl({
  244. 'can_move_threads': True
  245. })
  246. self.override_other_acl({
  247. 'can_browse': False
  248. })
  249. response = self.client.patch(self.api_link, json.dumps([
  250. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  251. ]),
  252. content_type="application/json")
  253. self.assertEqual(response.status_code, 400)
  254. response_json = json.loads(smart_str(response.content))
  255. self.assertEqual(response_json['detail'][0],
  256. 'You don\'t have permission to browse "Category B" contents.')
  257. self.override_other_acl({})
  258. thread_json = self.get_thread_json()
  259. self.assertEqual(thread_json['category']['id'], self.category.pk)
  260. def test_thread_flatten_categories(self):
  261. """api flatten thread categories"""
  262. response = self.client.patch(self.api_link, json.dumps([
  263. {'op': 'replace', 'path': 'flatten-categories', 'value': None}
  264. ]),
  265. content_type="application/json")
  266. self.assertEqual(response.status_code, 200)
  267. response_json = json.loads(smart_str(response.content))
  268. self.assertEqual(response_json['category'], self.category.pk)
  269. def test_thread_top_flatten_categories(self):
  270. """api flatten thread with top category"""
  271. self.thread.category = self.category_b
  272. self.thread.save()
  273. self.override_other_acl({})
  274. response = self.client.patch(self.api_link, json.dumps([
  275. {
  276. 'op': 'add',
  277. 'path': 'top-category',
  278. 'value': Category.objects.root_category().pk,
  279. },
  280. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  281. ]),
  282. content_type="application/json")
  283. self.assertEqual(response.status_code, 200)
  284. response_json = json.loads(smart_str(response.content))
  285. self.assertEqual(response_json['top_category'], self.category.pk)
  286. self.assertEqual(response_json['category'], self.category_b.pk)
  287. class ThreadCloseApiTests(ThreadsApiTestCase):
  288. def test_close_thread(self):
  289. """api makes it possible to close thread"""
  290. self.override_acl({
  291. 'can_close_threads': True
  292. })
  293. response = self.client.patch(self.api_link, json.dumps([
  294. {'op': 'replace', 'path': 'is-closed', 'value': True}
  295. ]),
  296. content_type="application/json")
  297. self.assertEqual(response.status_code, 200)
  298. thread_json = self.get_thread_json()
  299. self.assertTrue(thread_json['is_closed'])
  300. def test_open_thread(self):
  301. """api makes it possible to open thread"""
  302. self.thread.is_closed = True
  303. self.thread.save()
  304. thread_json = self.get_thread_json()
  305. self.assertTrue(thread_json['is_closed'])
  306. self.override_acl({
  307. 'can_close_threads': True
  308. })
  309. response = self.client.patch(self.api_link, json.dumps([
  310. {'op': 'replace', 'path': 'is-closed', 'value': False}
  311. ]),
  312. content_type="application/json")
  313. self.assertEqual(response.status_code, 200)
  314. thread_json = self.get_thread_json()
  315. self.assertFalse(thread_json['is_closed'])
  316. def test_close_thread_no_permission(self):
  317. """api close thread with no permission fails"""
  318. self.override_acl({
  319. 'can_close_threads': False
  320. })
  321. response = self.client.patch(self.api_link, json.dumps([
  322. {'op': 'replace', 'path': 'is-closed', 'value': True}
  323. ]),
  324. content_type="application/json")
  325. self.assertEqual(response.status_code, 400)
  326. response_json = json.loads(smart_str(response.content))
  327. self.assertEqual(response_json['detail'][0],
  328. "You don't have permission to close this thread.")
  329. thread_json = self.get_thread_json()
  330. self.assertFalse(thread_json['is_closed'])
  331. def test_open_thread_no_permission(self):
  332. """api open thread with no permission fails"""
  333. self.thread.is_closed = True
  334. self.thread.save()
  335. thread_json = self.get_thread_json()
  336. self.assertTrue(thread_json['is_closed'])
  337. self.override_acl({
  338. 'can_close_threads': False
  339. })
  340. response = self.client.patch(self.api_link, json.dumps([
  341. {'op': 'replace', 'path': 'is-closed', 'value': False}
  342. ]),
  343. content_type="application/json")
  344. self.assertEqual(response.status_code, 400)
  345. response_json = json.loads(smart_str(response.content))
  346. self.assertEqual(response_json['detail'][0],
  347. "You don't have permission to open this thread.")
  348. thread_json = self.get_thread_json()
  349. self.assertTrue(thread_json['is_closed'])
  350. class ThreadApproveApiTests(ThreadsApiTestCase):
  351. def test_approve_thread(self):
  352. """api makes it possible to approve thread"""
  353. self.thread.is_unapproved = True
  354. self.thread.save()
  355. self.override_acl({
  356. 'can_approve_content': 1
  357. })
  358. response = self.client.patch(self.api_link, json.dumps([
  359. {'op': 'replace', 'path': 'is-unapproved', 'value': False}
  360. ]),
  361. content_type="application/json")
  362. self.assertEqual(response.status_code, 200)
  363. thread_json = self.get_thread_json()
  364. self.assertFalse(thread_json['is_unapproved'])
  365. def test_unapprove_thread(self):
  366. """api returns permission error on approval removal"""
  367. self.override_acl({
  368. 'can_approve_content': 1
  369. })
  370. response = self.client.patch(self.api_link, json.dumps([
  371. {'op': 'replace', 'path': 'is-unapproved', 'value': True}
  372. ]),
  373. content_type="application/json")
  374. self.assertEqual(response.status_code, 400)
  375. response_json = json.loads(smart_str(response.content))
  376. self.assertEqual(response_json['detail'][0],
  377. "Content approval can't be reversed.")
  378. class ThreadHideApiTests(ThreadsApiTestCase):
  379. def test_hide_thread(self):
  380. """api makes it possible to hide thread"""
  381. self.override_acl({
  382. 'can_hide_threads': 1
  383. })
  384. response = self.client.patch(self.api_link, json.dumps([
  385. {'op': 'replace', 'path': 'is-hidden', 'value': True}
  386. ]),
  387. content_type="application/json")
  388. self.assertEqual(response.status_code, 200)
  389. self.override_acl({
  390. 'can_hide_threads': 1
  391. })
  392. thread_json = self.get_thread_json()
  393. self.assertTrue(thread_json['is_hidden'])
  394. def test_show_thread(self):
  395. """api makes it possible to unhide thread"""
  396. self.thread.is_hidden = True
  397. self.thread.save()
  398. self.override_acl({
  399. 'can_hide_threads': 1
  400. })
  401. thread_json = self.get_thread_json()
  402. self.assertTrue(thread_json['is_hidden'])
  403. self.override_acl({
  404. 'can_hide_threads': 1
  405. })
  406. response = self.client.patch(self.api_link, json.dumps([
  407. {'op': 'replace', 'path': 'is-hidden', 'value': False}
  408. ]),
  409. content_type="application/json")
  410. self.assertEqual(response.status_code, 200)
  411. self.override_acl({
  412. 'can_hide_threads': 1
  413. })
  414. thread_json = self.get_thread_json()
  415. self.assertFalse(thread_json['is_hidden'])
  416. def test_hide_thread_no_permission(self):
  417. """api hide thread with no permission fails"""
  418. self.override_acl({
  419. 'can_hide_threads': 0
  420. })
  421. response = self.client.patch(self.api_link, json.dumps([
  422. {'op': 'replace', 'path': 'is-hidden', 'value': True}
  423. ]),
  424. content_type="application/json")
  425. self.assertEqual(response.status_code, 400)
  426. response_json = json.loads(smart_str(response.content))
  427. self.assertEqual(response_json['detail'][0],
  428. "You don't have permission to hide this thread.")
  429. thread_json = self.get_thread_json()
  430. self.assertFalse(thread_json['is_hidden'])
  431. def test_show_thread_no_permission(self):
  432. """api unhide thread with no permission fails"""
  433. self.thread.is_hidden = True
  434. self.thread.save()
  435. self.override_acl({
  436. 'can_hide_threads': 1
  437. })
  438. thread_json = self.get_thread_json()
  439. self.assertTrue(thread_json['is_hidden'])
  440. self.override_acl({
  441. 'can_hide_threads': 0
  442. })
  443. response = self.client.patch(self.api_link, json.dumps([
  444. {'op': 'replace', 'path': 'is-hidden', 'value': False}
  445. ]),
  446. content_type="application/json")
  447. self.assertEqual(response.status_code, 404)
  448. class ThreadSubscribeApiTests(ThreadsApiTestCase):
  449. def test_subscribe_thread(self):
  450. """api makes it possible to subscribe thread"""
  451. response = self.client.patch(self.api_link, json.dumps([
  452. {'op': 'replace', 'path': 'subscription', 'value': 'notify'}
  453. ]),
  454. content_type="application/json")
  455. self.assertEqual(response.status_code, 200)
  456. thread_json = self.get_thread_json()
  457. self.assertFalse(thread_json['subscription'])
  458. subscription = self.user.subscription_set.get(thread=self.thread)
  459. self.assertFalse(subscription.send_email)
  460. def test_subscribe_thread_with_email(self):
  461. """api makes it possible to subscribe thread with emails"""
  462. response = self.client.patch(self.api_link, json.dumps([
  463. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  464. ]),
  465. content_type="application/json")
  466. self.assertEqual(response.status_code, 200)
  467. thread_json = self.get_thread_json()
  468. self.assertTrue(thread_json['subscription'])
  469. subscription = self.user.subscription_set.get(thread=self.thread)
  470. self.assertTrue(subscription.send_email)
  471. def test_unsubscribe_thread(self):
  472. """api makes it possible to unsubscribe thread"""
  473. response = self.client.patch(self.api_link, json.dumps([
  474. {'op': 'replace', 'path': 'subscription', 'value': 'remove'}
  475. ]),
  476. content_type="application/json")
  477. self.assertEqual(response.status_code, 200)
  478. thread_json = self.get_thread_json()
  479. self.assertIsNone(thread_json['subscription'])
  480. self.assertEqual(self.user.subscription_set.count(), 0)
  481. def test_subscribe_as_guest(self):
  482. """api makes it impossible to subscribe thread"""
  483. self.logout_user()
  484. response = self.client.patch(self.api_link, json.dumps([
  485. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  486. ]),
  487. content_type="application/json")
  488. self.assertEqual(response.status_code, 403)
  489. def test_subscribe_nonexistant_thread(self):
  490. """api makes it impossible to subscribe nonexistant thread"""
  491. bad_api_link = self.api_link.replace(
  492. six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9))
  493. response = self.client.patch(bad_api_link, json.dumps([
  494. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  495. ]),
  496. content_type="application/json")
  497. self.assertEqual(response.status_code, 404)