from base64 import b64decode from io import BytesIO from unittest import mock from PIL import Image from django.test import TestCase from django.utils import timezone from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from api import settings from desecapi.models import Captcha from desecapi.serializers import CaptchaSolutionSerializer from desecapi.tests.base import DesecTestCase class CaptchaClient(APIClient): def obtain(self, **kwargs): return self.post(reverse('v1:captcha'), data=kwargs) class CaptchaModelTestCase(TestCase): captcha_class = Captcha def test_random_initialization(self): captcha = [self.captcha_class() for _ in range(2)] self.assertNotEqual(captcha[0].content, None) self.assertNotEqual(captcha[0].content, '') self.assertNotEqual(captcha[0].content, captcha[1].content) def test_verify_solution(self): for _ in range(10): c = self.captcha_class.objects.create() self.assertFalse(c.verify('likely the wrong solution!')) c = self.captcha_class.objects.create() self.assertTrue(c.verify(c.content)) class CaptchaWorkflowTestCase(DesecTestCase): client_class = CaptchaClient captcha_class = Captcha serializer_class = CaptchaSolutionSerializer kind = None def verify(self, id, solution): """ Given unsafe (user-input) id and solution, this is how the CAPTCHA module expects you to verify that id and solution are valid. :param id: unsafe ID :param solution: unsafe proposed solution :return: whether the id/solution pair is correct """ # use the serializer to validate the solution; id is validated implicitly on DB lookup return self.serializer_class(data={'id': id, 'solution': solution}).is_valid() def obtain(self): if self.kind is None: return self.client.obtain() else: return self.client.obtain(kind=self.kind) def test_obtain(self): response = self.obtain() self.assertContains(response, 'id', status_code=status.HTTP_201_CREATED) self.assertContains(response, 'challenge', status_code=status.HTTP_201_CREATED) self.assertTrue('content' not in response.data) self.assertTrue(len(response.data) == 3) self.assertEqual(self.captcha_class.objects.all().count(), 1) # use the value of f'' # to display the CAPTCHA in a browser def test_verify_correct(self): id = self.obtain().data['id'] correct_solution = Captcha.objects.get(id=id).content self.assertTrue(self.verify(id, correct_solution)) def test_verify_incorrect(self): id = self.obtain().data['id'] wrong_solution = 'most certainly wrong!' self.assertFalse(self.verify(id, wrong_solution)) def test_expired(self): id = self.obtain().data['id'] correct_solution = Captcha.objects.get(id=id).content with mock.patch('desecapi.models.timezone.now', return_value=timezone.now() + settings.CAPTCHA_VALIDITY_PERIOD): self.assertFalse(self.verify(id, correct_solution)) class ImageCaptchaWorkflowTestCase(CaptchaWorkflowTestCase): kind = 'image' def test_length(self): self.assertTrue(5000 < len(self.obtain().data['challenge']) < 50000) def test_parses(self): for _ in range(10): # use the show method on the Image object to see the actual image during test run # This also allows an impression of how the CAPTCHAs will look like. cap = self.obtain().data challenge = b64decode(cap['challenge']) Image.open(BytesIO(challenge)) # .show() class AudioCaptchaWorkflowTestCase(CaptchaWorkflowTestCase): kind = 'audio' def test_length(self): self.assertTrue(10**5 < len(self.obtain().data['challenge']) < 10**6) def test_parses(self): for _ in range(10): challenge = b64decode(self.obtain().data['challenge']) self.assertTrue(b'WAVE' in challenge)