diff --git a/combo/apps/lingo/templates/lingo/combo/payment-status.html b/combo/apps/lingo/templates/lingo/combo/payment-status.html new file mode 100644 index 00000000..d3621300 --- /dev/null +++ b/combo/apps/lingo/templates/lingo/combo/payment-status.html @@ -0,0 +1,53 @@ +{% extends "combo/page_template.html" %} +{% load staticfiles i18n %} + +{% block combo-content %} +{% block wait-js %} + +{% endblock %} + +{% block wait-content%} +
+ {% block wait-message %} +

{% trans "Please wait while your request is being processed..." %}

+ {% endblock %} + +

{% trans "Continue" %}

+

+
+{% endblock %} +{% endblock %} diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py index 902d8954..1a1bd31c 100644 --- a/combo/apps/lingo/urls.py +++ b/combo/apps/lingo/urls.py @@ -21,7 +21,8 @@ from combo.urls_utils import decorated_includes, manager_required from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView, ReturnView, ItemDownloadView, ItemView, CancelItemView, RemoveBasketItemApiView, ValidateTransactionApiView, - CancelTransactionApiView, SelfInvoiceView, BasketItemPayView) + CancelTransactionApiView, SelfInvoiceView, BasketItemPayView, + TransactionStatusApiView, PaymentStatusView) from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView, RegieDeleteView, TransactionListView, BasketItemErrorListView, download_transactions_csv, PaymentBackendListView, @@ -60,6 +61,10 @@ urlpatterns = [ name='api-validate-transaction'), url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(), name='api-cancel-transaction'), + url( + '^api/lingo/transaction-status/(?P.+)/$', TransactionStatusApiView.as_view(), + name='api-transaction-status' + ), url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'), url(r'^lingo/cancel/(?P\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'), url(r'^lingo/callback/(?P\w+)/$', CallbackView.as_view(), name='lingo-callback'), @@ -74,8 +79,10 @@ urlpatterns = [ ItemDownloadView.as_view(), name='download-item-pdf'), url(r'^lingo/item/(?P[\w,-]+)/(?P[\w,-]+)/$', ItemView.as_view(), name='view-item'), - url(r'^lingo/item/(?P\d+)/pay$', + url(r'^lingo/item/(?P.+)/pay$', BasketItemPayView.as_view(), name='basket-item-pay-view'), + url(r'^lingo/payment-status$', + PaymentStatusView.as_view(), name='payment-status'), url(r'^lingo/self-invoice/(?P\w+)/$', SelfInvoiceView.as_view(), name='lingo-self-invoice'), ] diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py index bb90d4b4..4656beab 100644 --- a/combo/apps/lingo/views.py +++ b/combo/apps/lingo/views.py @@ -23,11 +23,13 @@ import requests from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest +from django.core import signing +from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseBadRequest from django.http import HttpResponseForbidden, Http404, JsonResponse from django.template.response import TemplateResponse from django.utils import timezone, dateparse, six from django.utils.encoding import force_text +from django.utils.http import urlencode from django.views.decorators.csrf import csrf_exempt from django.views.generic import View, DetailView, ListView, TemplateView from django.conf import settings @@ -41,22 +43,28 @@ import eopayment from combo.data.models import Page from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError from combo.profile.utils import get_user_from_name_id +from combo.public.views import publish_page from .models import (Regie, BasketItem, Transaction, TransactionOperation, - LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend) + LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED) -def get_eopayment_object(request, regie_or_payment_backend): + +def get_eopayment_object(request, regie_or_payment_backend, transaction_id=None): payment_backend = regie_or_payment_backend if isinstance(regie_or_payment_backend, Regie): payment_backend = regie_or_payment_backend.payment_backend options = payment_backend.service_options + normal_return_url = reverse( + 'lingo-return-payment-backend', + kwargs={'payment_backend_pk': payment_backend.id} + ) + if transaction_id: + normal_return_url = "%s?lingo-transaction-id=%s" % (normal_return_url, signing.dumps(transaction_id)) options.update({ 'automatic_return_url': request.build_absolute_uri( reverse('lingo-callback-payment-backend', kwargs={'payment_backend_pk': payment_backend.id})), - 'normal_return_url': request.build_absolute_uri( - reverse('lingo-return-payment-backend', - kwargs={'payment_backend_pk': payment_backend.id})), + 'normal_return_url': request.build_absolute_uri(normal_return_url) }) return eopayment.Payment(payment_backend.service, options) @@ -150,7 +158,7 @@ class AddBasketItemApiView(View): elif request.GET.get('email'): user = User.objects.get(email=request.GET.get('email')) else: - raise Exception('no user specified') + user = None except User.DoesNotExist: raise Exception('unknown user') @@ -192,9 +200,17 @@ class AddBasketItemApiView(View): 'Bad format for capture date, it should be yyyy-mm-dd.') item.save() - item.regie.compute_extra_fees(user=item.user) + if user: + item.regie.compute_extra_fees(user=item.user) + else: + if item.regie.extra_fees_ws_url: + HttpResponseBadRequest('Can not compute extra fees with anonymous user.') - payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id}) + payment_url = reverse( + 'basket-item-pay-view', + kwargs={ + 'item_signature': signing.dumps(item.pk) + }) return JsonResponse({'result': 'success', 'id': str(item.id), 'payment_url': request.build_absolute_uri(payment_url)}) @@ -321,7 +337,10 @@ class CancelTransactionApiView(View): class PayMixin(object): @atomic - def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''): + def handle_payment( + self, request, regie, items, remote_items, next_url='/', email='', firstname='', + lastname=''): + if remote_items: total_amount = sum([x.amount for x in remote_items]) else: @@ -329,7 +348,7 @@ class PayMixin(object): if total_amount < regie.payment_min_amount: messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount) - return HttpResponseRedirect(next_url) + return HttpResponseRedirect(get_payment_status_view(next_url=items[0].source_url)) for item in items: if item.regie != regie: @@ -344,8 +363,6 @@ class PayMixin(object): lastname = user.last_name else: transaction.user = None - firstname = '' - lastname = '' transaction.save() transaction.regie = regie @@ -354,7 +371,7 @@ class PayMixin(object): transaction.status = 0 transaction.amount = total_amount - payment = get_eopayment_object(request, regie) + payment = get_eopayment_object(request, regie, transaction.pk) kwargs = { 'email': email, 'first_name': firstname, 'last_name': lastname } @@ -373,8 +390,9 @@ class PayMixin(object): # store the next url in session in order to be able to redirect to # it if payment is canceled - request.session.setdefault('lingo_next_url', - {})[order_id] = request.build_absolute_uri(next_url) + if next_url: + request.session.setdefault('lingo_next_url', + {})[str(transaction.pk)] = request.build_absolute_uri(next_url) request.session.modified = True if kind == eopayment.URL: @@ -433,21 +451,45 @@ class PayView(PayMixin, View): return self.handle_payment(request, regie, items, remote_items, next_url, email) -class BasketItemPayView(PayMixin, View): - def get(self, request, *args, **kwargs): - next_url = request.GET.get('next_url') or '/' - if not (request.user and request.user.is_authenticated): - return HttpResponseForbidden(_('No item payment allowed for anonymous users.')) +def get_payment_status_view(transaction_id=None, next_url=None): + url = reverse('payment-status') + params = [] + if transaction_id: + params.append(('transaction-id', signing.dumps(transaction_id))) + if next_url: + params.append(('next', next_url)) + return "%s?%s" % (url, urlencode(params)) - item = BasketItem.objects.get(pk=kwargs['item_id']) + +class BasketItemPayView(PayMixin, View): + + def get(self, request, *args, **kwargs): + next_url = request.GET.get('next_url') + email = request.GET.get('email', '') + firstname = request.GET.get('firstname', '') + lastname = request.GET.get('lastname', '') + + item_signature = kwargs.get('item_signature') + try: + item_id = signing.loads(item_signature) + except signing.BadSignature: + return HttpResponseForbidden(_('Invalid payment request.')) + + item = BasketItem.objects.get(pk=item_id) regie = item.regie if regie.extra_fees_ws_url: return HttpResponseForbidden(_('No item payment allowed as extra fees set.')) - if item.user != request.user: + if item.user and item.user != request.user: return HttpResponseForbidden(_('Wrong item: payment not allowed.')) - return self.handle_payment(request, regie, [item], [], next_url) + if not next_url: + next_url = item.source_url + + return self.handle_payment( + request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email, + firstname=firstname, lastname=lastname + ) class PaymentException(Exception): @@ -600,25 +642,34 @@ class ReturnView(PaymentView): def handle_return(self, request, backend_response, **kwargs): transaction = None + transaction_id = request.GET.get('lingo-transaction-id') + if transaction_id: + try: + transaction_id = signing.loads(transaction_id) + except signing.BadSignature: + pass try: transaction = self.handle_response(request, backend_response, **kwargs) except UnsignedPaymentException as e: # some payment backends do not sign return URLs, don't mark this as # an error, they will provide a notification to the callback # endpoint. - pass + if transaction_id: + return HttpResponseRedirect(get_payment_status_view(transaction_id)) + return HttpResponseRedirect(get_basket_url()) + except PaymentException as e: messages.error(request, _('We are sorry but the payment service ' 'failed to provide a correct answer.')) + if transaction_id: + return HttpResponseRedirect(get_payment_status_view(transaction_id)) return HttpResponseRedirect(get_basket_url()) if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED): messages.info(request, transaction.regie.get_text_on_success()) - if transaction and request.session.get('lingo_next_url'): - redirect_url = request.session['lingo_next_url'].get(transaction.order_id) - if redirect_url: - return HttpResponseRedirect(redirect_url) + if transaction: + return HttpResponseRedirect(get_payment_status_view(transaction.pk)) # return to basket page if there are still items to pay if request.user.is_authenticated: @@ -765,3 +816,97 @@ class SelfInvoiceView(View): return HttpResponseRedirect(url) messages.warning(request, msg) return HttpResponseRedirect(request.GET.get('page_path') or '/') + + +class PaymentStatusView(View): + + http_method_names = ['get'] + + def get(self, request, *args, **kwargs): + page = Page() + page.template_name = 'standard' + template_name = 'lingo/combo/payment-status.html' + + extra_context_data = getattr(request, 'extra_context_data', {}) + extra_context_data['transaction_id'] = '' + + transaction_id = request.GET.get('transaction-id') + + if not transaction_id: + next_url = request.GET.get('next') + if not next_url: + next_url = '/' + extra_context_data['next_url'] = request.build_absolute_uri(next_url) + request.extra_context_data = extra_context_data + return publish_page(request, page, template_name=template_name) + + try: + transaction_id = signing.loads(transaction_id) + except signing.BadSignature: + return HttpResponseForbidden(_('Invalid transaction signature.')) + + try: + transaction = Transaction.objects.get(pk=transaction_id) + except Transaction.DoesNotExist: + return HttpResponseForbidden(_('Invalid transaction.')) + + next_url = request.session.get('lingo_next_url', {}).get(str(transaction_id)) + if not next_url: + next_url = get_basket_url() + if len(set([item.source_url for item in transaction.items.all()])) == 1: + next_url = transaction.items.first().source_url + next_url = request.build_absolute_uri(next_url) + + extra_context_data['transaction_id'] = signing.dumps(transaction.pk) + extra_context_data['next_url'] = next_url + request.extra_context_data = extra_context_data + return publish_page(request, page, template_name=template_name) + + +class TransactionStatusApiView(View): + + http_method_names = ['get'] + + def get(self, request, *args, **kwargs): + transaction_signature = kwargs.get('transaction_signature') + try: + transaction_id = signing.loads(transaction_signature) + except signing.BadSignature: + return HttpResponseBadRequest(_('Invalid transaction.')) + + try: + transaction = Transaction.objects.get(pk=transaction_id) + except (Transaction.DoesNotExist, ValueError): + return HttpResponseNotFound(_('Unknown transaction.')) + + user = request.user if request.user.is_authenticated() else None + error_msg = _('Transaction does not belong to the requesting user') + if user and transaction.user and user != transaction.user: + return HttpResponseForbidden(error_msg) + if not user and transaction.user: + return HttpResponseForbidden(error_msg) + + if transaction.is_paid(): + data = { + 'wait': False, + 'error': False, + 'error_msg': '' + } + return JsonResponse(data=data) + + if transaction.status in ( + eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED + ): + data = { + 'wait': True, + 'error': True, + 'error_msg': _('Payment error, you can continue and make another payment') + } + return JsonResponse(data=data) + + data = { + 'wait': True, + 'error': False, + 'error_msg': '' + } + return JsonResponse(data=data) diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py index 0a0e1529..09da44f2 100644 --- a/tests/test_lingo_payment.py +++ b/tests/test_lingo_payment.py @@ -9,11 +9,13 @@ import mock from django.apps import apps from django.contrib.auth.models import User +from django.core import signing from django.core.urlresolvers import reverse from django.core.wsgi import get_wsgi_application from django.conf import settings from django.test import override_settings from django.utils import timezone +from django.utils.http import urlencode from django.utils.six.moves.urllib import parse as urlparse from django.contrib.messages.storage.session import SessionStorage from webtest import TestApp @@ -22,7 +24,7 @@ from combo.data.models import Page from combo.apps.lingo.models import ( Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell, PaymentBackend) -from combo.utils import sign_url +from combo.utils import aes_hex_decrypt, sign_url from .test_manager import login @@ -120,6 +122,20 @@ def key(request, settings): else: return settings.LINGO_API_SIGN_KEY + +def assert_payment_status(url, transaction_id=None): + if hasattr(url, 'path'): + url = url.path + + if transaction_id: + url, part = url.split('?') + query = urlparse.parse_qs(part) + assert 'transaction-id' in query + assert signing.loads(query['transaction-id'][0]) == transaction_id + + assert url.startswith('/lingo/payment-status') + + def test_default_regie(): payment_backend = PaymentBackend.objects.create(label='foo', slug='foo') Regie.objects.all().delete() @@ -218,10 +234,14 @@ def test_successfull_items_payment(app, basket_page, regie, user, with_payment_b assert resp.status_code == 200 # simulate successful return URL resp = app.get(qs['return_url'][0], params=args) + # redirect to payment status assert resp.status_code == 302 - assert urlparse.urlparse(resp.url).path == '/test_basket_cell/' + assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') resp = resp.follow() assert 'Your payment has been succesfully registered.' in resp.text + assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ + '/test_basket_cell/' + def test_add_amount_to_basket(app, key, regie, user): payment_backend = PaymentBackend.objects.create( @@ -297,7 +317,9 @@ def test_add_amount_to_basket(app, key, regie, user): assert resp.status_code == 200 response = json.loads(resp.text) assert response['result'] == 'success' - assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id) + payment_url = urlparse.urlparse(response['payment_url']) + assert payment_url.path.startswith('/lingo/item/') + assert payment_url.path.endswith('/pay') assert BasketItem.objects.filter(amount=Decimal('22.23')).exists() assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id @@ -417,7 +439,7 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe): assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists() payment_url = resp.json['payment_url'] resp = app.get(payment_url, status=403) - assert 'No item payment allowed for anonymous users.' in resp.text + assert 'Wrong item: payment not allowed.' in resp.text login(app, username='john.doe', password='john.doe') resp = app.get(payment_url, status=403) @@ -440,7 +462,6 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe): assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) assert qs['amount'] == ['12.00'] - # simulate successful payment response from dummy backend data = {'transaction_id': qs['transaction_id'][0], 'ok': True, 'amount': qs['amount'][0], 'signed': True} @@ -448,9 +469,10 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe): # dummy module put that URL in return_url query string parameter). resp = app.get(qs['return_url'][0], params=data) # check that item is paid - assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).exists() - # check that user is redirected to the next_url passed previously - assert resp.location == 'http://example.net/form/id/' + item = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first() + # check that user is redirected to the item payment status view + assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk) + def test_pay_multiple_regies(app, key, regie, user): test_add_amount_to_basket(app, key, regie, user) @@ -743,6 +765,8 @@ def test_payment_no_callback_just_return( data = {'transaction_id': transaction_id, 'amount': qs['amount'][0], 'ok': True} assert data['amount'] == '10.50' + return_qs = urlparse.parse_qs(urlparse.urlparse(qs['return_url'][0]).query) + lingo_transaction_id = return_qs['lingo-transaction-id'][0] # call return with unsigned POST with check_log(caplog, 'received unsigned payment'): @@ -768,17 +792,22 @@ def test_payment_no_callback_just_return( # call return with signed POST data['signed'] = True - return_url = get_url(with_payment_backend, 'lingo-return', regie) + return_url = get_url(with_payment_backend, 'lingo-return', regie) + \ + '?lingo-transaction-id=%s' % lingo_transaction_id with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: get_resp = app.post(return_url, params=data) url = request.call_args[0][1] assert url.startswith('http://example.org/testitem/jump/trigger/paid') + # redirect to payment status assert get_resp.status_code == 302 - assert urlparse.urlparse(get_resp['location']).path == '/test_basket_cell/' - resp = app.get(get_resp['Location']) + assert urlparse.urlparse(get_resp.url).path.startswith('/lingo/payment-status') + resp = get_resp.follow() assert 'Your payment has been succesfully registered.' in resp.text + assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ + '/test_basket_cell/' assert Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID + def test_transaction_expiration(): t1 = Transaction(status=0) t1.save() @@ -995,3 +1024,207 @@ def test_payment_callback_error(app, basket_page, regie, user, with_payment_back assert url.startswith('http://example.org/testitem/jump/trigger/paid') assert BasketItem.objects.get(id=item.id).payment_date assert BasketItem.objects.get(id=item.id).notification_date + + +@pytest.mark.parametrize("authenticated", [True, False]) +def test_payment_no_basket(app, user, regie, authenticated): + url = reverse('api-add-basket-item') + source_url = 'http://example.org/item/1' + data = {'amount': 10, 'display_name': 'test item', 'url': source_url} + if authenticated: + data['email'] = user.email + url = sign_url(url, settings.LINGO_API_SIGN_KEY) + resp = app.post_json(url, params=data) + assert resp.status_code == 200 + payment_url = resp.json['payment_url'] + + item = BasketItem.objects.first() + assert item.user is None + assert item.amount == Decimal('10.00') + path = urlparse.urlparse(payment_url).path + start = '/lingo/item/' + end = '/pay' + assert path.startswith(start) + assert path.endswith(end) + signature = path.replace(start, '').replace(end, '') + assert signing.loads(signature) == item.id + + if authenticated: + app = login(app) + + # payment error due to too small amount + item.amount = Decimal('1.00') + item.save() + resp = app.get(payment_url) + assert_payment_status(resp.location) + resp = resp.follow() + assert 'Minimal payment amount is 4.50' in resp.text + # we can go back to form + assert source_url in resp.text + + # amount ok, redirection to payment backend + item.amount = Decimal('10.00') + item.save() + resp = app.get(payment_url) + assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') + qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) + assert qs['amount'] == ['10.00'] + if authenticated: + assert qs['email'] == ['foo@example.com'] + else: + assert 'email' not in qs + + # mail can be specified here for anonymous user + resp = app.get( + payment_url, + params={ + 'email': 'foo@localhost', + } + ) + assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') + qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) + assert qs['amount'] == ['10.00'] + if authenticated: + assert qs['email'] == ['foo@example.com'] + else: + assert qs['email'] == ['foo@localhost'] + + # simulate bad responseform payment backend, no transaction id + data = {'amount': qs['amount'][0], 'signed': True} + return_url = qs['return_url'][0] + assert 'lingo-transaction-id' in return_url + resp = app.get(return_url, params=data) + assert_payment_status(resp.location) + resp = resp.follow() + assert 'We are sorry but the payment service failed to provide a correct answer.' in resp.text + assert 'http://example.org/item/1' in resp.text + # check that item is not paid + item = BasketItem.objects.get(pk=item.pk) + assert not item.payment_date + + # simulate successful payment response from dummy backend + data = { + 'transaction_id': qs['transaction_id'][0], 'ok': True, + 'amount': qs['amount'][0], 'signed': True + } + return_url = qs['return_url'][0] + assert 'lingo-transaction-id' in return_url + resp = app.get(return_url, params=data) + assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk) + # check that item is paid + item = BasketItem.objects.get(pk=item.pk) + assert item.payment_date + # accept redirection to item payment status view + resp = resp.follow() + # which should it self redirect to item.source_url as it is paid + assert 'Please wait while your request is being processed' in resp.text + assert source_url in resp.text + + +def test_transaction_status_api(app, regie, user): + # invalid transaction signature + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps('xxxx')} + + ) + resp = app.get(url, status=404) + assert 'Unknown transaction.' in resp.text + + # unkown transaction identifier + transaction_id = 1000 + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction_id)} + ) + resp = app.get(url, status=404) + assert 'Unknown transaction.' in resp.text + + wait_response = { + 'wait': True, + 'error': False, + 'error_msg': '' + } + # anonymous user on anonymous transaction: OK + transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = app.get(url) + assert resp.json == wait_response + + # authenticated user on anonymous transaction: OK + transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = login(app).get(url) + assert resp.json == wait_response + app.reset() + + # authenticated user on his transaction: OK + transaction = Transaction.objects.create( + amount=Decimal('10.0'), regie=regie, status=0, user=user) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = login(app).get(url) + assert resp.json == wait_response + app.reset() + + error_msg = 'Transaction does not belong to the requesting user' + # anonymous user on other user transaction transaction: NOTOK + transaction = Transaction.objects.create( + amount=Decimal('10.0'), regie=regie, status=0, user=user) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = app.get(url, status=403) + assert error_msg in resp.text + + # authenticated user on other user transaction transaction: NOTOK + user2 = User.objects.create_user( + 'user2', password='user2', email='user2@example.com' + ) + transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0, user=user2) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = login(app).get(url, status=403) + assert error_msg in resp.text + app.reset() + + # transaction error + transaction = Transaction.objects.create( + amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR + ) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = app.get(url) + assert resp.json == { + 'wait': True, + 'error': True, + 'error_msg': 'Payment error, you can continue and make another payment' + } + + # transaction paid + transaction = Transaction.objects.create( + amount=Decimal('10.0'), regie=regie, status=eopayment.PAID + ) + url = reverse( + 'api-transaction-status', + kwargs={'transaction_signature': signing.dumps(transaction.pk)} + ) + resp = app.get(url) + assert resp.json == { + 'wait': False, + 'error': False, + 'error_msg': '' + } diff --git a/tests/test_lingo_remote_regie.py b/tests/test_lingo_remote_regie.py index d4e2c085..3cd6b6f3 100644 --- a/tests/test_lingo_remote_regie.py +++ b/tests/test_lingo_remote_regie.py @@ -211,8 +211,11 @@ def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remo kwargs={'payment_backend_pk': remote_regie.payment_backend.id})) # simulate successful return URL resp = app.get(qs['return_url'][0], params=args) + # redirect to payment status assert resp.status_code == 302 - assert urlparse.urlparse(resp.url).path == '/' + assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') + resp = resp.follow() + assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == '/' # simulate successful call to callback URL resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args) trans = Transaction.objects.all() @@ -335,8 +338,13 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app, # simulate payment failure mock_get.side_effect = ConnectionError('where is my hostname?') resp = app.get(qs['return_url'][0], params=args) + # redirect to payment status assert resp.status_code == 302 - assert urlparse.urlparse(resp.url).path == '/active-remote-invoices-page/' + assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') + resp = resp.follow() + assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ + '/active-remote-invoices-page/' + # simulate successful call to callback URL resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args) trans = Transaction.objects.all() @@ -383,10 +391,13 @@ def test_remote_invoice_successfull_payment_redirect(mock_get, mock_pay_invoice, qs = urlparse.parse_qs(parsed.query) args = {'transaction_id': qs['transaction_id'][0], 'signed': True, 'ok': True, 'reason': 'Paid'} - resp = app.get(qs['return_url'][0], params=args) + # redirect to payment status assert resp.status_code == 302 - assert urlparse.urlparse(resp.location).path == '/active-remote-invoices-page/' + assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') + resp = resp.follow() + assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ + '/active-remote-invoices-page/' @mock.patch('combo.apps.lingo.models.UserSAMLIdentifier')