test_thread_patch_api.py 23 KB

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