test_thread_pollvotes_api.py 19 KB

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