Skip to content

Commit 4c92dc2

Browse files
committed
Tests and CI
1 parent 346d563 commit 4c92dc2

25 files changed

+826
-37
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.db
33
*~
44
.*
5+
.idea/
56

67
html/
78
htmlcov/

.travis.yml

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
python: '3.5'
2+
3+
language: python
4+
5+
sudo: false
6+
7+
env:
8+
- TOX_ENV=py35-flake8
9+
10+
- TOX_ENV=py33-django1.8-drf3.1
11+
- TOX_ENV=py33-django1.8-drf3.2
12+
- TOX_ENV=py33-django1.8-drf3.3
13+
- TOX_ENV=py33-django1.8-drf3.4
14+
- TOX_ENV=py33-django1.8-drf3.5
15+
- TOX_ENV=py33-django1.8-drf3.6
16+
17+
- TOX_ENV=py34-django1.8-drf3.1
18+
- TOX_ENV=py34-django1.8-drf3.2
19+
- TOX_ENV=py34-django1.8-drf3.3
20+
- TOX_ENV=py34-django1.8-drf3.4
21+
- TOX_ENV=py34-django1.8-drf3.5
22+
- TOX_ENV=py34-django1.8-drf3.6
23+
24+
- TOX_ENV=py34-django1.9-drf3.1
25+
- TOX_ENV=py34-django1.9-drf3.2
26+
- TOX_ENV=py34-django1.9-drf3.3
27+
- TOX_ENV=py34-django1.9-drf3.4
28+
- TOX_ENV=py34-django1.9-drf3.5
29+
- TOX_ENV=py34-django1.9-drf3.6
30+
31+
- TOX_ENV=py34-django1.10-drf3.4
32+
- TOX_ENV=py34-django1.10-drf3.5
33+
- TOX_ENV=py34-django1.10-drf3.6
34+
35+
- TOX_ENV=py35-django1.10-drf3.4
36+
- TOX_ENV=py35-django1.10-drf3.5
37+
- TOX_ENV=py35-django1.10-drf3.6
38+
39+
matrix:
40+
fast_finish: true
41+
42+
install:
43+
- pip install tox
44+
45+
script:
46+
- tox -e $TOX_ENV

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Aaron Ng
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ The template renders a single variable `{{ callback_token }}` which is the 6 dig
120120
## Contact Point Validation
121121
Endpoints can automatically mark themselves as validated when a user logs in with a token sent to a specific endpoint. They can also automatically mark themselves as invalid when a user changes a contact point.
122122

123-
This is off by default but can be turned on with `PASSWORDLESS_USER_MARK_VERIFIED_EMAIL` or `PASSWORDLESS_USER_MARK_VERIFIED_MOBILE`. By default when these are enabled they look for the User model fields `email_verified` or `mobile_verified`.
123+
This is off by default but can be turned on with `PASSWORDLESS_USER_MARK_EMAIL_VERIFIED` or `PASSWORDLESS_USER_MARK_MOBILE_VERIFIED`. By default when these are enabled they look for the User model fields `email_verified` or `mobile_verified`.
124124

125125
## Registration
126126
all unrecognized emails and mobile numbers create new accounts by default. New accounts are automatically set with `set_unusable_password()` but it’s recommended that admins have some type of password.
@@ -145,12 +145,12 @@ Here’s a full list of the configurable defaults.
145145

146146
# Marks itself as verified the first time a user completes auth via token.
147147
# Automatically unmarks itself if email is changed.
148-
'PASSWORDLESS_USER_MARK_VERIFIED_EMAIL': False,
148+
'PASSWORDLESS_USER_MARK_EMAIL_VERIFIED': False,
149149
'PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME': 'email_verified',
150150

151151
# Marks itself as verified the first time a user completes auth via token.
152152
# Automatically unmarks itself if mobile number is changed.
153-
'PASSWORDLESS_USER_MARK_VERIFIED_MOBILE': False,
153+
'PASSWORDLESS_USER_MARK_MOBILE_VERIFIED': False,
154154
'PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME': 'mobile_verified',
155155

156156
# The email the callback token is sent from
@@ -166,14 +166,18 @@ Here’s a full list of the configurable defaults.
166166
'PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME': "passwordless_default_token_email.html",
167167

168168
# The SMS sent to mobile users logging in. Takes one string.
169-
'PASSWORDLESS_MOBILE_MESSAGE': "Use this code to log in: %s"
169+
'PASSWORDLESS_MOBILE_MESSAGE': "Use this code to log in: %s",
170170

171171
# Registers previously unseen aliases as new users.
172-
'PASSWORDLESS_REGISTER_NEW_USERS': True
172+
'PASSWORDLESS_REGISTER_NEW_USERS': True,
173+
174+
# Suppresses actual SMS for testing
175+
'PASSWORDLESS_TEST_SUPPRESSION': False
173176
}
174177

