test_thread_pollcreate_api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. from django.urls import reverse
  2. from misago.acl import add_acl
  3. from misago.core.utils import serialize_datetime
  4. from misago.threads.models import Poll, Thread
  5. from misago.threads.serializers.poll import MAX_POLL_OPTIONS
  6. from .test_thread_poll_api import ThreadPollApiTestCase
  7. class ThreadPollCreateTests(ThreadPollApiTestCase):
  8. def test_anonymous(self):
  9. """api requires you to sign in to create poll"""
  10. self.logout_user()
  11. response = self.post(self.api_link)
  12. self.assertEqual(response.status_code, 403)
  13. self.assertEqual(response.json(), {
  14. 'detail': "This action is not available to guests.",
  15. })
  16. def test_invalid_thread_id(self):
  17. """api validates that thread id is integer"""
  18. api_link = reverse(
  19. 'misago:api:thread-poll-list', kwargs={
  20. 'thread_pk': 'kjha6dsa687sa',
  21. }
  22. )
  23. response = self.post(api_link)
  24. self.assertEqual(response.status_code, 404)
  25. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  26. def test_nonexistant_thread_id(self):
  27. """api validates that thread exists"""
  28. api_link = reverse(
  29. 'misago:api:thread-poll-list', kwargs={
  30. 'thread_pk': self.thread.pk + 1,
  31. }
  32. )
  33. response = self.post(api_link)
  34. self.assertEqual(response.status_code, 404)
  35. self.assertEqual(response.json(), {'detail': 'NOT FOUND'})
  36. def test_no_permission(self):
  37. """api validates that user has permission to start poll in thread"""
  38. self.override_acl({'can_start_polls': 0})
  39. response = self.post(self.api_link)
  40. self.assertEqual(response.status_code, 403)
  41. self.assertEqual(response.json(), {
  42. 'detail': "You can't start polls.",
  43. })
  44. def test_no_permission_closed_thread(self):
  45. """api validates that user has permission to start poll in closed thread"""
  46. self.override_acl(category={'can_close_threads': 0})
  47. self.thread.is_closed = True
  48. self.thread.save()
  49. response = self.post(self.api_link)
  50. self.assertEqual(response.status_code, 403)
  51. self.assertEqual(response.json(), {
  52. 'detail': "This thread is closed. You can't start polls in it.",
  53. })
  54. self.override_acl(category={'can_close_threads': 1})
  55. response = self.post(self.api_link)
  56. self.assertEqual(response.status_code, 400)
  57. def test_no_permission_closed_category(self):
  58. """api validates that user has permission to start poll in closed category"""
  59. self.override_acl(category={'can_close_threads': 0})
  60. self.category.is_closed = True
  61. self.category.save()
  62. response = self.post(self.api_link)
  63. self.assertEqual(response.status_code, 403)
  64. self.assertEqual(response.json(), {
  65. 'detail': "This category is closed. You can't start polls in it.",
  66. })
  67. self.override_acl(category={'can_close_threads': 1})
  68. response = self.post(self.api_link)
  69. self.assertEqual(response.status_code, 400)
  70. def test_no_permission_other_user_thread(self):
  71. """api validates that user has permission to start poll in other user's thread"""
  72. self.override_acl({'can_start_polls': 1})
  73. self.thread.starter = None
  74. self.thread.save()
  75. response = self.post(self.api_link)
  76. self.assertEqual(response.status_code, 403)
  77. self.assertEqual(response.json(), {
  78. 'detail': "You can't start polls in other users threads.",
  79. })
  80. self.override_acl({'can_start_polls': 2})
  81. response = self.post(self.api_link)
  82. self.assertEqual(response.status_code, 400)
  83. def test_no_permission_poll_exists(self):
  84. """api validates that user can't start second poll in thread"""
  85. self.thread.poll = Poll.objects.create(
  86. thread=self.thread,
  87. category=self.category,
  88. poster_name='Test',
  89. poster_slug='test',
  90. poster_ip='127.0.0.1',
  91. length=30,
  92. question='Test',
  93. choices=[
  94. {
  95. 'hash': 't3st'
  96. },
  97. ],
  98. allowed_choices=1,
  99. )
  100. response = self.post(self.api_link)
  101. self.assertEqual(response.status_code, 403)
  102. self.assertEqual(response.json(), {
  103. 'detail': "There's already a poll in this thread.",
  104. })
  105. def test_empty_data(self):
  106. """api handles empty request data"""
  107. response = self.post(self.api_link)
  108. self.assertEqual(response.status_code, 400)
  109. self.assertEqual(response.json(), {
  110. 'question': ["This field is required."],
  111. 'choices': ["This field is required."],
  112. 'length': ["This field is required."],
  113. 'allowed_choices': ["This field is required."],
  114. })
  115. def test_length_validation(self):
  116. """api validates poll's length"""
  117. response = self.post(
  118. self.api_link, data={
  119. 'length': -1,
  120. }
  121. )
  122. self.assertEqual(response.status_code, 400)
  123. self.assertEqual(response.json(), {
  124. 'question': ["This field is required."],
  125. 'choices': ["This field is required."],
  126. 'length': ["Ensure this value is greater than or equal to 0."],
  127. 'allowed_choices': ["This field is required."],
  128. })
  129. response = self.post(
  130. self.api_link, data={
  131. 'length': 200,
  132. }
  133. )
  134. self.assertEqual(response.status_code, 400)
  135. self.assertEqual(response.json(), {
  136. 'question': ["This field is required."],
  137. 'choices': ["This field is required."],
  138. 'length': ["Ensure this value is less than or equal to 180."],
  139. 'allowed_choices': ["This field is required."],
  140. })
  141. def test_question_validation(self):
  142. """api validates question length"""
  143. response = self.post(
  144. self.api_link, data={
  145. 'question': 'abcd' * 255,
  146. }
  147. )
  148. self.assertEqual(response.status_code, 400)
  149. self.assertEqual(response.json(), {
  150. 'question': ["Ensure this field has no more than 255 characters."],
  151. 'choices': ["This field is required."],
  152. 'length': ["This field is required."],
  153. 'allowed_choices': ["This field is required."],
  154. })
  155. def test_validate_choice_length(self):
  156. """api validates single choice length"""
  157. response = self.post(
  158. self.api_link, data={
  159. 'choices': [
  160. {
  161. 'hash': 'qwertyuiopas',
  162. 'label': '',
  163. },
  164. ],
  165. }
  166. )
  167. self.assertEqual(response.status_code, 400)
  168. self.assertEqual(response.json(), {
  169. 'question': ["This field is required."],
  170. 'choices': ["One or more poll choices are invalid."],
  171. 'length': ["This field is required."],
  172. 'allowed_choices': ["This field is required."],
  173. })
  174. response = self.post(
  175. self.api_link,
  176. data={
  177. 'choices': [
  178. {
  179. 'hash': 'qwertyuiopas',
  180. 'label': 'abcd' * 255,
  181. },
  182. ],
  183. }
  184. )
  185. self.assertEqual(response.status_code, 400)
  186. self.assertEqual(response.json(), {
  187. 'question': ["This field is required."],
  188. 'choices': ["One or more poll choices are invalid."],
  189. 'length': ["This field is required."],
  190. 'allowed_choices': ["This field is required."],
  191. })
  192. def test_validate_two_choices(self):
  193. """api validates that there are at least two choices in poll"""
  194. response = self.post(self.api_link, data={'choices': [{'label': 'Choice'}]})
  195. self.assertEqual(response.status_code, 400)
  196. self.assertEqual(response.json(), {
  197. 'question': ["This field is required."],
  198. 'choices': ["You need to add at least two choices to a poll."],
  199. 'length': ["This field is required."],
  200. 'allowed_choices': ["This field is required."],
  201. })
  202. def test_validate_max_choices(self):
  203. """api validates that there are no more choices in poll than allowed number"""
  204. response = self.post(
  205. self.api_link, data={
  206. 'choices': [
  207. {
  208. 'label': 'Choice',
  209. },
  210. ] * (MAX_POLL_OPTIONS + 1),
  211. }
  212. )
  213. self.assertEqual(response.status_code, 400)
  214. error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
  215. self.assertEqual(response.json(), {
  216. 'question': ["This field is required."],
  217. 'choices': [
  218. "You can't add more than %s options to a single poll (added %s)." % error_formats
  219. ],
  220. 'length': ["This field is required."],
  221. 'allowed_choices': ["This field is required."],
  222. })
  223. def test_allowed_choices_validation(self):
  224. """api validates allowed choices number"""
  225. response = self.post(self.api_link, data={'allowed_choices': 0})
  226. self.assertEqual(response.status_code, 400)
  227. self.assertEqual(response.json(), {
  228. 'question': ["This field is required."],
  229. 'choices': ["This field is required."],
  230. 'length': ["This field is required."],
  231. 'allowed_choices': ["Ensure this value is greater than or equal to 1."],
  232. })
  233. response = self.post(
  234. self.api_link,
  235. data={
  236. 'length': 0,
  237. 'question': "Lorem ipsum",
  238. 'allowed_choices': 3,
  239. 'choices': [
  240. {
  241. 'label': 'Choice',
  242. },
  243. {
  244. 'label': 'Choice',
  245. },
  246. ],
  247. }
  248. )
  249. self.assertEqual(response.status_code, 400)
  250. self.assertEqual(response.json(), {
  251. 'non_field_errors': [
  252. "Number of allowed choices can't be greater than number of all choices."
  253. ],
  254. })
  255. def test_poll_created(self):
  256. """api creates public poll if provided with valid data"""
  257. response = self.post(
  258. self.api_link,
  259. data={
  260. 'length': 40,
  261. 'question': "Select two best colors",
  262. 'allowed_choices': 2,
  263. 'allow_revotes': True,
  264. 'is_public': True,
  265. 'choices': [
  266. {
  267. 'label': '\nRed ',
  268. },
  269. {
  270. 'label': 'Green',
  271. },
  272. {
  273. 'label': 'Blue',
  274. },
  275. ],
  276. }
  277. )
  278. self.assertEqual(response.status_code, 200)
  279. self.maxDiff = None
  280. poll = Poll.objects.all()[0]
  281. add_acl(self.user, poll)
  282. expected_choices = []
  283. for choice in poll.choices:
  284. expected_choices.append(choice.copy())
  285. expected_choices[-1]['selected'] = False
  286. self.assertEqual(response.json(), {
  287. 'id': poll.id,
  288. 'poster_name': self.user.username,
  289. 'posted_on': serialize_datetime(poll.posted_on),
  290. 'length': 40,
  291. 'question': "Select two best colors",
  292. 'allowed_choices': 2,
  293. 'allow_revotes': True,
  294. 'votes': 0,
  295. 'is_public': True,
  296. 'acl': poll.acl,
  297. 'choices': expected_choices,
  298. 'api': {
  299. 'index': poll.get_api_url(),
  300. 'votes': poll.get_votes_api_url(),
  301. },
  302. 'url': {
  303. 'poster': self.user.get_absolute_url(),
  304. },
  305. })
  306. self.assertEqual(len(poll.choices), 3)
  307. self.assertEqual(len(set([c['hash'] for c in poll.choices])), 3)
  308. self.assertEqual([c['label'] for c in poll.choices], ['Red', 'Green', 'Blue'])
  309. thread = Thread.objects.get(pk=self.thread.pk)
  310. self.assertTrue(thread.has_poll)
  311. self.assertEqual(thread.poll, poll)