test_thread_patch_api.py 23 KB

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