175178

176179
### Todo
177180
- Support non-US mobile numbers
178181
- Tests
179182
- Custom URLs
183+
- Change bad settings to 500's

drfpasswordless/apps.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ class DrfpasswordlessConfig(AppConfig):
55
name = 'drfpasswordless'
66

77
def ready(self):
8-
import drfpasswordless.signals
8+
import drfpasswordless.signals # NOQA

drfpasswordless/serializers.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class TokenField(serializers.CharField):
2020
'max_length': _('Tokens are {max_length} digits long.'),
2121
'min_length': _('Tokens are {min_length} digits long.')}
2222

23+
2324
"""
2425
Serializers
2526
"""
@@ -43,7 +44,7 @@ def validate(self, attrs):
4344
# Return a token for them to log in
4445
# Consider moving this into somewhere else. Serializer should only serialize.
4546

46-
if api_settings.PASSWORDLESS_REGISTER_NEW_USERS:
47+
if api_settings.PASSWORDLESS_REGISTER_NEW_USERS is True:
4748
# If new aliases should register new users.
4849
user, created = User.objects.get_or_create(**{self.alias_type: alias})
4950
else:

drfpasswordless/settings.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import sys
21
from django.conf import settings
32
from rest_framework.settings import APISettings
43

@@ -20,12 +19,12 @@
2019

2120
# Marks itself as verified the first time a user completes auth via token.
2221
# Automatically unmarks itself if email is changed.
23-
'PASSWORDLESS_USER_MARK_VERIFIED_EMAIL': False,
22+
'PASSWORDLESS_USER_MARK_EMAIL_VERIFIED': False,
2423
'PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME': 'email_verified',
2524

2625
# Marks itself as verified the first time a user completes auth via token.
2726
# Automatically unmarks itself if mobile number is changed.
28-
'PASSWORDLESS_USER_MARK_VERIFIED_MOBILE': False,
27+
'PASSWORDLESS_USER_MARK_MOBILE_VERIFIED': False,
2928
'PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME': 'mobile_verified',
3029

3130
# The email the callback token is sent from
@@ -47,7 +46,10 @@
4746
'PASSWORDLESS_MOBILE_MESSAGE': "Use this code to log in: %s",
4847

4948
# Registers previously unseen aliases as new users.
50-
'PASSWORDLESS_REGISTER_NEW_USERS': True
49+
'PASSWORDLESS_REGISTER_NEW_USERS': True,
50+
51+
# Suppresses actual SMS for testing
52+
'PASSWORDLESS_TEST_SUPPRESSION': False
5153
}
5254

5355
# List of settings that may be in string import notation.

drfpasswordless/signals.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def update_alias_verification(sender, instance, **kwargs):
4444
if instance.id:
4545
user_old = User.objects.get(id=instance.id) # Pre-save object
4646

47-
if api_settings.PASSWORDLESS_USER_MARK_VERIFIED_EMAIL:
47+
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED is True:
4848
"""
4949
For marking email aliases as not verified when a user changes it.
5050
"""
@@ -65,7 +65,7 @@ def update_alias_verification(sender, instance, **kwargs):
6565
# User probably is just initially being created
6666
setattr(instance, email_verified_field, True)
6767

68-
if api_settings.PASSWORDLESS_USER_MARK_VERIFIED_MOBILE:
68+
if api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED is True:
6969
"""
7070
For marking mobile aliases as not verified when a user changes it.
7171
"""

drfpasswordless/urls.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from rest_framework.urlpatterns import format_suffix_patterns
33
from .views import ObtainEmailCallbackToken, ObtainMobileCallbackToken, ObtainAuthTokenFromCallbackToken
44

