diff --git a/docs/configuration.rst b/docs/configuration.rst index a01081614..414ae926a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -19,6 +19,8 @@ General Settings Twilio_. * ``'two_factor.gateways.fake.Fake'`` for development, recording tokens to the default logger. + * ``'two_factor.gateways.fake.QueryableFake'`` for testing, recording tokens + to ``QueryableFake.call_tokens``. ``TWO_FACTOR_SMS_GATEWAY`` (default: ``None``) Which gateway to use for sending text messages. Should be set to a module or @@ -28,6 +30,8 @@ General Settings Twilio_. * ``'two_factor.gateways.fake.Fake'`` for development, recording tokens to the default logger. + * ``'two_factor.gateways.fake.QueryableFake'`` for testing, recording tokens + to the ``QueryableFake.sms_tokens``. ``LOGIN_URL`` Should point to the login view provided by this application as described in @@ -118,6 +122,10 @@ Fake Gateway ------------ .. autoclass:: two_factor.gateways.fake.Fake +QueryableFake Gateway +------------ +.. autoclass:: two_factor.gateways.fake.QueryableFake + .. _LOGIN_URL: https://docs.djangoproject.com/en/dev/ref/settings/#login-url .. _LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url .. _LOGOUT_REDIRECT_URL: https://docs.djangoproject.com/en/dev/ref/settings/#logout-redirect-url diff --git a/tests/test_gateways.py b/tests/test_gateways.py index 16bb3f195..0f322cc93 100644 --- a/tests/test_gateways.py +++ b/tests/test_gateways.py @@ -7,7 +7,7 @@ from django.utils.six.moves.urllib.parse import urlencode from phonenumber_field.phonenumber import PhoneNumber -from two_factor.gateways.fake import Fake +from two_factor.gateways.fake import Fake, QueryableFake from two_factor.gateways.twilio.gateway import Twilio try: @@ -117,3 +117,32 @@ def test_gateway(self, logger): fake.send_sms(device=Mock(number=PhoneNumber.from_string('+123')), token=code) logger.info.assert_called_with( 'Fake SMS to %s: "Your token is: %s"', '+123', code) + + +class QueryableFakeGatewayTest(TestCase): + + def tearDown(self): + QueryableFake.reset() + + def test_gateway(self): + fake = QueryableFake() + + for code in ['654321', '87654321']: + fake.make_call(device=Mock(number=PhoneNumber.from_string('+123')), token=code) + self.assertEqual(fake.call_tokens['+123'].pop(), code) + + fake.send_sms(device=Mock(number=PhoneNumber.from_string('+123')), token=code) + self.assertEqual(fake.sms_tokens['+123'].pop(), code) + + def test_gateway_must_be_reset_between_tests(self): + fake = QueryableFake() + codes = ['654321', '87654321'] + for code in codes: + fake.make_call(device=Mock(number=PhoneNumber.from_string('+123')), token=code) + fake.send_sms(device=Mock(number=PhoneNumber.from_string('+123')), token=code) + + self.assertEqual(len(fake.call_tokens['+123']), len(codes)) + self.assertEqual(len(fake.sms_tokens['+123']), len(codes)) + fake.reset() + self.assertEqual(len(fake.call_tokens['+123']), 0) + self.assertEqual(len(fake.sms_tokens['+123']), 0) diff --git a/two_factor/gateways/fake.py b/two_factor/gateways/fake.py index ceb9d3144..5e6d5233e 100644 --- a/two_factor/gateways/fake.py +++ b/two_factor/gateways/fake.py @@ -1,4 +1,5 @@ import logging +from collections import defaultdict logger = logging.getLogger(__name__) @@ -33,3 +34,53 @@ def make_call(device, token): @staticmethod def send_sms(device, token): logger.info('Fake SMS to %s: "Your token is: %s"', device.number.as_e164, token) + + +class QueryableFake(object): + """A Fake gateway that can be queried. + + For example, you may use this in your unit tests:: + + >>> from django.test import TestCase, override_settings + >>> from django.contrib.auth import get_user_model + >>> from django.conf import settings + >>> from two_factor.gateways.fake import QueryableFake + >>> from two_factor.models import PhoneDevice + >>> from phonenumber_field.phonenumber import PhoneNumber + >>> + >>> class MyTestCase(TestCase): + ... def tearDown(self): + ... QueryableFake.reset() + ... + ... @override_settings( + ... TWO_FACTOR_SMS_GATEWAY='two_factor.gateways.fake.QueryableFake', + ... ) + ... def test_something(self): + ... user = get_user_model().objects.create(...) + ... PhoneDevice.objects.create( + ... user=user, name='default', method='sms', + ... number=PhoneNumber.from_string('+441234567890'), + ... ) + ... self.client.post(settings.LOGIN_URL, + ... 'username': user.username, + ... 'password': 'password', + ... }) + ... token = QueryableFake.sms_tokens['+441234567890'].pop() + ... self.client.post(settings.LOGIN_URL, {'token': token}) + """ + sms_tokens = defaultdict(list) + call_tokens = defaultdict(list) + + @classmethod + def clear_all_tokens(cls): + cls.sms_tokens = defaultdict(list) + cls.call_tokens = defaultdict(list) + reset = clear_all_tokens + + @classmethod + def make_call(cls, device, token): + cls.call_tokens[str(device.number)].append(token) + + @classmethod + def send_sms(cls, device, token): + cls.sms_tokens[str(device.number)].append(token)