test_captcha.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. from base64 import b64decode
  2. from io import BytesIO
  3. from unittest import mock
  4. from PIL import Image
  5. from django.test import TestCase
  6. from django.utils import timezone
  7. from rest_framework import status
  8. from rest_framework.reverse import reverse
  9. from rest_framework.test import APIClient
  10. from api import settings
  11. from desecapi.models import Captcha
  12. from desecapi.serializers import CaptchaSolutionSerializer
  13. from desecapi.tests.base import DesecTestCase
  14. class CaptchaClient(APIClient):
  15. def obtain(self, **kwargs):
  16. return self.post(reverse('v1:captcha'), data=kwargs)
  17. class CaptchaModelTestCase(TestCase):
  18. captcha_class = Captcha
  19. def test_random_initialization(self):
  20. captcha = [self.captcha_class() for _ in range(2)]
  21. self.assertNotEqual(captcha[0].content, None)
  22. self.assertNotEqual(captcha[0].content, '')
  23. self.assertNotEqual(captcha[0].content, captcha[1].content)
  24. def test_verify_solution(self):
  25. for _ in range(10):
  26. c = self.captcha_class.objects.create()
  27. self.assertFalse(c.verify('likely the wrong solution!'))
  28. c = self.captcha_class.objects.create()
  29. self.assertTrue(c.verify(c.content))
  30. class CaptchaWorkflowTestCase(DesecTestCase):
  31. client_class = CaptchaClient
  32. captcha_class = Captcha
  33. serializer_class = CaptchaSolutionSerializer
  34. kind = None
  35. def verify(self, id, solution):
  36. """
  37. Given unsafe (user-input) id and solution, this is how the CAPTCHA module expects you to
  38. verify that id and solution are valid.
  39. :param id: unsafe ID
  40. :param solution: unsafe proposed solution
  41. :return: whether the id/solution pair is correct
  42. """
  43. # use the serializer to validate the solution; id is validated implicitly on DB lookup
  44. return self.serializer_class(data={'id': id, 'solution': solution}).is_valid()
  45. def obtain(self):
  46. if self.kind is None:
  47. return self.client.obtain()
  48. else:
  49. return self.client.obtain(kind=self.kind)
  50. def test_obtain(self):
  51. response = self.obtain()
  52. self.assertContains(response, 'id', status_code=status.HTTP_201_CREATED)
  53. self.assertContains(response, 'challenge', status_code=status.HTTP_201_CREATED)
  54. self.assertTrue('content' not in response.data)
  55. self.assertTrue(len(response.data) == 3)
  56. self.assertEqual(self.captcha_class.objects.all().count(), 1)
  57. # use the value of f'<img src="data:image/png;base64,{response.data["challenge"].decode()}" />'
  58. # to display the CAPTCHA in a browser
  59. def test_verify_correct(self):
  60. id = self.obtain().data['id']
  61. correct_solution = Captcha.objects.get(id=id).content
  62. self.assertTrue(self.verify(id, correct_solution))
  63. def test_verify_incorrect(self):
  64. id = self.obtain().data['id']
  65. wrong_solution = 'most certainly wrong!'
  66. self.assertFalse(self.verify(id, wrong_solution))
  67. def test_expired(self):
  68. id = self.obtain().data['id']
  69. correct_solution = Captcha.objects.get(id=id).content
  70. with mock.patch('desecapi.models.timezone.now', return_value=timezone.now() + settings.CAPTCHA_VALIDITY_PERIOD):
  71. self.assertFalse(self.verify(id, correct_solution))
  72. class ImageCaptchaWorkflowTestCase(CaptchaWorkflowTestCase):
  73. kind = 'image'
  74. def test_length(self):
  75. self.assertTrue(5000 < len(self.obtain().data['challenge']) < 50000)
  76. def test_parses(self):
  77. for _ in range(10):
  78. # use the show method on the Image object to see the actual image during test run
  79. # This also allows an impression of how the CAPTCHAs will look like.
  80. cap = self.obtain().data
  81. challenge = b64decode(cap['challenge'])
  82. Image.open(BytesIO(challenge)) # .show()
  83. class AudioCaptchaWorkflowTestCase(CaptchaWorkflowTestCase):
  84. kind = 'audio'
  85. def test_length(self):
  86. self.assertTrue(10**5 < len(self.obtain().data['challenge']) < 10**6)
  87. def test_parses(self):
  88. for _ in range(10):
  89. challenge = b64decode(self.obtain().data['challenge'])
  90. self.assertTrue(b'WAVE' in challenge)