Permalink
Browse files

Front end for PIN (bug 795115, bug 795118, bug 795120)

  • Loading branch information...
1 parent 0430204 commit 52b103ec33ca5c7bccfbd947e557603b583b8b79 @wraithan wraithan committed Oct 22, 2012
View
0 lib/solitude/__init__.py
No changes.
View
47 lib/solitude/api.py
@@ -0,0 +1,47 @@
+import json
+
+from django.conf import settings
+
+from slumber import API
+
+
+client = None
+
+
+class SolitudeAPI(object):
+
+ def __init__(self, url):
+ self.slumber = API(url)
+
+ def _buyer_from_response(self, res):
+ buyer = {}
+ if res.get('objects'):
+ buyer['id'] = res['objects'][0]['resource_pk']
+ buyer['pin'] = res['objects'][0]['pin']
+ buyer['uuid'] = res['objects'][0]['uuid']
+ elif res.get('resource_pk'):
+ buyer['id'] = res['resource_pk']
+ buyer['pin'] = res['pin']
+ buyer['uuid'] = res['uuid']
+ return buyer
+
+ def create_buyer(self, uuid, pin=None):
+ res = self.slumber.generic.buyer.post({'uuid': uuid, 'pin': pin})
+ return self._buyer_from_response(res)
+
+ def change_pin(self, buyer, pin):
+ buyer['pin'] = pin
+ return self.slumber.generic.buyer(id=buyer['id']).put(buyer)
+
+ def get_buyer(self, uuid):
+ res = self.slumber.generic.buyer.get(uuid=uuid)
+ return self._buyer_from_response(res)
+
+ def verify_pin(self, uuid, pin):
+ res = json.loads(self.slumber.buyer.check_pin.post({'uuid': uuid,
+ 'pin': pin}))
+ return res['valid']
+
+
+if not client:
+ client = SolitudeAPI(settings.SOLITUDE_URL)
View
57 lib/solitude/tests.py
@@ -0,0 +1,57 @@
+from django.conf import settings
+from django.test import TestCase
+
+from nose.exc import SkipTest
+from nose.tools import eq_
+
+from lib.solitude.api import client
+
+
+class SolitudeAPITest(TestCase):
+
+ def setUp(self):
+ self.uuid = 'dat:uuid'
+ self.pin = '1234'
+
+ @classmethod
+ def setUpClass(cls):
+ # TODO(Wraithan): Add a mocked backend so we have idempotent tests.
+ if getattr(settings, 'SOLITUDE_URL', None) is None:
+ raise SkipTest
+ client.create_buyer('dat:uuid', '1234')
+
+ def test_change_pin(self):
+ buyer = client.get_buyer(self.uuid)
+ assert client.change_pin(buyer, '4321')
+ assert client.verify_pin(self.uuid, '4321')
+ assert client.change_pin(buyer, self.pin)
+
+ def test_get_buyer(self):
+ buyer = client.get_buyer(self.uuid)
+ eq_(buyer.get('uuid'), self.uuid)
+ assert buyer.get('pin')
+ assert buyer.get('id')
+
+ def test_non_existent_get_buyer(self):
+ buyer = client.get_buyer('something that does not exist')
+ assert not buyer
+
+ def test_create_buyer_without_pin(self):
+ uuid = 'no_pin:1234'
+ buyer = client.create_buyer(uuid)
+ eq_(buyer.get('uuid'), uuid)
+ assert not buyer.get('pin')
+ assert buyer.get('id')
+
+ def test_create_buyer_with_pin(self):
+ uuid = 'with_pin:!234'
+ buyer = client.create_buyer(uuid, self.pin)
+ eq_(buyer.get('uuid'), uuid)
+ assert buyer.get('pin')
+ assert buyer.get('id')
+
+ def test_verify_pin(self):
+ assert client.verify_pin(self.uuid, self.pin)
+
+ def test_verify_pin(self):
+ assert not client.verify_pin(self.uuid, 'lame')
View
0 webpay/pin/__init__.py
No changes.
View
47 webpay/pin/forms.py
@@ -0,0 +1,47 @@
+from django import forms
+
+from tower import ugettext_lazy as _
+
+from lib.solitude.api import client
+
+
+class BasePinForm(forms.Form):
+
+ def __init__(self, uuid=None, *args, **kwargs):
+ self.uuid = uuid
+ super(BasePinForm, self).__init__(*args, **kwargs)
+
+
+class CreatePinForm(BasePinForm):
+ pin = forms.CharField(max_length=4, required=True)
+
+ def clean_pin(self, *args, **kwargs):
+ pin = self.cleaned_data['pin']
+ buyer = client.get_buyer(self.uuid)
+ if buyer:
+ self.buyer = buyer
+ if buyer.get('pin'):
+ raise forms.ValidationError(_('Buyer already has a PIN.'))
+ return pin
+
+
+class VerifyPinForm(BasePinForm):
+ pin = forms.CharField(max_length=4, required=True)
+
+ def clean_pin(self, *args, **kwargs):
+ pin = self.cleaned_data['pin']
+ if client.verify_pin(self.uuid, pin):
+ return pin
+
+ raise forms.ValidationError(_('Incorrect PIN.'))
+
+
+class ChangePinForm(BasePinForm):
+ old_pin = forms.CharField(max_length=4, required=True)
+ new_pin = forms.CharField(max_length=4, required=True)
+
+ def clean_old_pin(self, *args, **kwargs):
+ old_pin = self.cleaned_data['old_pin']
+ if client.verify_pin(self.uuid, old_pin):
+ return old_pin
+ raise forms.ValidationError(_('Incorrect PIN'))
View
0 webpay/pin/models.py
No changes.
View
25 webpay/pin/templates/pin/base.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html LANG="{{ LANG }}" dir="{{ DIR }}">
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <title>{% block page_title %}{{ _('Web Pay') }}{% endblock %}</title>
+
+ {% block site_css %}
+ {{ css('pin') }}
+ {% endblock %}
+
+ {% block extra_head %}
+ {% endblock %}
+
+ </head>
+ <body {% block body_attrs %}{% endblock %}>
+ {% block content %}{% endblock %}
+ {% block site_js %}
+ <script src="https://login.persona.org/include.js"></script>
+ <script src="{{ url('jsi18n')|urlparams(lang=LANG) }}"></script>
+ {{ js('pin') }}
+ {% endblock %}
+ </body>
+</html>
View
11 webpay/pin/templates/pin/change.html
@@ -0,0 +1,11 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Change your PIN:</p>
+ <p>
+ <form action="{{ url('pin_create') }}" method="post">
+ {{ form.as_p()}}
+ <input type="submit" value="Create">
+ </form>
+ </p>
+{% endblock %}
View
5 webpay/pin/templates/pin/change_success.html
@@ -0,0 +1,5 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Success</p>
+{% endblock %}
View
11 webpay/pin/templates/pin/create.html
@@ -0,0 +1,11 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Create you PIN:</p>
+ <p>
+ <form action="{{ url('pin_create') }}" method="post">
+ {{ form.as_p()}}
+ <input type="submit" value="Create">
+ </form>
+ </p>
+{% endblock %}
View
5 webpay/pin/templates/pin/create_success.html
@@ -0,0 +1,5 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Success</p>
+{% endblock %}
View
11 webpay/pin/templates/pin/verify.html
@@ -0,0 +1,11 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Enter your PIN:</p>
+ <p>
+ <form action="{{ url('pin_create') }}" method="post">
+ {{ form.as_p()}}
+ <input type="submit" value="Create">
+ </form>
+ </p>
+{% endblock %}
View
5 webpay/pin/templates/pin/verify_success.html
@@ -0,0 +1,5 @@
+{% extends "pay/base.html" %}
+
+{% block content %}
+ <p>Success</p>
+{% endblock %}
View
0 webpay/pin/tests/__init__.py
No changes.
View
84 webpay/pin/tests/test_forms.py
@@ -0,0 +1,84 @@
+from django.test import TestCase
+from mock import patch
+
+from lib.solitude.api import client
+from webpay.pin import forms
+
+
+class BasePinFormTestCase(TestCase):
+
+ def setUp(self):
+ self.uuid = 'dat:uuid'
+ self.data = {'pin': '1234'}
+
+
+class CreatePinFormTest(BasePinFormTestCase):
+
+ @patch.object(client, 'get_buyer', lambda x: {})
+ def test_new_user(self):
+ form = forms.CreatePinForm(uuid=self.uuid, data=self.data)
+ assert form.is_valid(), form.errors
+ assert not hasattr(form, 'buyer')
+
+ @patch.object(client, 'get_buyer', lambda x: {'uuid': x})
+ def test_existing_user(self):
+ form = forms.CreatePinForm(uuid=self.uuid, data=self.data)
+ assert form.is_valid(), form.errors
+ assert hasattr(form, 'buyer')
+
+ @patch.object(client, 'get_buyer', lambda x: {'uuid:': x, 'pin': 'fake'})
+ def test_has_pin(self):
+ form = forms.CreatePinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert hasattr(form, 'buyer')
+ assert 'Buyer already has a PIN' in str(form.errors)
+
+ def test_too_long_pin(self):
+ self.data.update({'pin': 'way too long pin'})
+ form = forms.CreatePinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert 'has at most 4' in str(form.errors['pin'])
+
+
+class VerifyPinFormTest(BasePinFormTestCase):
+
+ @patch.object(client, 'verify_pin', lambda x, y: True)
+ def test_correct_pin(self):
+ form = forms.VerifyPinForm(uuid=self.uuid, data=self.data)
+ assert form.is_valid()
+
+ @patch.object(client, 'verify_pin', lambda x, y: False)
+ def test_incorrect_pin(self):
+ form = forms.VerifyPinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert 'Incorrect PIN' in str(form.errors)
+
+ def test_too_long_pin(self):
+ self.data.update({'pin': 'way too long pin'})
+ form = forms.VerifyPinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert 'has at most 4' in str(form.errors['pin'])
+
+
+class ChangePinFormTest(BasePinFormTestCase):
+
+ def setUp(self):
+ super(ChangePinFormTest, self).setUp()
+ self.data = {'old_pin': 'old', 'new_pin': 'new'}
+
+ @patch.object(client, 'verify_pin', lambda x, y: True)
+ def test_correct_pin(self):
+ form = forms.ChangePinForm(uuid=self.uuid, data=self.data)
+ assert form.is_valid()
+
+ @patch.object(client, 'verify_pin', lambda x, y: False)
+ def test_incorrect_pin(self):
+ form = forms.ChangePinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert 'Incorrect PIN' in str(form.errors)
+
+ def test_too_long_new_pin(self):
+ self.data.update({'new_pin': 'way too long pin'})
+ form = forms.ChangePinForm(uuid=self.uuid, data=self.data)
+ assert not form.is_valid()
+ assert 'has at most 4' in str(form.errors['new_pin'])
View
78 webpay/pin/tests/test_views.py
@@ -0,0 +1,78 @@
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+from mock import patch
+
+from lib.solitude.api import client
+
+
+class PinViewTestCase(TestCase):
+ url_name = ''
+
+ def setUp(self):
+ self.url = reverse(self.url_name)
+
+
+class CreatePinViewTest(PinViewTestCase):
+ url_name = 'pin_create'
+
+ @patch('lib.solitude.api.client.create_buyer', auto_spec=True)
+ @patch('lib.solitude.api.client.change_pin', auto_spec=True)
+ @patch.object(client, 'get_buyer', lambda x: {})
+ def test_buyer_does_not_exist(self, change_pin, create_buyer):
+ res = self.client.post(self.url, data={'pin': '1234'})
+ assert create_buyer.called
+ assert not change_pin.called
+ assert 'Success' in res.content
+
+ @patch('lib.solitude.api.client.create_buyer', auto_spec=True)
+ @patch('lib.solitude.api.client.change_pin', auto_spec=True)
+ @patch.object(client, 'get_buyer', lambda x: {'uuid': 'some:uuid'})
+ def test_buyer_does_exist_with_no_pin(self, change_pin, create_buyer):
+ res = self.client.post(self.url, data={'pin': '1234'})
+ assert not create_buyer.called
+ assert change_pin.called
+ assert 'Success' in res.content
+
+ @patch('lib.solitude.api.client.create_buyer', auto_spec=True)
+ @patch('lib.solitude.api.client.change_pin', auto_spec=True)
+ @patch.object(client, 'get_buyer', lambda x: {'uuid': 'some:uuid',
+ 'pin': 'fake'})
+ def test_buyer_does_exist_with_pin(self, change_pin, create_buyer):
+ res = self.client.post(self.url, data={'pin': '1234'})
+ assert not create_buyer.called
+ assert not change_pin.called
+ assert not 'Success' in res.content
+
+
+class VerifyPinViewTest(PinViewTestCase):
+ url_name = 'pin_verify'
+
+ @patch.object(client, 'verify_pin', lambda x, y: True)
+ def test_good_pin(self):
+ res = self.client.post(self.url, data={'pin': '1234'})
+ assert 'Success' in res.content
+
+ @patch.object(client, 'verify_pin', lambda x, y: False)
+ def test_bad_pin(self):
+ res = self.client.post(self.url, data={'pin': '1234'})
+ assert not 'Success' in res.content
+
+
+class ChangePinViewTest(PinViewTestCase):
+ url_name = 'pin_change'
+
+ @patch('lib.solitude.api.client.change_pin', auto_spec=True)
+ @patch.object(client, 'verify_pin', lambda x, y: True)
+ def test_good_pin(self, change_pin):
+ res = self.client.post(self.url, data={'old_pin': '1234',
+ 'new_pin': '4321'})
+ assert change_pin.called
+ assert 'Success' in res.content
+
+ @patch('lib.solitude.api.client.change_pin', auto_spec=True)
+ @patch.object(client, 'verify_pin', lambda x, y: False)
+ def test_bad_pin(self, change_pin):
+ res = self.client.post(self.url, data={'old_pin': '1234',
+ 'new_pin': '4321'})
+ assert not change_pin.called
+ assert not 'Success' in res.content
View
10 webpay/pin/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import patterns, url
+
+from . import views
+
+
+urlpatterns = patterns('',
+ url(r'^create', views.create, name='pin_create'),
+ url(r'^verify', views.verify, name='pin_verify'),
+ url(r'^change', views.change, name='pin_change'),
+)
View
54 webpay/pin/views.py
@@ -0,0 +1,54 @@
+from django.shortcuts import render
+
+from session_csrf import anonymous_csrf_exempt
+
+from lib.solitude.api import client
+from . import forms
+
+
+# TODO(Wraithan): remove all the anonymous once identity is figured out.
+@anonymous_csrf_exempt
+def create(request):
+ form = forms.CreatePinForm()
+ if request.method == 'POST':
+ # TODO(Wraithan): Get the buyer's UUID once identity is figured out
+ # with webpay.
+ stub_uuid = 'dat:uuid'
+ form = forms.CreatePinForm(uuid=stub_uuid, data=request.POST)
+ if form.is_valid():
+ if hasattr(form, 'buyer'):
+ client.change_pin(form.buyer, form.cleaned_data['pin'])
+ else:
+ client.create_buyer(form.uuid, form.cleaned_data['pin'])
+ # TODO(Wraithan): Replace with proper redirect
+ return render(request, 'pin/create_success.html', {'form': form})
+ return render(request, 'pin/create.html', {'form': form})
+
+
+@anonymous_csrf_exempt
+def verify(request):
+ form = forms.VerifyPinForm()
+ if request.method == 'POST':
+ # TODO(Wraithan): Get the buyer's UUID once identity is figured out
+ # with webpay.
+ stub_uuid = 'dat:uuid'
+ form = forms.VerifyPinForm(uuid=stub_uuid, data=request.POST)
+ if form.is_valid():
+ # TODO(Wraithan): Replace with proper redirect
+ return render(request, 'pin/verify_success.html', {'form': form})
+ return render(request, 'pin/verify.html', {'form': form})
+
+
+@anonymous_csrf_exempt
+def change(request):
+ form = forms.ChangePinForm()
+ if request.method == 'POST':
+ # TODO(Wraithan): Get the buyer's UUID once identity is figured out
+ # with webpay.
+ stub_uuid = 'dat:uuid'
+ form = forms.ChangePinForm(uuid=stub_uuid, data=request.POST)
+ if form.is_valid():
+ client.change_pin(stub_uuid, form.cleaned_data['new_pin'])
+ # TODO(Wraithan): Replace with proper redirect
+ return render(request, 'pin/change_success.html', {'form': form})
+ return render(request, 'pin/change.html', {'form': form})
View
13 webpay/settings/base.py
@@ -12,6 +12,7 @@
INSTALLED_APPS = list(INSTALLED_APPS) + [
'webpay.base', # Needed for global templates, etc.
'webpay.pay',
+ 'webpay.pin',
'webpay.services',
'tower',
]
@@ -98,12 +99,12 @@
'tower.management.commands.extract.extract_tower_template'),
]
-HAS_SYSLOG = True # syslog is used if HAS_SYSLOG and NOT DEBUG.
+HAS_SYSLOG = True # syslog is used if HAS_SYSLOG and NOT DEBUG.
# See settings/local.py for SYSLOG_TAG, etc
-LOGGING = dict(loggers = dict(playdoh = {'level': logging.DEBUG},
- w = {'level': logging.INFO}),
- handlers = {'unicode': {'class':
- 'webpay.unicode_log.UnicodeHandler'}})
+LOGGING = dict(loggers=dict(playdoh={'level': logging.DEBUG},
+ w={'level': logging.INFO}),
+ handlers={'unicode': {'class':
+ 'webpay.unicode_log.UnicodeHandler'}})
MIDDLEWARE_CLASSES = (
@@ -112,7 +113,7 @@
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'session_csrf.CsrfMiddleware', # Must be after auth middleware.
+ 'session_csrf.CsrfMiddleware', # Must be after auth middleware.
'django.contrib.messages.middleware.MessageMiddleware',
'commonware.middleware.FrameOptionsHeader',
'mobility.middleware.DetectMobileMiddleware',
View
16 webpay/urls.py
@@ -17,14 +17,16 @@
url('^mozpay/jsi18n.js$',
cache_page(60 * 60 * 24 * 365)(javascript_catalog),
{'domain': 'javascript', 'packages': ['webpay']}, name='jsi18n'),
-
+ url(r'^mozpay/pin/', include('webpay.pin.urls')),
# This is served by marketplace.
- #(r'^robots\.txt$',
- # lambda r: HttpResponse(
- # "User-agent: *\n%s: /" % 'Allow' if settings.ENGAGE_ROBOTS else 'Disallow' ,
- # mimetype="text/plain"
- # )
- #),
+ # (r'^robots\.txt$',
+ # lambda r: HttpResponse(
+ # "User-agent: *\n%s: /" % (
+ # 'Allow' if settings.ENGAGE_ROBOTS else 'Disallow'
+ # ),
+ # mimetype="text/plain"
+ # )
+ # ),
# Uncomment the admin/doc line below to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),

1 comment on commit 52b103e

@wraithan

Pushed without finishing all the comments so other things can start moving on. Will have commits to fullfil the rest of wraithan@e33586a soon.

Please sign in to comment.