test_thread_pollvotes_api.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. from datetime import timedelta
  2. from django.contrib.auth import get_user_model
  3. from django.urls import reverse
  4. from django.utils import timezone
  5. from misago.acl import add_acl
  6. from misago.core.utils import serialize_datetime
  7. from misago.threads.models import Poll
  8. from .test_thread_poll_api import ThreadPollApiTestCase
  9. UserModel = get_user_model()
  10. class ThreadGetVotesTests(ThreadPollApiTestCase):
  11. def setUp(self):
  12. super(ThreadGetVotesTests, self).setUp()
  13. self.mock_poll()
  14. self.poll.is_public = True
  15. self.poll.save()
  16. self.api_link = reverse(
  17. 'misago:api:thread-poll-votes',
  18. kwargs={
  19. 'thread_pk': self.thread.pk,
  20. 'pk': self.poll.pk,
  21. }
  22. )
  23. def get_votes_json(self):
  24. choices_votes = {choice['hash']: [] for choice in self.poll.choices}
  25. queryset = self.poll.pollvote_set.order_by('-id').select_related()
  26. for vote in queryset:
  27. if vote.voter:
  28. url = vote.voter.get_absolute_url()
  29. else:
  30. url = None
  31. choices_votes[vote.choice_hash].append({
  32. 'username': vote.voter_name,
  33. 'voted_on': serialize_datetime(vote.voted_on),
  34. 'url': url
  35. })
  36. return choices_votes
  37. def test_anonymous(self):
  38. """api allows guests to get poll votes"""
  39. self.logout_user()
  40. votes_json = self.get_votes_json()
  41. response = self.client.get(self.api_link)
  42. self.assertEqual(response.status_code, 200)
  43. self.assertEqual(response.json(), [
  44. {
  45. 'hash': 'aaaaaaaaaaaa',
  46. 'label': 'Alpha',
  47. 'votes': 1,
  48. 'voters': votes_json['aaaaaaaaaaaa'],
  49. },
  50. {
  51. 'hash': 'bbbbbbbbbbbb',
  52. 'label': 'Beta',
  53. 'votes': 0,
  54. 'voters': [],
  55. },
  56. {
  57. 'hash': 'gggggggggggg',
  58. 'label': 'Gamma',
  59. 'votes': 2,
  60. 'voters': votes_json['gggggggggggg'],
  61. },
  62. {
  63. 'hash': 'dddddddddddd',
  64. 'label': 'Delta',
  65. 'votes': 1,
  66. 'voters': votes_json['dddddddddddd'],
  67. },
  68. ])
  69. self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
  70. self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
  71. self.assertEqual(len(votes_json['gggggggggggg']), 2)
  72. self.assertEqual(len(votes_json['dddddddddddd']), 1)
  73. def test_invalid_thread_id(self):
  74. """api validates that thread id is integer"""
  75. api_link = reverse(
  76. 'misago:api:thread-poll-votes',
  77. kwargs={
  78. 'thread_pk': 'kjha6dsa687sa',
  79. 'pk': self.poll.pk,
  80. }
  81. )
  82. response = self.client.get(api_link)
  83. self.assertEqual(response.status_code, 404)
  84. self.assertEqual(response.json(), {
  85. 'detail': "NOT FOUND",
  86. })
  87. def test_nonexistant_thread_id(self):
  88. """api validates that thread exists"""
  89. api_link = reverse(
  90. 'misago:api:thread-poll-votes',
  91. kwargs={
  92. 'thread_pk': self.thread.pk + 1,
  93. 'pk': self.poll.pk,
  94. }
  95. )
  96. response = self.client.get(api_link)
  97. self.assertEqual(response.status_code, 404)
  98. self.assertEqual(response.json(), {
  99. 'detail': "No Thread matches the given query.",
  100. })
  101. def test_invalid_poll_id(self):
  102. """api validates that poll id is integer"""
  103. api_link = reverse(
  104. 'misago:api:thread-poll-votes',
  105. kwargs={
  106. 'thread_pk': self.thread.pk,
  107. 'pk': 'sad98as7d97sa98',
  108. }
  109. )
  110. response = self.client.get(api_link)
  111. self.assertEqual(response.status_code, 404)
  112. self.assertEqual(response.json(), {
  113. 'detail': "NOT FOUND",
  114. })
  115. def test_nonexistant_poll_id(self):
  116. """api validates that poll exists"""
  117. api_link = reverse(
  118. 'misago:api:thread-poll-votes',
  119. kwargs={
  120. 'thread_pk': self.thread.pk,
  121. 'pk': self.poll.pk + 123,
  122. }
  123. )
  124. response = self.client.get(api_link)
  125. self.assertEqual(response.status_code, 404)
  126. self.assertEqual(response.json(), {
  127. 'detail': "NOT FOUND",
  128. })
  129. def test_no_permission(self):
  130. """api chcecks permission to see poll voters"""
  131. self.override_acl({'can_always_see_poll_voters': False})
  132. self.poll.is_public = False
  133. self.poll.save()
  134. response = self.client.get(self.api_link)
  135. self.assertEqual(response.status_code, 403)
  136. self.assertEqual(response.json(), {
  137. 'detail': "You dont have permission to this poll's voters.",
  138. })
  139. def test_nonpublic_poll(self):
  140. """api validates that poll is public"""
  141. self.logout_user()
  142. self.poll.is_public = False
  143. self.poll.save()
  144. response = self.client.get(self.api_link)
  145. self.assertEqual(response.status_code, 403)
  146. self.assertEqual(response.json(), {
  147. 'detail': "You dont have permission to this poll's voters.",
  148. })
  149. def test_get_votes(self):
  150. """api returns list of voters"""
  151. votes_json = self.get_votes_json()
  152. response = self.client.get(self.api_link)
  153. self.assertEqual(response.status_code, 200)
  154. self.assertEqual(response.json(), [
  155. {
  156. 'hash': 'aaaaaaaaaaaa',
  157. 'label': 'Alpha',
  158. 'votes': 1,
  159. 'voters': votes_json['aaaaaaaaaaaa'],
  160. },
  161. {
  162. 'hash': 'bbbbbbbbbbbb',
  163. 'label': 'Beta',
  164. 'votes': 0,
  165. 'voters': [],
  166. },
  167. {
  168. 'hash': 'gggggggggggg',
  169. 'label': 'Gamma',
  170. 'votes': 2,
  171. 'voters': votes_json['gggggggggggg'],
  172. },
  173. {
  174. 'hash': 'dddddddddddd',
  175. 'label': 'Delta',
  176. 'votes': 1,
  177. 'voters': votes_json['dddddddddddd'],
  178. },
  179. ])
  180. self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
  181. self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
  182. self.assertEqual(len(votes_json['gggggggggggg']), 2)
  183. self.assertEqual(len(votes_json['dddddddddddd']), 1)
  184. def test_get_votes_private_poll(self):
  185. """api returns list of voters on private poll for user with permission"""
  186. self.override_acl({'can_always_see_poll_voters': True})
  187. self.poll.is_public = False
  188. self.poll.save()
  189. votes_json = self.get_votes_json()
  190. response = self.client.get(self.api_link)
  191. self.assertEqual(response.status_code, 200)
  192. self.assertEqual(response.json(), [
  193. {
  194. 'hash': 'aaaaaaaaaaaa',
  195. 'label': 'Alpha',
  196. 'votes': 1,
  197. 'voters': votes_json['aaaaaaaaaaaa'],
  198. },
  199. {
  200. 'hash': 'bbbbbbbbbbbb',
  201. 'label': 'Beta',
  202. 'votes': 0,
  203. 'voters': [],
  204. },
  205. {
  206. 'hash': 'gggggggggggg',
  207. 'label': 'Gamma',
  208. 'votes': 2,
  209. 'voters': votes_json['gggggggggggg'],
  210. },
  211. {
  212. 'hash': 'dddddddddddd',
  213. 'label': 'Delta',
  214. 'votes': 1,
  215. 'voters': votes_json['dddddddddddd'],
  216. },
  217. ])
  218. self.assertEqual(len(votes_json['aaaaaaaaaaaa']), 1)
  219. self.assertEqual(len(votes_json['bbbbbbbbbbbb']), 0)
  220. self.assertEqual(len(votes_json['gggggggggggg']), 2)
  221. self.assertEqual(len(votes_json['dddddddddddd']), 1)
  222. class ThreadPostVotesTests(ThreadPollApiTestCase):
  223. def setUp(self):
  224. super(ThreadPostVotesTests, self).setUp()
  225. self.mock_poll()
  226. self.api_link = reverse(
  227. 'misago:api:thread-poll-votes',
  228. kwargs={
  229. 'thread_pk': self.thread.pk,
  230. 'pk': self.poll.pk,
  231. }
  232. )
  233. def delete_user_votes(self):
  234. self.poll.choices[2]['votes'] = 1
  235. self.poll.choices[3]['votes'] = 0
  236. self.poll.votes = 2
  237. self.poll.save()
  238. self.poll.pollvote_set.filter(voter=self.user).delete()
  239. def test_anonymous(self):
  240. """api requires you to sign in to vote in poll"""
  241. self.logout_user()
  242. response = self.post(self.api_link)
  243. self.assertEqual(response.status_code, 403)
  244. self.assertEqual(response.json(), {
  245. 'detail': "This action is not available to guests.",
  246. })
  247. def test_empty_vote_json(self):
  248. """api validates if vote that user has made was empty"""
  249. self.delete_user_votes()
  250. response = self.client.post(
  251. self.api_link, '[]', content_type='application/json'
  252. )
  253. self.assertEqual(response.status_code, 400)
  254. self.assertEqual(response.json(), {
  255. 'choices': ["You have to make a choice."],
  256. })
  257. def test_empty_vote_form(self):
  258. """api validates if vote that user has made was empty"""
  259. self.delete_user_votes()
  260. response = self.client.post(self.api_link)
  261. self.assertEqual(response.status_code, 400)
  262. self.assertEqual(response.json(), {
  263. 'choices': ["You have to make a choice."],
  264. })
  265. def test_malformed_vote(self):
  266. """api validates if vote that user has made was correctly structured"""
  267. self.delete_user_votes()
  268. response = self.post(self.api_link)
  269. self.assertEqual(response.status_code, 400)
  270. self.assertEqual(response.json(), {
  271. 'choices': ['Expected a list of items but got type "dict".'],
  272. })
  273. response = self.post(self.api_link, data={})
  274. self.assertEqual(response.status_code, 400)
  275. self.assertEqual(response.json(), {
  276. 'choices': ['Expected a list of items but got type "dict".'],
  277. })
  278. response = self.post(self.api_link, data='hello')
  279. self.assertEqual(response.status_code, 400)
  280. self.assertEqual(response.json(), {
  281. 'choices': ['Expected a list of items but got type "str".'],
  282. })
  283. response = self.post(self.api_link, data=123)
  284. self.assertEqual(response.status_code, 400)
  285. self.assertEqual(response.json(), {
  286. 'choices': ['Expected a list of items but got type "int".'],
  287. })
  288. def test_invalid_choices(self):
  289. """api validates if vote that user has made overlaps with allowed votes"""
  290. self.delete_user_votes()
  291. response = self.post(self.api_link, data=['lorem', 'ipsum'])
  292. self.assertContains(response, "One or more of poll choices were invalid.", status_code=400)
  293. def test_too_many_choices(self):
  294. """api validates if vote that user has made overlaps with allowed votes"""
  295. self.poll.allowed_choices = 1
  296. self.poll.allow_revotes = True
  297. self.poll.save()
  298. response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
  299. self.assertEqual(response.status_code, 400)
  300. self.assertEqual(response.json(), {
  301. 'choices': ["This poll disallows voting for more than 1 choice."],
  302. })
  303. def test_revote(self):
  304. """api validates if user is trying to change vote in poll that disallows revoting"""
  305. response = self.post(self.api_link, data=['lorem', 'ipsum'])
  306. self.assertEqual(response.status_code, 403)
  307. self.assertEqual(response.json(), {
  308. 'detail': "You have already voted in this poll.",
  309. })
  310. self.delete_user_votes()
  311. response = self.post(self.api_link)
  312. self.assertEqual(response.status_code, 400)
  313. def test_vote_in_closed_thread(self):
  314. """api validates is user has permission to vote poll in closed thread"""
  315. self.override_acl(category={'can_close_threads': 0})
  316. self.thread.is_closed = True
  317. self.thread.save()
  318. self.delete_user_votes()
  319. response = self.post(self.api_link)
  320. self.assertEqual(response.status_code, 403)
  321. self.assertEqual(response.json(), {
  322. 'detail': "This thread is closed. You can't vote in it.",
  323. })
  324. self.override_acl(category={'can_close_threads': 1})
  325. response = self.post(self.api_link)
  326. self.assertEqual(response.status_code, 400)
  327. def test_vote_in_closed_category(self):
  328. """api validates is user has permission to vote poll in closed category"""
  329. self.override_acl(category={'can_close_threads': 0})
  330. self.category.is_closed = True
  331. self.category.save()
  332. self.delete_user_votes()
  333. response = self.post(self.api_link)
  334. self.assertEqual(response.status_code, 403)
  335. self.assertEqual(response.json(), {
  336. 'detail': "This category is closed. You can't vote in it.",
  337. })
  338. self.override_acl(category={'can_close_threads': 1})
  339. response = self.post(self.api_link)
  340. self.assertEqual(response.status_code, 400)
  341. def test_vote_in_finished_poll(self):
  342. """api valdiates if poll has finished before letting user to vote in it"""
  343. self.poll.posted_on = timezone.now() - timedelta(days=15)
  344. self.poll.length = 5
  345. self.poll.save()
  346. self.delete_user_votes()
  347. response = self.post(self.api_link)
  348. self.assertEqual(response.status_code, 403)
  349. self.assertEqual(response.json(), {
  350. 'detail': "This poll is over. You can't vote in it.",
  351. })
  352. self.poll.length = 50
  353. self.poll.save()
  354. response = self.post(self.api_link)
  355. self.assertEqual(response.status_code, 400)
  356. def test_fresh_vote(self):
  357. """api handles first vote in poll"""
  358. self.delete_user_votes()
  359. add_acl(self.user, self.poll)
  360. self.poll.acl['can_vote'] = False
  361. response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
  362. self.assertEqual(response.status_code, 200)
  363. self.assertEqual(response.json(), {
  364. 'id': self.poll.id,
  365. 'poster_name': self.user.username,
  366. 'posted_on': serialize_datetime(self.poll.posted_on),
  367. 'length': 0,
  368. 'question': "Lorem ipsum dolor met?",
  369. 'allowed_choices': 2,
  370. 'allow_revotes': False,
  371. 'votes': 4,
  372. 'is_public': False,
  373. 'acl': self.poll.acl,
  374. 'choices': [
  375. {
  376. 'hash': 'aaaaaaaaaaaa',
  377. 'label': 'Alpha',
  378. 'selected': True,
  379. 'votes': 2
  380. },
  381. {
  382. 'hash': 'bbbbbbbbbbbb',
  383. 'label': 'Beta',
  384. 'selected': True,
  385. 'votes': 1
  386. },
  387. {
  388. 'hash': 'gggggggggggg',
  389. 'label': 'Gamma',
  390. 'selected': False,
  391. 'votes': 1
  392. },
  393. {
  394. 'hash': 'dddddddddddd',
  395. 'label': 'Delta',
  396. 'selected': False,
  397. 'votes': 0
  398. },
  399. ],
  400. 'api': {
  401. 'index': self.poll.get_api_url(),
  402. 'votes': self.poll.get_votes_api_url(),
  403. },
  404. 'url': {
  405. 'poster': self.user.get_absolute_url(),
  406. },
  407. })
  408. # validate state change
  409. poll = Poll.objects.get(pk=self.poll.pk)
  410. self.assertEqual(poll.votes, 4)
  411. self.assertEqual(poll.choices, [
  412. {
  413. 'hash': 'aaaaaaaaaaaa',
  414. 'label': 'Alpha',
  415. 'votes': 2
  416. },
  417. {
  418. 'hash': 'bbbbbbbbbbbb',
  419. 'label': 'Beta',
  420. 'votes': 1
  421. },
  422. {
  423. 'hash': 'gggggggggggg',
  424. 'label': 'Gamma',
  425. 'votes': 1
  426. },
  427. {
  428. 'hash': 'dddddddddddd',
  429. 'label': 'Delta',
  430. 'votes': 0
  431. },
  432. ])
  433. self.assertEqual(poll.pollvote_set.count(), 4)
  434. # validate poll disallows for revote
  435. response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
  436. self.assertEqual(response.status_code, 403)
  437. self.assertEqual(response.json(), {
  438. 'detail': "You have already voted in this poll.",
  439. })
  440. def test_vote_change(self):
  441. """api handles vote change"""
  442. self.poll.allow_revotes = True
  443. self.poll.save()
  444. add_acl(self.user, self.poll)
  445. response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
  446. self.assertEqual(response.status_code, 200)
  447. self.assertEqual(response.json(), {
  448. 'id': self.poll.id,
  449. 'poster_name': self.user.username,
  450. 'posted_on': serialize_datetime(self.poll.posted_on),
  451. 'length': 0,
  452. 'question': "Lorem ipsum dolor met?",
  453. 'allowed_choices': 2,
  454. 'allow_revotes': True,
  455. 'votes': 4,
  456. 'is_public': False,
  457. 'acl': self.poll.acl,
  458. 'choices': [
  459. {
  460. 'hash': 'aaaaaaaaaaaa',
  461. 'label': 'Alpha',
  462. 'selected': True,
  463. 'votes': 2
  464. },
  465. {
  466. 'hash': 'bbbbbbbbbbbb',
  467. 'label': 'Beta',
  468. 'selected': True,
  469. 'votes': 1
  470. },
  471. {
  472. 'hash': 'gggggggggggg',
  473. 'label': 'Gamma',
  474. 'selected': False,
  475. 'votes': 1
  476. },
  477. {
  478. 'hash': 'dddddddddddd',
  479. 'label': 'Delta',
  480. 'selected': False,
  481. 'votes': 0
  482. },
  483. ],
  484. 'api': {
  485. 'index': self.poll.get_api_url(),
  486. 'votes': self.poll.get_votes_api_url(),
  487. },
  488. 'url': {
  489. 'poster': self.user.get_absolute_url(),
  490. },
  491. })
  492. # validate state change
  493. poll = Poll.objects.get(pk=self.poll.pk)
  494. self.assertEqual(poll.votes, 4)
  495. self.assertEqual(poll.choices, [
  496. {
  497. 'hash': 'aaaaaaaaaaaa',
  498. 'label': 'Alpha',
  499. 'votes': 2
  500. },
  501. {
  502. 'hash': 'bbbbbbbbbbbb',
  503. 'label': 'Beta',
  504. 'votes': 1
  505. },
  506. {
  507. 'hash': 'gggggggggggg',
  508. 'label': 'Gamma',
  509. 'votes': 1
  510. },
  511. {
  512. 'hash': 'dddddddddddd',
  513. 'label': 'Delta',
  514. 'votes': 0
  515. },
  516. ])
  517. self.assertEqual(poll.pollvote_set.count(), 4)
  518. # validate poll allows for revote
  519. response = self.post(self.api_link, data=['aaaaaaaaaaaa'])
  520. self.assertEqual(response.status_code, 200)