Skip to content

Commit

Permalink
Tests and CI
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronn committed Mar 26, 2017
1 parent 346d563 commit 4c92dc2
Show file tree
Hide file tree
Showing 25 changed files with 826 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.db
*~
.*
.idea/

html/
htmlcov/
Expand Down
46 changes: 46 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
python: '3.5'

language: python

sudo: false

env:
- TOX_ENV=py35-flake8

- TOX_ENV=py33-django1.8-drf3.1
- TOX_ENV=py33-django1.8-drf3.2
- TOX_ENV=py33-django1.8-drf3.3
- TOX_ENV=py33-django1.8-drf3.4
- TOX_ENV=py33-django1.8-drf3.5
- TOX_ENV=py33-django1.8-drf3.6

- TOX_ENV=py34-django1.8-drf3.1
- TOX_ENV=py34-django1.8-drf3.2
- TOX_ENV=py34-django1.8-drf3.3
- TOX_ENV=py34-django1.8-drf3.4
- TOX_ENV=py34-django1.8-drf3.5
- TOX_ENV=py34-django1.8-drf3.6

- TOX_ENV=py34-django1.9-drf3.1
- TOX_ENV=py34-django1.9-drf3.2
- TOX_ENV=py34-django1.9-drf3.3
- TOX_ENV=py34-django1.9-drf3.4
- TOX_ENV=py34-django1.9-drf3.5
- TOX_ENV=py34-django1.9-drf3.6

- TOX_ENV=py34-django1.10-drf3.4
- TOX_ENV=py34-django1.10-drf3.5
- TOX_ENV=py34-django1.10-drf3.6

- TOX_ENV=py35-django1.10-drf3.4
- TOX_ENV=py35-django1.10-drf3.5
- TOX_ENV=py35-django1.10-drf3.6

matrix:
fast_finish: true

install:
- pip install tox

script:
- tox -e $TOX_ENV
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Aaron Ng

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ The template renders a single variable `{{ callback_token }}` which is the 6 dig
## Contact Point Validation
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.

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`.
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`.

## Registration
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.
Expand All @@ -145,12 +145,12 @@ Here’s a full list of the configurable defaults.

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

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

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

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

# Registers previously unseen aliases as new users.
'PASSWORDLESS_REGISTER_NEW_USERS': True
'PASSWORDLESS_REGISTER_NEW_USERS': True,

# Suppresses actual SMS for testing
'PASSWORDLESS_TEST_SUPPRESSION': False
}


### Todo
- Support non-US mobile numbers
- Tests
- Custom URLs
- Change bad settings to 500's
2 changes: 1 addition & 1 deletion drfpasswordless/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ class DrfpasswordlessConfig(AppConfig):
name = 'drfpasswordless'

def ready(self):
import drfpasswordless.signals
import drfpasswordless.signals # NOQA
3 changes: 2 additions & 1 deletion drfpasswordless/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class TokenField(serializers.CharField):
'max_length': _('Tokens are {max_length} digits long.'),
'min_length': _('Tokens are {min_length} digits long.')}


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

if api_settings.PASSWORDLESS_REGISTER_NEW_USERS:
if api_settings.PASSWORDLESS_REGISTER_NEW_USERS is True:
# If new aliases should register new users.
user, created = User.objects.get_or_create(**{self.alias_type: alias})
else:
Expand Down
10 changes: 6 additions & 4 deletions drfpasswordless/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from django.conf import settings
from rest_framework.settings import APISettings

Expand All @@ -20,12 +19,12 @@

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

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

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

# Registers previously unseen aliases as new users.
'PASSWORDLESS_REGISTER_NEW_USERS': True
'PASSWORDLESS_REGISTER_NEW_USERS': True,

# Suppresses actual SMS for testing
'PASSWORDLESS_TEST_SUPPRESSION': False
}

# List of settings that may be in string import notation.
Expand Down
4 changes: 2 additions & 2 deletions drfpasswordless/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def update_alias_verification(sender, instance, **kwargs):
if instance.id:
user_old = User.objects.get(id=instance.id) # Pre-save object

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

if api_settings.PASSWORDLESS_USER_MARK_VERIFIED_MOBILE:
if api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED is True:
"""
For marking mobile aliases as not verified when a user changes it.
"""
Expand Down
1 change: 0 additions & 1 deletion drfpasswordless/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from rest_framework.urlpatterns import format_suffix_patterns
from .views import ObtainEmailCallbackToken, ObtainMobileCallbackToken, ObtainAuthTokenFromCallbackToken

