test_thread_patch_api.py 39 KB

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