Permalink
Browse files

add in a single page app api (bug 957677)

  • Loading branch information...
1 parent 0d4d95c commit 4b2065661134b15ad6176dcbadb916ec2c8ac4d3 @andymckay andymckay committed Feb 12, 2014
View
@@ -27,6 +27,7 @@ Contents
use_hosted_webpay
developers
+ api
solitude_api
localization_testing
services
View
@@ -16,6 +16,7 @@ django-multidb-router==0.5
django-nose==1.1
django-paranoia==0.1.8.6
django-statsd-mozilla==0.3.9
+djangorestframework==2.3.9
fudge==1.0.3
funfactory==2.2.0
gelato.constants==0.1.3
View
@@ -57,4 +57,4 @@
CACHE_PREFIX = 'webpay:test'
-ENABLE_SPA = False
+ENABLE_SPA = True
View
@@ -0,0 +1,84 @@
+from django.http import Http404
+
+from lib.solitude.api import client
+
+from rest_framework import permissions, response, serializers, viewsets
+
+from webpay.auth.utils import set_user_has_pin
+from webpay.base.utils import app_error
+from webpay.pay.views import process_pay_req
+from webpay.pin.forms import CreatePinForm, ResetPinForm
+
+
+class Permission(permissions.IsAuthenticated):
+
+ def has_permission(self, request, view):
+ return bool(request.session.get('uuid'))
+
+
+class PinSerializer(serializers.Serializer):
+ pin = serializers.BooleanField()
+ pin_locked_out = serializers.DateTimeField()
+ pin_is_locked_out = serializers.BooleanField()
+ pin_was_locked_out = serializers.BooleanField()
+
+
+class Flag(object):
+
+ def dispatch(self, request, *args, **kwargs):
+ return super(Flag, self).dispatch(request, *args, **kwargs)
+
+
+class PinViewSet(Flag, viewsets.ViewSet):
+ permission_classes = (Permission,)
+ serializer_class = PinSerializer
+
+ def retrieve(self, request):
+ res = client.get_buyer(request.session['uuid'])
+ if not res:
+ raise Http404
+ serial = PinSerializer(res)
+ return response.Response(serial.data)
+
+ def create(self, request):
+ form = CreatePinForm(uuid=request.session['uuid'], data=request.DATA)
+ if form.is_valid():
+ if getattr(form, 'buyer_exists', None):
+ res = client.change_pin(form.uuid,
+ form.cleaned_data['pin'],
+ etag=form.buyer_etag)
+ else:
+ res = client.create_buyer(form.uuid, form.cleaned_data['pin'])
+
+ if form.handle_client_errors(res):
+ set_user_has_pin(request, True)
+ return response.Response(status=201)
+
+ return response.Response(status=201)
+
+ return app_error(request)
+
+ def update(self, request):
+ form = ResetPinForm(uuid=request.session['uuid'], data=request.DATA)
+
+ if not request.session.get('was_reverified', False):
+ return app_error(request)
+
+ if form.is_valid():
+ res = client.set_new_pin(form.uuid, form.cleaned_data['pin'])
+ if form.handle_client_errors(res):
+ request.session['was_reverified'] = False
+ return response.Response(status=204)
+
+ return app_error(request)
+
+
+class PayViewSet(Flag, viewsets.ViewSet):
+ permission_classes = (Permission,)
+
+ def create(self, request):
+ res = process_pay_req(request, request.DATA)
+ if res:
+ return res
+
+ return response.Response(status=204)
@@ -0,0 +1,192 @@
+import json
+
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+import mock
+from curling.lib import HttpClientError
+from nose import SkipTest
+from nose.tools import eq_, ok_
+
+from lib.marketplace.api import client as marketplace
+from lib.solitude import constants
+from lib.solitude.api import client as solitude
+from webpay.base.tests import BasicSessionCase
+from webpay.pay.tests import Base, sample
+
+
+class Response():
+
+ def __init__(self, code, content=''):
+ self.status_code = code
+ self.content = content
+
+
+class BaseCase(BasicSessionCase):
+
+ def setUp(self, *args, **kw):
+ super(BaseCase, self).setUp(*args, **kw)
+ self.set_session(uuid='a')
+
+ p = mock.patch.object(solitude, 'slumber', name='patched:solitude')
+ self.solitude = p.start()
+ self.addCleanup(p.stop)
+
+ m = mock.patch.object(marketplace, 'api', name='patched:market')
+ prices = mock.Mock()
+ prices.get_object.return_value = 1
+ self.marketplace = m.start()
+ self.marketplace.webpay.prices.return_value = prices
+ self.addCleanup(m.stop)
+
+ def set_session(self, **kwargs):
+ self.session.update(kwargs)
+ self.save_session()
+
+ def error(self, status):
+ error = HttpClientError
+ error.response = Response(404)
+ return error
+
+ def wrap(self, data):
+ return {'objects': [data]}
+
+
+class PIN(BaseCase):
+
+ def setUp(self, *args, **kw):
+ super(PIN, self).setUp(*args, **kw)
+ self.url = reverse('api:pin')
+
+
+class TestError(PIN):
+
+ def test_error(self):
+ # A test that the API returns JSON.
+ res = self.client.post(self.url, {}, HTTP_ACCEPT='application/json')
+ eq_(res.status_code, 400)
+ ok_('error_code', json.loads(res.content))
+
+
+class TestGet(PIN):
+
+ def test_anon(self):
+ self.set_session(uuid=None)
+ eq_(self.client.get(self.url).status_code, 403)
+
+ def test_no_pin(self):
+ self.solitude.generic.buyer.get.side_effect = self.error(404)
+ res = self.client.get(self.url)
+ eq_(res.status_code, 404)
+
+ def test_some_pin(self):
+ self.solitude.generic.buyer.get.return_value = self.wrap({'pin': True})
+ res = self.client.get(self.url)
+ self.solitude.generic.buyer.get.assert_called_with(headers={},
+ uuid='a')
+ eq_(json.loads(res.content)['pin'], True)
+
+
+class TestPost(PIN):
+
+ def test_anon(self):
+ self.set_session(uuid=None)
+ eq_(self.client.post(self.url, {}).status_code, 403)
+
+ def test_no_data(self):
+ res = self.client.post(self.url, {})
+ eq_(res.status_code, 400)
+
+ def test_no_user(self):
+ self.solitude.generic.buyer.get.side_effect = self.error(404)
+ res = self.client.post(self.url, {'pin': '1234'})
+ self.solitude.generic.buyer.post.assert_called_with({'uuid': 'a',
+ 'pin': '1234'})
+ eq_(res.status_code, 201)
+
+ def test_user(self):
+ self.solitude.generic.buyer.get.return_value = self.wrap(
+ {'pin': False, 'resource_pk': 'abc'})
+ res = self.client.post(self.url, {'pin': '1234'})
+ eq_(res.status_code, 201)
+ self.solitude.generic.buyer.assert_called_with(id='abc')
+
+ def test_user_with_pin(self):
+ self.solitude.generic.buyer.get.return_value = self.wrap(
+ {'pin': True, 'resource_pk': 'abc'})
+ res = self.client.post(self.url, {'pin': '1234'})
+ eq_(res.status_code, 400)
+
+
+class TestPatch(PIN):
+
+ def setUp(self):
+ super(TestPatch, self).setUp()
+ self.set_session(was_reverified=True)
+
+ def patch(self, url, data=None):
+ """
+ A wrapper around self.client.generic until we upgrade Django
+ and get the patch method in the test client.
+ """
+ data = data or {}
+ return self.client.generic('PATCH', url, data=json.dumps(data),
+ content_type='application/json')
+
+ def test_anon(self):
+ self.set_session(uuid=None)
+ eq_(self.patch(self.url, {}).status_code, 403)
+
+ def test_no_data(self):
+ res = self.patch(self.url, data={})
+ eq_(res.status_code, 400)
+
+ def test_no_user(self):
+ # TODO: it looks like the PIN flows doesn't take this into account.
+ raise SkipTest
+
+ def test_not_reverified(self):
+ self.set_session(was_reverified=False)
+ res = self.patch(self.url, data={})
+ eq_(res.status_code, 400)
+
+ def test_change(self):
+ self.solitude.generic.buyer.get.return_value = self.wrap(
+ {'pin': True, 'resource_pk': 'abc'})
+ res = self.patch(self.url, data={'pin': '1234'})
+ eq_(res.status_code, 204)
+ # TODO: figure out how to check that patch was called.
+ self.solitude.generic.buyer.assert_called_with(id='abc')
+ eq_(res.status_code, 204)
+
+ def test_reverified(self):
+ self.solitude.generic.buyer.get.return_value = self.wrap(
+ {'pin': True, 'resource_pk': 'abc'})
+ res = self.patch(self.url, data={'pin': '1234'})
+ eq_(res.status_code, 204)
+ # A cheap way to confirm that was_reverified was flipped.
+ res = self.patch(self.url, data={'pin': '1234'})
+ eq_(res.status_code, 400)
+
+
+# TODO: this could be made smaller.
+@mock.patch.object(settings, 'KEY', 'marketplace.mozilla.org')
+@mock.patch.object(settings, 'SECRET', 'marketplace.secret')
+@mock.patch.object(settings, 'ISSUER', 'marketplace.mozilla.org')
+@mock.patch.object(settings, 'INAPP_KEY_PATHS', {None: sample})
+@mock.patch.object(settings, 'DEBUG', True)
+class TestPay(Base, BaseCase):
+
+ def setUp(self):
+ super(TestPay, self).setUp()
+ self.url = reverse('api:pay')
+
+ def test_bad(self):
+ res = self.client.post(self.url, data={})
+ eq_(res.status_code, 400)
+
+ def test_inapp(self):
+ self.solitude.generic.product.get_object.return_value = {
+ 'secret': 'p.secret', 'access': constants.ACCESS_PURCHASE}
+ req = self.request()
+ eq_(self.client.post(self.url, data={'req': req}).status_code, 204)
View
@@ -0,0 +1,12 @@
+from django.conf.urls import patterns, url
+
+from api import PayViewSet, PinViewSet
+
+
+# Disable these API's on production until we are sure they are working well.
+urlpatterns = patterns('',
+ url('^pin/', PinViewSet.as_view({
+ 'get': 'retrieve', 'post': 'create', 'patch': 'update'}),
+ name='pin'),
+ url('^pay/', PayViewSet.as_view({'post': 'create'}), name='pay'),
+)
View
@@ -1,8 +1,10 @@
import calendar
import functools
+import json
import time
from django.conf import settings
+from django.http import HttpResponse
from django.shortcuts import render
from cef import log_cef as _log_cef
@@ -57,5 +59,9 @@ def system_error(request, **kw):
def custom_error(request, user_message, code=None, status=400):
- return render(request, 'error.html',
- {'error': user_message, 'error_code': code}, status=status)
+ error = {'error': user_message, 'error_code': code}
+ if request.META.get('HTTP_ACCEPT') == 'application/json':
+ return HttpResponse(content=json.dumps(error),
+ content_type='application/json; charset=utf-8',
+ status=status)
+ return render(request, 'error.html', error, status=status)
View
@@ -16,6 +16,10 @@
class VerifyForm(ParanoidForm):
req = forms.CharField()
+ # If mcc or mnc are given, we'll accept any value that conforms to the
+ # format. We'll then whitelist actions on particular values.
+ mcc = forms.RegexField(regex='^\d{3}$', required=False)
+ mnc = forms.RegexField(regex='^\d{3}$', required=False)
key = settings.KEY
secret = settings.SECRET
is_simulation = False
@@ -57,6 +57,18 @@ def test_broken(self):
def test_unicode(self):
self.failed(VerifyForm({'req': u'Հ'}))
+ def test_not_mcc(self):
+ form = VerifyForm({'mcc': 'fooo', 'mnc': '1'})
+ form.is_valid()
+ assert 'mcc' in form.errors
+ assert 'mnc' in form.errors
+
+ def test_mcc(self):
+ form = VerifyForm({'mcc': '123', 'mnc': '456'})
+ form.is_valid()
+ assert 'mcc' not in form.errors
+ assert 'mnc' not in form.errors
+
@mock.patch('lib.solitude.api.SolitudeAPI.get_active_product')
def test_non_existant(self, get_active_product):
get_active_product.side_effect = ObjectDoesNotExist
View
@@ -37,8 +37,9 @@
log = getLogger('w.pay')
-def process_pay_req(request):
- form = VerifyForm(request.GET)
+def process_pay_req(request, data=None):
+ data = request.GET if data is None else data
+ form = VerifyForm(data)
if not form.is_valid():
codes = []
for erlist in form.errors.values():
@@ -141,3 +141,5 @@
'logout': '/users/reset',
},
}
+
+ENABLE_SPA = True
Oops, something went wrong.

0 comments on commit 4b20656

Please sign in to comment.