5-
# The URL a user posts a 6 digit token to to get their auth token.
65
urlpatterns = [url(r'^callback/auth/$', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'),
76
url(r'^auth/email/$', ObtainEmailCallbackToken.as_view(), name='auth_email'),
87
url(r'^auth/mobile/$', ObtainMobileCallbackToken.as_view(), name='auth_mobile')]

drfpasswordless/utils.py

+30-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
import os
33
from django.contrib.auth import get_user_model
4-
from django.conf import settings
54
from django.core.exceptions import PermissionDenied
65
from django.core.mail import send_mail
76
from django.template import loader
@@ -26,21 +25,23 @@ def authenticate_by_token(callback_token):
2625
if token is not None:
2726
# Our token becomes used now that it's passing through the authentication pipeline.
2827
token.is_active = False
29-
token.save()
3028

31-
if api_settings.PASSWORDLESS_USER_MARK_VERIFIED_EMAIL \
32-
or api_settings.PASSWORDLESS_USER_MARK_VERIFIED_MOBILE:
29+
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED \
30+
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED:
3331
# Mark this alias as verified
34-
user = User.objects.get(pk=token.user.pk)
35-
verify_user_alias(user, token)
32+
user = verify_user_alias(User.objects.get(pk=token.user.pk), token)
33+
token.user = user
3634

3735
# Returning a user designates a successful authentication.
36+
token.save()
3837
return token.user
3938

4039
except CallbackToken.DoesNotExist:
41-
pass
40+
log.debug("drfpasswordless: Challenged with a callback token that doesn't exist.")
41+
except User.DoesNotExist:
42+
log.debug("drfpasswordless: Authenticated user somehow doesn't exist.")
4243
except PermissionDenied:
43-
pass
44+
log.debug("drfpasswordless: Permission denied while authenticating.")
4445

4546
return None
4647

@@ -70,7 +71,7 @@ def validate_token_age(token):
7071
"""
7172
Returns True if a given token is within the age expiration limit.
7273
"""
73-
seconds = (timezone.now()-token.created_at).total_seconds()
74+
seconds = (timezone.now() - token.created_at).total_seconds()
7475
token_expiry_time = api_settings.PASSWORDLESS_TOKEN_EXPIRE_TIME
7576

7677
if seconds <= token_expiry_time:
@@ -98,7 +99,7 @@ def verify_user_alias(user, token):
9899
return user
99100

100101

101-
def send_email_with_callback_token(user, email_token):
102+
def send_email_with_callback_token(self, user, email_token):
102103
"""
103104
Sends a SMS to user.mobile.
104105
@@ -107,6 +108,8 @@ def send_email_with_callback_token(user, email_token):
107108

108109
try:
109110
if api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS:
111+
# Make sure we have a sending address before sending.
112+
110113
html_message = loader.render_to_string(
111114
api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME,
112115
{'callback_token': email_token.key, }
@@ -133,7 +136,7 @@ def send_email_with_callback_token(user, email_token):
133136
return False
134137

135138

136-
def send_sms_with_callback_token(user, mobile_token):
139+
def send_sms_with_callback_token(self, user, mobile_token):
137140
"""
138141
Sends a SMS to user.mobile via Twilio.
139142
@@ -142,19 +145,24 @@ def send_sms_with_callback_token(user, mobile_token):
142145
base_string = api_settings.PASSWORDLESS_MOBILE_MESSAGE
143146

144147
try:
145-
if hasattr(settings, 'TEST'):
146-
# If TEST = True in settings, we assume success to prevent spamming SMS during testing.
147-
if settings.TEST is True:
148+
149+
if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
150+
# We need a sending number to send properly
151+
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
152+
# we assume success to prevent spamming SMS during testing.
148153
return True
149154

150-
from twilio.rest import TwilioRestClient
151-
twilio_client = TwilioRestClient(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
152-
twilio_client.messages.create(
153-
body=base_string % mobile_token.key,
154-
to=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
155-
from_=os.environ['PASSWORDLESS_MOBILE_NOREPLY_NUMBER']
156-
)
157-
return True
155+
from twilio.rest import TwilioRestClient
156+
twilio_client = TwilioRestClient(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
157+
twilio_client.messages.create(
158+
body=base_string % mobile_token.key,
159+
to=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
160+
from_=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER
161+
)
162+
return True
163+
else:
164+
log.debug("Failed to send login sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
165+
return False
158166
except ImportError:
159167
log.debug("Couldn't import Twilio client. Is twilio installed?")
160168
return False

drfpasswordless/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def send_action(self):
3838
raise NotImplementedError
3939

4040
def post(self, request, *args, **kwargs):
41-
if self.alias_type not in api_settings.PASSWORDLESS_AUTH_TYPES:
41+
if self.alias_type.upper() not in api_settings.PASSWORDLESS_AUTH_TYPES:
4242
# Only allow auth types allowed in settings.
4343
return Response(status=status.HTTP_404_NOT_FOUND)
4444

manifest.in

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include README.md LICENSE
2+
recursive-exclude * __pycache__
3+
recursive-exclude * *.py[co]

requirements.txt

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Laying these out as separate requirements files, allows us to
2+
# only included the relevant sets when running tox, and ensures
3+
# we are only ever declaring our dependencies in one place.
4+
5+
-r requirements/codestyle.txt
6+
-r requirements/optionals.txt
7+
-r requirements/packaging.txt
8+
-r requirements/testing.txt

requirements/codestyle.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# PEP8 code linting, which we run on all commits.
2+
flake8==3.3.0
3+
pep8==1.7.0

requirements/optionals.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
twilio==5.7.0

requirements/packaging.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Wheel for PyPI installs.
2+
wheel==0.24.0
3+
4+
# Twine for secured PyPI uploads.
5+
twine==1.8.1

requirements/testing.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# PyTest for running the tests.
2+
pytest==2.6.4
3+
pytest-django==2.8.0
4+
pytest-cov==1.6

0 commit comments

Comments
 (0)