This repository has been archived by the owner. It is now read-only.
Permalink
Browse files

Merge branch 'master' of github.com:mozilla/webpay

  • Loading branch information...
potch committed Dec 17, 2012
2 parents 572f3d3 + c86f93d commit e20d3965ae54aeeba16a7cd33997c7bec7062882
View
@@ -57,6 +57,18 @@ def safe_run(self, command, *args, **kwargs):
return {'errors': res}
return res
+ def buyer_has_pin(self, uuid):
+ """Returns True if the existing buyer has a PIN.
+
+ :param uuid: String to identify the buyer by.
+ :rtype: boolean
+ """
+ res = self.safe_run(self.slumber.generic.buyer.get, **{'uuid': uuid})
+ if res['meta']['total_count'] == 0:
+ return False
+ else:
+ return res['objects'][0]['pin']
+
def create_buyer(self, uuid, pin=None):
"""Creates a buyer with an optional PIN in solitude.
@@ -138,20 +150,14 @@ def configure_product_for_billing(self, webpay_trans_id,
seller_product__external_id=product_id
)
if res['meta']['total_count'] == 0:
- # TODO(Kumar) create products on the fly. bug 820164
- raise NotImplementedError(
- 'this product does not exist and must be created')
-
- # Create the product on the fly.
- # This case exists for in-app purchases where the
- # seller is selling a new item for the first time.
-
+ bango_product_uri = self.create_product(product_id,
+ product_name, currency, amount, res['objects'][0])
else:
bango_product_uri = res['objects'][0]['resource_uri']
log.info('transaction %s: bango product: %s'
% (webpay_trans_id, bango_product_uri))
- res = self.slumber.bango('create-billing').post({
+ res = self.slumber.bango.billing.post({
'pageTitle': product_name,
'price_currency': currency,
'price_amount': str(amount),
@@ -162,6 +168,39 @@ def configure_product_for_billing(self, webpay_trans_id,
% (webpay_trans_id, bill_id))
return bill_id
+ def create_product(self, external_id, product_name, currency, amount, seller):
+ """
+ Creates a product and a Bango ID on the fly in solitude.
+ """
+ if not seller['bango']:
+ raise ValueError('No bango account set up for %s' %
+ seller['resource_pk'])
+
+ product = self.slumber.generic.product.post({
+ 'external_id': external_id,
+ 'seller': seller['bango']['seller']
+ })
+ bango = self.slumber.bango.product.post({
+ 'seller_bango': seller['bango']['resource_uri'],
+ 'seller_product': product['resource_uri'],
+ 'name': product_name,
+ 'categoryId': 1,
+ 'secret': 'n' # This is likely going to be removed.
+ })
+ self.slumber.bango.premium.post({
+ 'price': amount,
+ 'currencyIso': currency,
+ 'seller_product_bango': bango['resource_uri']
+ })
+
+ self.slumber.bango.rating.post({
+ 'rating': 'UNIVERSAL',
+ 'ratingScheme': 'GLOBAL',
+ 'seller_product_bango': bango['resource_uri']
+ })
+ return bango['resource_uri']
+
+
if getattr(settings, 'SOLITUDE_URL', False):
client = SolitudeAPI(settings.SOLITUDE_URL)
View
@@ -1,6 +1,7 @@
from django.conf import settings
from django.test import TestCase
+import mock
from nose.exc import SkipTest
from nose.tools import eq_
@@ -100,3 +101,24 @@ def test_verify_without_confirm_and_good_pin(self):
def test_verify_alpha_pin(self):
assert not client.verify_pin(self.uuid, 'lame')
+
+
+class CreateBangoTest(TestCase):
+ uuid = 'some:pin'
+
+ def test_create_no_bango(self):
+ with self.assertRaises(ValueError):
+ client.create_product('ext:id', None, None, None,
+ {'bango': None, 'resource_pk': 'foo'})
+
+ @mock.patch('lib.solitude.api.client.slumber')
+ def test_create_bango(self, slumber):
+ # Temporary mocking. Remove when this is mocked properly.
+ slumber.bango.generic.post.return_value = {'product': 'some:uri'}
+ slumber.bango.product.post.return_value = {'resource_uri': 'some:uri'}
+ assert client.create_product('ext:id', 'product:name', 'CAD', 1,
+ {'bango': {'seller': 's', 'resource_uri': 'r'},
+ 'resource_pk': 'foo'})
+ assert slumber.generic.product.post.called
+ assert slumber.bango.rating.post.called
+ assert slumber.bango.premium.post.called
View
@@ -32,11 +32,14 @@ $(function() {
$.post(verifyUrl, {assertion: assertion})
.success(function(data, textStatus, jqXHR) {
console.log('login success');
- $('.message').hide();
- $('#enter-pin').fadeIn();
- console.log($('#pin [name="pin"]')[0]);
- $('#pin [name="pin"]')[0].focus();
- console.log('foo');
+ if (!data.has_pin) {
+ window.location = data.pin_create;
+ } else {
+ $('.message').hide();
+ $('#enter-pin').fadeIn();
+ console.log($('#pin [name="pin"]')[0]);
+ $('#pin [name="pin"]')[0].focus();
+ }
})
.error(function() {
console.log('login error');
View
@@ -0,0 +1,32 @@
+$(function() {
+ "use strict";
+
+ var startUrl;
+ var timeout;
+
+ if ($('body').data('waitflow')) {
+ startUrl = $('body').data('trans-start-url');
+ poll();
+ }
+
+ function poll() {
+ $.get(startUrl)
+ .success(function(data, textStatus, jqXHR) {
+ if (data.url) {
+ if (timeout) {
+ window.clearTimeout(timeout);
+ }
+ window.location = data.url;
+ } else {
+ // The transaction is pending or it failed.
+ // TODO(Kumar) check for failed transactions here.
+ console.log('transaction state: ' + data.state)
+ timeout = window.setTimeout(poll, 1000);
+ }
+ })
+ .error(function() {
+ console.log('error checking transaction');
+ });
+ }
+
+});
View
@@ -1,3 +1,4 @@
+amqplib==1.0.2
Babel==0.9.6
Django==1.4.3
anyjson==0.3.3
@@ -10,6 +10,8 @@ UPDATE_REF = "origin/master"
UPDATE_VENDOR_REF = "origin/master"
WEB_HOSTGROUP="addons-dev"
+CELERY_HOSTGROUP = ''
+CELERY_SERVICE = ''
SSH_KEY="/root/keys/addons-updater"
View
@@ -41,6 +41,15 @@ def update_code(ctx, ref='origin/master'):
ctx.local("git submodule foreach 'git submodule sync --quiet'")
ctx.local("git submodule foreach 'git submodule update --init --recursive'")
+
+@task
+@hostgroups(settings.CELERY_HOSTGROUP, remote_kwargs={'ssh_key': settings.SSH_KEY})
+def update_celery(ctx):
+ ctx.remote(settings.REMOTE_UPDATE_SCRIPT)
+ if getattr(settings, 'CELERY_SERVICE', False):
+ ctx.remote("/sbin/service %s restart" % settings.CELERY_SERVICE)
+
+
@task
def compress_assets(ctx, arg=''):
with ctx.lcd(settings.SRC_DIR):
@@ -77,6 +86,7 @@ def deploy_app(ctx):
def deploy(ctx):
checkin_changes()
deploy_app()
+ update_celery()
@task
def pre_update(ctx, ref=settings.UPDATE_REF):
View
@@ -13,3 +13,4 @@
# We want to act as if we are hitting Solitude APIs even though it will
# be intercepted by mock objects.
FAKE_PAYMENTS = False
+FAKE_PAY_COMPLETE = FAKE_PAYMENTS
View
@@ -1,3 +1,5 @@
+import json
+
from django import test
from django.conf import settings
from django.core.urlresolvers import reverse
@@ -7,6 +9,10 @@
import mock
from nose.tools import eq_
+from webpay.auth.utils import client
+from webpay.auth import views as auth_views
+
+
good_assertion = {u'status': u'okay',
u'audience': u'http://some.site',
u'expires': 1351707833170,
@@ -48,6 +54,7 @@ def unverify(self):
del self.client.cookies[settings.SESSION_COOKIE_NAME]
+@mock.patch.object(client, 'buyer_has_pin', lambda *args: False)
@mock.patch.object(settings, 'DOMAIN', 'web.pay')
class TestAuth(SessionTestCase):
@@ -76,3 +83,41 @@ def test_session_cleaned(self, verify_assertion):
verify_assertion.return_value = False
eq_(self.client.post(self.url, {'assertion': 'bad'}).status_code, 400)
eq_(self.client.session.get('uuid'), None)
+
+
+@mock.patch.object(auth_views, 'verify_assertion', lambda *a: good_assertion)
+class TestBuyerHasPin(SessionTestCase):
+
+ def do_auth(self):
+ res = self.client.post(reverse('auth.verify'), {'assertion': 'good'})
+ eq_(res.status_code, 200, res)
+ return json.loads(res.content)
+
+ @mock.patch('lib.solitude.api.client.slumber')
+ def test_no_user(self, slumber):
+ slumber.generic.buyer.get.return_value = {
+ 'meta': {'total_count': 0}
+ }
+ data = self.do_auth()
+ eq_(self.client.session.get('uuid_has_pin'), False)
+ eq_(data['has_pin'], False)
+
+ @mock.patch('lib.solitude.api.client.slumber')
+ def test_user_no_pin(self, slumber):
+ slumber.generic.buyer.get.return_value = {
+ 'meta': {'total_count': 1},
+ 'objects': [{'pin': False}]
+ }
+ self.do_auth()
+ eq_(self.client.session.get('uuid_has_pin'), False)
+
+ @mock.patch('lib.solitude.api.client.slumber')
+ def test_user_with_pin(self, slumber):
+ slumber.generic.buyer.get.return_value = {
+ 'meta': {'total_count': 1},
+ 'objects': [{'pin': True}]
+ }
+ data = self.do_auth()
+ eq_(self.client.session.get('uuid_has_pin'), True)
+ eq_(data['has_pin'], True)
+ eq_(data['pin_create'], reverse('pin.create'))
View
@@ -2,6 +2,8 @@
from django.conf import settings
+from lib.solitude.api import client
+
def get_uuid(email):
"""
@@ -26,5 +28,12 @@ def get_user(request):
raise KeyError('Attempt to access user without it being set, '
'did you use the user_verified decorator?')
+
def set_user(request, email):
- request.session['uuid'] = get_uuid(email)
+ uuid = get_uuid(email)
+ request.session['uuid'] = uuid
+ set_user_has_pin(request, client.buyer_has_pin(uuid))
+
+
+def set_user_has_pin(request, has_pin):
+ request.session['uuid_has_pin'] = has_pin
View
@@ -1,18 +1,21 @@
from django import http
+from django.core.urlresolvers import reverse
from django.views.decorators.http import require_POST
import commonware.log
from django_browserid import get_audience, verify as verify_assertion
from django_browserid.forms import BrowserIDForm
from session_csrf import anonymous_csrf_exempt
+from webpay.base.decorators import json_view
from utils import set_user
log = commonware.log.getLogger('w.auth')
@anonymous_csrf_exempt
@require_POST
+@json_view
def verify(request):
form = BrowserIDForm(data=request.POST)
if form.is_valid():
@@ -22,7 +25,8 @@ def verify(request):
if result:
log.info('assertion ok: %s' % result)
set_user(request, result['email'])
- return http.HttpResponse('ok')
+ return {'has_pin': request.session['uuid_has_pin'],
+ 'pin_create': reverse('pin.create')}
request.session.clear()
return http.HttpResponseBadRequest()
View
@@ -0,0 +1,23 @@
+import functools
+import json
+
+from django import http
+
+
+def json_view(f=None, status_code=200):
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ response = func(*args, **kw)
+ if isinstance(response, http.HttpResponse):
+ return response
+ else:
+ return http.HttpResponse(
+ json.dumps(response),
+ content_type='application/json',
+ status=status_code)
+ return wrapper
+ if f:
+ return decorator(f)
+ else:
+ return decorator
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <div class="message">
+ <p>
+ Pretend this is a Bango payment screen for configuration ID {{ bill_config_id }}
+ </p>
+ <p>
+ <form action="{{ url('pay.complete') }}" method="post">
+ <input type="submit" value="Complete the payment">
+ </form>
+ </p>
+ </div>
+{% endblock %}
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% block body_attrs -%}
+ data-waitflow="true"
+ data-trans-start-url="{{ url('pay.trans_start_url') }}"
+{%- endblock %}
+
+{% block content %}
+ <div id="begin" class="message">
+ Waiting for payment to complete ...
+ </div>
+{% endblock %}
Oops, something went wrong.

0 comments on commit e20d396

Please sign in to comment.