test_thread_patch_api.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  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. other_category_acl = self.user.acl['categories'][self.category.pk].copy()
  191. other_category_acl.update({
  192. 'can_see': 1,
  193. 'can_browse': 1,
  194. 'can_see_all_threads': 1,
  195. 'can_see_own_threads': 0,
  196. 'can_hide_threads': 0,
  197. 'can_approve_content': 0,
  198. })
  199. other_category_acl.update(acl)
  200. categories_acl = self.user.acl['categories']
  201. categories_acl[self.category_b.pk] = other_category_acl
  202. visible_categories = [self.category.pk]
  203. if other_category_acl['can_see']:
  204. visible_categories.append(self.category_b.pk)
  205. override_acl(self.user, {
  206. 'visible_categories': visible_categories,
  207. 'categories': categories_acl,
  208. })
  209. def test_move_thread_no_top(self):
  210. """api moves thread to other category, sets no top category"""
  211. self.override_acl({
  212. 'can_move_threads': True
  213. })
  214. self.override_other_acl({
  215. 'can_start_threads': 2
  216. })
  217. response = self.patch(self.api_link, [
  218. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
  219. {'op': 'add', 'path': 'top-category', 'value': self.category_b.pk},
  220. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  221. ])
  222. self.assertEqual(response.status_code, 200)
  223. self.override_other_acl({})
  224. thread_json = self.get_thread_json()
  225. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  226. reponse_json = json.loads(smart_str(response.content))
  227. self.assertEqual(reponse_json['category'], self.category_b.pk)
  228. self.assertEqual(reponse_json['top_category'], None)
  229. def test_move_thread_with_top(self):
  230. """api moves thread to other category, sets top"""
  231. self.override_acl({
  232. 'can_move_threads': True
  233. })
  234. self.override_other_acl({
  235. 'can_start_threads': 2
  236. })
  237. response = self.patch(self.api_link, [
  238. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
  239. {
  240. 'op': 'add',
  241. 'path': 'top-category',
  242. 'value': Category.objects.root_category().pk,
  243. },
  244. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  245. ])
  246. self.assertEqual(response.status_code, 200)
  247. self.override_other_acl({})
  248. thread_json = self.get_thread_json()
  249. self.assertEqual(thread_json['category']['id'], self.category_b.pk)
  250. reponse_json = json.loads(smart_str(response.content))
  251. self.assertEqual(reponse_json['category'], self.category_b.pk)
  252. self.assertEqual(reponse_json['top_category'], self.category.pk)
  253. def test_move_thread_no_permission(self):
  254. """api move thread to other category with no permission fails"""
  255. self.override_acl({
  256. 'can_move_threads': False
  257. })
  258. self.override_other_acl({})
  259. response = self.patch(self.api_link, [
  260. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  261. ])
  262. self.assertEqual(response.status_code, 400)
  263. response_json = json.loads(smart_str(response.content))
  264. self.assertEqual(response_json['detail'][0],
  265. "You don't have permission to move this thread.")
  266. self.override_other_acl({})
  267. thread_json = self.get_thread_json()
  268. self.assertEqual(thread_json['category']['id'], self.category.pk)
  269. def test_move_thread_no_category_access(self):
  270. """api move thread to category with no access fails"""
  271. self.override_acl({
  272. 'can_move_threads': True
  273. })
  274. self.override_other_acl({
  275. 'can_see': False
  276. })
  277. response = self.patch(self.api_link, [
  278. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  279. ])
  280. self.assertEqual(response.status_code, 400)
  281. response_json = json.loads(smart_str(response.content))
  282. self.assertEqual(response_json['detail'][0], 'NOT FOUND')
  283. self.override_other_acl({})
  284. thread_json = self.get_thread_json()
  285. self.assertEqual(thread_json['category']['id'], self.category.pk)
  286. def test_move_thread_no_category_browse(self):
  287. """api move thread to category with no browsing access fails"""
  288. self.override_acl({
  289. 'can_move_threads': True
  290. })
  291. self.override_other_acl({
  292. 'can_browse': False
  293. })
  294. response = self.patch(self.api_link, [
  295. {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
  296. ])
  297. self.assertEqual(response.status_code, 400)
  298. response_json = json.loads(smart_str(response.content))
  299. self.assertEqual(response_json['detail'][0],
  300. 'You don\'t have permission to browse "Category B" contents.')
  301. self.override_other_acl({})
  302. thread_json = self.get_thread_json()
  303. self.assertEqual(thread_json['category']['id'], self.category.pk)
  304. def test_move_thread_same_category(self):
  305. """api move thread to category it's already in fails"""
  306. self.override_acl({
  307. 'can_move_threads': True
  308. })
  309. self.override_other_acl({
  310. 'can_start_threads': 2
  311. })
  312. response = self.patch(self.api_link, [
  313. {'op': 'replace', 'path': 'category', 'value': self.thread.category_id}
  314. ])
  315. self.assertEqual(response.status_code, 400)
  316. response_json = json.loads(smart_str(response.content))
  317. self.assertEqual(response_json['detail'][0],
  318. "You can't move thread to the category it's already in.")
  319. self.override_other_acl({})
  320. thread_json = self.get_thread_json()
  321. self.assertEqual(thread_json['category']['id'], self.category.pk)
  322. def test_thread_flatten_categories(self):
  323. """api flatten thread categories"""
  324. response = self.patch(self.api_link, [
  325. {'op': 'replace', 'path': 'flatten-categories', 'value': None}
  326. ])
  327. self.assertEqual(response.status_code, 200)
  328. response_json = json.loads(smart_str(response.content))
  329. self.assertEqual(response_json['category'], self.category.pk)
  330. def test_thread_top_flatten_categories(self):
  331. """api flatten thread with top category"""
  332. self.thread.category = self.category_b
  333. self.thread.save()
  334. self.override_other_acl({})
  335. response = self.patch(self.api_link, [
  336. {
  337. 'op': 'add',
  338. 'path': 'top-category',
  339. 'value': Category.objects.root_category().pk,
  340. },
  341. {'op': 'replace', 'path': 'flatten-categories', 'value': None},
  342. ])
  343. self.assertEqual(response.status_code, 200)
  344. response_json = json.loads(smart_str(response.content))
  345. self.assertEqual(response_json['top_category'], self.category.pk)
  346. self.assertEqual(response_json['category'], self.category_b.pk)
  347. class ThreadCloseApiTests(ThreadPatchApiTestCase):
  348. def test_close_thread(self):
  349. """api makes it possible to close thread"""
  350. self.override_acl({
  351. 'can_close_threads': True
  352. })
  353. response = self.patch(self.api_link, [
  354. {'op': 'replace', 'path': 'is-closed', 'value': True}
  355. ])
  356. self.assertEqual(response.status_code, 200)
  357. thread_json = self.get_thread_json()
  358. self.assertTrue(thread_json['is_closed'])
  359. def test_open_thread(self):
  360. """api makes it possible to open thread"""
  361. self.thread.is_closed = True
  362. self.thread.save()
  363. thread_json = self.get_thread_json()
  364. self.assertTrue(thread_json['is_closed'])
  365. self.override_acl({
  366. 'can_close_threads': True
  367. })
  368. response = self.patch(self.api_link, [
  369. {'op': 'replace', 'path': 'is-closed', 'value': False}
  370. ])
  371. self.assertEqual(response.status_code, 200)
  372. thread_json = self.get_thread_json()
  373. self.assertFalse(thread_json['is_closed'])
  374. def test_close_thread_no_permission(self):
  375. """api close thread with no permission fails"""
  376. self.override_acl({
  377. 'can_close_threads': False
  378. })
  379. response = self.patch(self.api_link, [
  380. {'op': 'replace', 'path': 'is-closed', 'value': True}
  381. ])
  382. self.assertEqual(response.status_code, 400)
  383. response_json = json.loads(smart_str(response.content))
  384. self.assertEqual(response_json['detail'][0],
  385. "You don't have permission to close this thread.")
  386. thread_json = self.get_thread_json()
  387. self.assertFalse(thread_json['is_closed'])
  388. def test_open_thread_no_permission(self):
  389. """api open thread with no permission fails"""
  390. self.thread.is_closed = True
  391. self.thread.save()
  392. thread_json = self.get_thread_json()
  393. self.assertTrue(thread_json['is_closed'])
  394. self.override_acl({
  395. 'can_close_threads': False
  396. })
  397. response = self.patch(self.api_link, [
  398. {'op': 'replace', 'path': 'is-closed', 'value': False}
  399. ])
  400. self.assertEqual(response.status_code, 400)
  401. response_json = json.loads(smart_str(response.content))
  402. self.assertEqual(response_json['detail'][0],
  403. "You don't have permission to open this thread.")
  404. thread_json = self.get_thread_json()
  405. self.assertTrue(thread_json['is_closed'])
  406. class ThreadApproveApiTests(ThreadPatchApiTestCase):
  407. def test_approve_thread(self):
  408. """api makes it possible to approve thread"""
  409. self.thread.is_unapproved = True
  410. self.thread.save()
  411. self.override_acl({
  412. 'can_approve_content': 1
  413. })
  414. response = self.patch(self.api_link, [
  415. {'op': 'replace', 'path': 'is-unapproved', 'value': False}
  416. ])
  417. self.assertEqual(response.status_code, 200)
  418. thread_json = self.get_thread_json()
  419. self.assertFalse(thread_json['is_unapproved'])
  420. def test_unapprove_thread(self):
  421. """api returns permission error on approval removal"""
  422. self.override_acl({
  423. 'can_approve_content': 1
  424. })
  425. response = self.patch(self.api_link, [
  426. {'op': 'replace', 'path': 'is-unapproved', 'value': True}
  427. ])
  428. self.assertEqual(response.status_code, 400)
  429. response_json = json.loads(smart_str(response.content))
  430. self.assertEqual(response_json['detail'][0],
  431. "Content approval can't be reversed.")
  432. class ThreadHideApiTests(ThreadPatchApiTestCase):
  433. def test_hide_thread(self):
  434. """api makes it possible to hide thread"""
  435. self.override_acl({
  436. 'can_hide_threads': 1
  437. })
  438. response = self.patch(self.api_link, [
  439. {'op': 'replace', 'path': 'is-hidden', 'value': True}
  440. ])
  441. self.assertEqual(response.status_code, 200)
  442. self.override_acl({
  443. 'can_hide_threads': 1
  444. })
  445. thread_json = self.get_thread_json()
  446. self.assertTrue(thread_json['is_hidden'])
  447. def test_show_thread(self):
  448. """api makes it possible to unhide thread"""
  449. self.thread.is_hidden = True
  450. self.thread.save()
  451. self.override_acl({
  452. 'can_hide_threads': 1
  453. })
  454. thread_json = self.get_thread_json()
  455. self.assertTrue(thread_json['is_hidden'])
  456. self.override_acl({
  457. 'can_hide_threads': 1
  458. })
  459. response = self.patch(self.api_link, [
  460. {'op': 'replace', 'path': 'is-hidden', 'value': False}
  461. ])
  462. self.assertEqual(response.status_code, 200)
  463. self.override_acl({
  464. 'can_hide_threads': 1
  465. })
  466. thread_json = self.get_thread_json()
  467. self.assertFalse(thread_json['is_hidden'])
  468. def test_hide_thread_no_permission(self):
  469. """api hide thread with no permission fails"""
  470. self.override_acl({
  471. 'can_hide_threads': 0
  472. })
  473. response = self.patch(self.api_link, [
  474. {'op': 'replace', 'path': 'is-hidden', 'value': True}
  475. ])
  476. self.assertEqual(response.status_code, 400)
  477. response_json = json.loads(smart_str(response.content))
  478. self.assertEqual(response_json['detail'][0],
  479. "You don't have permission to hide this thread.")
  480. thread_json = self.get_thread_json()
  481. self.assertFalse(thread_json['is_hidden'])
  482. def test_show_thread_no_permission(self):
  483. """api unhide thread with no permission fails"""
  484. self.thread.is_hidden = True
  485. self.thread.save()
  486. self.override_acl({
  487. 'can_hide_threads': 1
  488. })
  489. thread_json = self.get_thread_json()
  490. self.assertTrue(thread_json['is_hidden'])
  491. self.override_acl({
  492. 'can_hide_threads': 0
  493. })
  494. response = self.patch(self.api_link, [
  495. {'op': 'replace', 'path': 'is-hidden', 'value': False}
  496. ])
  497. self.assertEqual(response.status_code, 404)
  498. class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
  499. def test_subscribe_thread(self):
  500. """api makes it possible to subscribe thread"""
  501. response = self.patch(self.api_link, [
  502. {'op': 'replace', 'path': 'subscription', 'value': 'notify'}
  503. ])
  504. self.assertEqual(response.status_code, 200)
  505. thread_json = self.get_thread_json()
  506. self.assertFalse(thread_json['subscription'])
  507. subscription = self.user.subscription_set.get(thread=self.thread)
  508. self.assertFalse(subscription.send_email)
  509. def test_subscribe_thread_with_email(self):
  510. """api makes it possible to subscribe thread with emails"""
  511. response = self.patch(self.api_link, [
  512. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  513. ])
  514. self.assertEqual(response.status_code, 200)
  515. thread_json = self.get_thread_json()
  516. self.assertTrue(thread_json['subscription'])
  517. subscription = self.user.subscription_set.get(thread=self.thread)
  518. self.assertTrue(subscription.send_email)
  519. def test_unsubscribe_thread(self):
  520. """api makes it possible to unsubscribe thread"""
  521. response = self.patch(self.api_link, [
  522. {'op': 'replace', 'path': 'subscription', 'value': 'remove'}
  523. ])
  524. self.assertEqual(response.status_code, 200)
  525. thread_json = self.get_thread_json()
  526. self.assertIsNone(thread_json['subscription'])
  527. self.assertEqual(self.user.subscription_set.count(), 0)
  528. def test_subscribe_as_guest(self):
  529. """api makes it impossible to subscribe thread"""
  530. self.logout_user()
  531. response = self.patch(self.api_link, [
  532. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  533. ])
  534. self.assertEqual(response.status_code, 403)
  535. def test_subscribe_nonexistant_thread(self):
  536. """api makes it impossible to subscribe nonexistant thread"""
  537. bad_api_link = self.api_link.replace(
  538. six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9))
  539. response = self.patch(bad_api_link, [
  540. {'op': 'replace', 'path': 'subscription', 'value': 'email'}
  541. ])
  542. self.assertEqual(response.status_code, 404)