# The URL a user posts a 6 digit token to to get their auth token.
urlpatterns = [url(r'^callback/auth/$', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'),
url(r'^auth/email/$', ObtainEmailCallbackToken.as_view(), name='auth_email'),
url(r'^auth/mobile/$', ObtainMobileCallbackToken.as_view(), name='auth_mobile')]
Expand Down
52 changes: 30 additions & 22 deletions drfpasswordless/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
from django.contrib.auth import get_user_model
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.template import loader
Expand All @@ -26,21 +25,23 @@ def authenticate_by_token(callback_token):
if token is not None:
# Our token becomes used now that it's passing through the authentication pipeline.
token.is_active = False
token.save()

if api_settings.PASSWORDLESS_USER_MARK_VERIFIED_EMAIL \
or api_settings.PASSWORDLESS_USER_MARK_VERIFIED_MOBILE:
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED \
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED:
# Mark this alias as verified
user = User.objects.get(pk=token.user.pk)
verify_user_alias(user, token)
user = verify_user_alias(User.objects.get(pk=token.user.pk), token)
token.user = user

# Returning a user designates a successful authentication.
token.save()
return token.user

except CallbackToken.DoesNotExist:
pass
log.debug("drfpasswordless: Challenged with a callback token that doesn't exist.")
except User.DoesNotExist:
log.debug("drfpasswordless: Authenticated user somehow doesn't exist.")
except PermissionDenied:
pass
log.debug("drfpasswordless: Permission denied while authenticating.")

return None

Expand Down Expand Up @@ -70,7 +71,7 @@ def validate_token_age(token):
"""
Returns True if a given token is within the age expiration limit.
"""
seconds = (timezone.now()-token.created_at).total_seconds()
seconds = (timezone.now() - token.created_at).total_seconds()
token_expiry_time = api_settings.PASSWORDLESS_TOKEN_EXPIRE_TIME

if seconds <= token_expiry_time:
Expand Down Expand Up @@ -98,7 +99,7 @@ def verify_user_alias(user, token):
return user


def send_email_with_callback_token(user, email_token):
def send_email_with_callback_token(self, user, email_token):
"""
Sends a SMS to user.mobile.
Expand All @@ -107,6 +108,8 @@ def send_email_with_callback_token(user, email_token):

try:
if api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS:
# Make sure we have a sending address before sending.

html_message = loader.render_to_string(
api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME,
{'callback_token': email_token.key, }
Expand All @@ -133,7 +136,7 @@ def send_email_with_callback_token(user, email_token):
return False


def send_sms_with_callback_token(user, mobile_token):
def send_sms_with_callback_token(self, user, mobile_token):
"""
Sends a SMS to user.mobile via Twilio.
Expand All @@ -142,19 +145,24 @@ def send_sms_with_callback_token(user, mobile_token):
base_string = api_settings.PASSWORDLESS_MOBILE_MESSAGE

try:
if hasattr(settings, 'TEST'):
# If TEST = True in settings, we assume success to prevent spamming SMS during testing.
if settings.TEST is True:

if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
# We need a sending number to send properly
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
# we assume success to prevent spamming SMS during testing.
return True

from twilio.rest import TwilioRestClient
twilio_client = TwilioRestClient(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
twilio_client.messages.create(
body=base_string % mobile_token.key,
to=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
from_=os.environ['PASSWORDLESS_MOBILE_NOREPLY_NUMBER']
)
return True
from twilio.rest import TwilioRestClient
twilio_client = TwilioRestClient(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
twilio_client.messages.create(
body=base_string % mobile_token.key,
to=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
from_=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER
)
return True
else:
log.debug("Failed to send login sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
return False
except ImportError:
log.debug("Couldn't import Twilio client. Is twilio installed?")
return False
Expand Down
2 changes: 1 addition & 1 deletion drfpasswordless/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def send_action(self):
raise NotImplementedError

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

Expand Down
3 changes: 3 additions & 0 deletions manifest.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include README.md LICENSE
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Laying these out as separate requirements files, allows us to
# only included the relevant sets when running tox, and ensures
# we are only ever declaring our dependencies in one place.

-r requirements/codestyle.txt
-r requirements/optionals.txt
-r requirements/packaging.txt
-r requirements/testing.txt
3 changes: 3 additions & 0 deletions requirements/codestyle.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# PEP8 code linting, which we run on all commits.
flake8==3.3.0
pep8==1.7.0
1 change: 1 addition & 0 deletions requirements/optionals.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
twilio==5.7.0
5 changes: 5 additions & 0 deletions requirements/packaging.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Wheel for PyPI installs.
wheel==0.24.0

# Twine for secured PyPI uploads.
twine==1.8.1
4 changes: 4 additions & 0 deletions requirements/testing.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# PyTest for running the tests.
pytest==2.6.4
pytest-django==2.8.0
pytest-cov==1.6
Loading

0 comments on commit 4c92dc2

Please sign in to comment.