From f148cd62be654e4c06d35be8ac966448e61bb6df Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Mon, 13 May 2019 15:37:47 +0200
Subject: [PATCH 3/4] lingo: make payment related views compliant with
 PaymentBackend (#32441)

---
 combo/apps/lingo/urls.py         |  4 ++
 combo/apps/lingo/views.py        | 34 +++++++++-----
 tests/test_lingo_payment.py      | 80 ++++++++++++++++++++++----------
 tests/test_lingo_remote_regie.py | 16 +++++--
 4 files changed, 94 insertions(+), 40 deletions(-)

diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py
index eede96a..bc5aba4 100644
--- a/combo/apps/lingo/urls.py
+++ b/combo/apps/lingo/urls.py
@@ -49,7 +49,11 @@ urlpatterns = [
     url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
     url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
     url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
+    url(r'^lingo/callback-payment-backend/(?P<payment_backend_pk>\w+)/$',
+        CallbackView.as_view(), name='lingo-callback-payment-backend'),
     url(r'^lingo/return/(?P<regie_pk>\w+)/$', ReturnView.as_view(), name='lingo-return'),
+    url(r'^lingo/return-payment-backend/(?P<payment_backend_pk>\w+)/$',
+        ReturnView.as_view(), name='lingo-return-payment-backend'),
     url(r'^manage/lingo/', decorated_includes(manager_required,
         include(lingo_manager_urls))),
     url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/pdf$',
diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py
index dde0690..cb4f089 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -43,17 +43,22 @@ from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionErro
 from combo.profile.utils import get_user_from_name_id
 
 from .models import (Regie, BasketItem, Transaction, TransactionOperation,
-        LingoBasketCell, SelfDeclaredInvoicePayment)
+                     LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend)
 
-def get_eopayment_object(request, regie):
-    options = regie.service_options
+def get_eopayment_object(request, regie_or_payment_backend):
+    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
     options.update({
         'automatic_return_url': request.build_absolute_uri(
-                reverse('lingo-callback', kwargs={'regie_pk': regie.id})),
+                reverse('lingo-callback-payment-backend',
+                        kwargs={'payment_backend_pk': payment_backend.id})),
         'normal_return_url': request.build_absolute_uri(
-                reverse('lingo-return', kwargs={'regie_pk': regie.id})),
+                reverse('lingo-return-payment-backend',
+                        kwargs={'payment_backend_pk': payment_backend.id})),
     })
-    return eopayment.Payment(regie.service, options)
+    return eopayment.Payment(payment_backend.service, options)
 
 
 def get_basket_url():
@@ -434,8 +439,14 @@ class UnknownPaymentException(PaymentException):
 
 class PaymentView(View):
     def handle_response(self, request, backend_response, **kwargs):
-        regie = Regie.objects.get(id=kwargs.get('regie_pk'))
-        payment = get_eopayment_object(request, regie)
+        if 'regie_pk' in kwargs:
+            payment_backend = Regie.objects.get(id=kwargs['regie_pk']).payment_backend
+        elif 'payment_backend_pk' in kwargs:
+            payment_backend = PaymentBackend.objects.get(id=kwargs['payment_backend_pk'])
+        else:
+            raise Exception("A payment backend or regie primary key must be specified")
+
+        payment = get_eopayment_object(request, payment_backend)
         logger = logging.getLogger(__name__)
         logger.info(u'received payment response: %r', backend_response)
         try:
@@ -484,9 +495,10 @@ class PaymentView(View):
                         transaction.status, payment_response.result))
 
         # check if transaction belongs to right regie
-        if not transaction.regie == regie:
-            logger.warning(u'received payment for inappropriate regie '
-                            '(expecteds: %s, received: %s)' % (transaction.regie, regie))
+        if not transaction.regie.payment_backend == payment_backend:
+            logger.warning(u'received payment for inappropriate payment backend '
+                            '(expecteds: %s, received: %s)' % (
+                                transaction.regie.payment_backend, payment_backend))
             raise PaymentException('Invalid payment regie')
 
         transaction.status = payment_response.result
diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py
index ac1d4f5..45666ce 100644
--- a/tests/test_lingo_payment.py
+++ b/tests/test_lingo_payment.py
@@ -18,8 +18,9 @@ from django.contrib.messages.storage.session import SessionStorage
 from webtest import TestApp
 
 from combo.data.models import Page
-from combo.apps.lingo.models import (Regie, BasketItem, Transaction,
-        TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell)
+from combo.apps.lingo.models import (
+    Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell,
+    PaymentBackend)
 from combo.utils import sign_url
 
 from .test_manager import login
@@ -27,6 +28,14 @@ from .test_manager import login
 pytestmark = pytest.mark.django_db
 
 
+def get_url(with_payment_backend, view_name, regie):
+    if with_payment_backend:
+        return reverse(
+            view_name + '-payment-backend',
+            kwargs={'payment_backend_pk': regie.payment_backend.id})
+    return reverse(view_name, kwargs={'regie_pk': regie.id})
+
+
 @contextmanager
 def check_log(caplog, message):
     idx = len(caplog.records)
@@ -37,6 +46,11 @@ def check_log(caplog, message):
 
 @pytest.fixture
 def regie():
+    try:
+        payment_backend = PaymentBackend.objects.get(slug='test1')
+    except PaymentBackend.DoesNotExist:
+        payment_backend = PaymentBackend.objects.create(
+            label='test1', slug='test1', service='dummy', service_options={'siret': '1234'})
     try:
         regie = Regie.objects.get(slug='test')
     except Regie.DoesNotExist:
@@ -45,8 +59,7 @@ def regie():
         regie.slug = 'test'
         regie.description = 'test'
         regie.payment_min_amount = Decimal(4.5)
-        regie.service = 'dummy'
-        regie.service_options = {'siret': '1234'}
+        regie.payment_backend = payment_backend
         regie.save()
     return regie
 
@@ -85,11 +98,12 @@ def key(request, settings):
         return settings.LINGO_API_SIGN_KEY
 
 def test_default_regie():
+    payment_backend = PaymentBackend.objects.create(label='foo', slug='foo')
     Regie.objects.all().delete()
-    regie1 = Regie(label='foo', slug='foo')
+    regie1 = Regie(label='foo', slug='foo', payment_backend=payment_backend)
     regie1.save()
     assert bool(regie1.is_default) is True
-    regie2 = Regie(label='bar', slug='bar')
+    regie2 = Regie(label='bar', slug='bar', payment_backend=payment_backend)
     regie2.save()
     assert bool(regie2.is_default) is False
 
@@ -121,7 +135,9 @@ def test_payment_min_amount(app, basket_page, regie, user):
     resp = resp.form.submit()
     assert resp.status_code == 302
 
-def test_successfull_items_payment(app, basket_page, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_successfull_items_payment(app, basket_page, regie, user, with_payment_backend):
     items = {'item1': {'amount': '10.5', 'source_url': 'http://example.org/item/1'},
              'item2': {'amount': '42', 'source_url': 'http://example.org/item/2'},
              'item3': {'amount': '100', 'source_url': 'http://example.org/item/3'},
@@ -145,10 +161,11 @@ def test_successfull_items_payment(app, basket_page, regie, user):
             'ok': True, 'reason': 'Paid'}
     # make sure return url is the user return URL
     assert urlparse.urlparse(qs['return_url'][0]).path.startswith(
-            reverse('lingo-return', kwargs={'regie_pk': regie.id}))
+            reverse('lingo-return-payment-backend',
+                    kwargs={'payment_backend_pk': regie.payment_backend.id}))
     # simulate successful call to callback URL
     with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
-        resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': regie.id}), params=args)
+        resp = app.get(get_url(with_payment_backend, 'lingo-callback', regie), params=args)
     assert resp.status_code == 200
     # simulate successful return URL
     resp = app.get(qs['return_url'][0], params=args)
@@ -158,7 +175,9 @@ def test_successfull_items_payment(app, basket_page, regie, user):
     assert 'Your payment has been succesfully registered.' in resp.text
 
 def test_add_amount_to_basket(app, key, regie, user):
-    other_regie = Regie(label='test2', slug='test2', service='dummy', service_options={'siret': '1234'})
+    payment_backend = PaymentBackend.objects.create(
+            label='test2', slug='test2', service='dummy', service_options={'siret': '1234'})
+    other_regie = Regie(label='test2', slug='test2', payment_backend=payment_backend)
     other_regie.save()
 
     user_email = 'foo@example.com'
@@ -479,7 +498,9 @@ def test_cancel_basket_item_from_cell(app, key, regie, user):
     app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=404)
     app.post(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=403)
 
-def test_payment_callback(app, basket_page, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_payment_callback(app, basket_page, regie, user, with_payment_backend):
     page = Page(title='xxx', slug='index', template_name='standard')
     page.save()
     item = BasketItem.objects.create(user=user, regie=regie,
@@ -497,7 +518,7 @@ def test_payment_callback(app, basket_page, regie, user):
     assert data['amount'] == '10.50'
 
     # call callback with GET
-    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
+    callback_url = get_url(with_payment_backend, 'lingo-callback', regie)
     with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
         get_resp = app.get(callback_url, params=data)
         url = request.call_args[0][1]
@@ -526,12 +547,14 @@ def test_payment_callback(app, basket_page, regie, user):
     assert Transaction.objects.get(order_id=transaction_id).status == 3
 
     # call return view
-    get_resp = app.get(reverse('lingo-return', kwargs={'regie_pk': regie.pk}), params=data)
+    return_url = get_url(with_payment_backend, 'lingo-return', regie)
+    get_resp = app.get(return_url, params=data)
     assert get_resp.status_code == 302
     resp = app.get(get_resp['Location'])
     assert 'Your payment has been succesfully registered.' in resp.text
 
-def test_payment_callback_no_regie(app, basket_page, regie, user):
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_payment_callback_no_regie(app, basket_page, regie, user, with_payment_backend):
     item = BasketItem.objects.create(user=user, regie=regie,
                         subject='test_item', amount='10.5',
                         source_url='http://example.org/testitem/')
@@ -547,7 +570,7 @@ def test_payment_callback_no_regie(app, basket_page, regie, user):
     assert data['amount'] == '10.50'
 
     # call callback with GET
-    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
+    callback_url = get_url(with_payment_backend, 'lingo-callback', regie)
     with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
         get_resp = app.get(callback_url, params=data)
         url = request.call_args[0][1]
@@ -569,16 +592,20 @@ def test_payment_callback_no_regie(app, basket_page, regie, user):
             'amount': qs['amount'][0], 'ok': True}
     assert data['amount'] == '11.50'
 
-def test_nonexisting_transaction(app, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_nonexisting_transaction(app, regie, user, with_payment_backend):
     app = login(app)
     data = {'transaction_id': 'unknown', 'signed': True,
             'amount': '23', 'ok': True}
 
     # call callback with GET
-    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
+    callback_url = get_url(with_payment_backend, 'lingo-callback', regie)
     app.get(callback_url, params=data, status=404)
 
-def test_payment_callback_waiting(app, basket_page, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_payment_callback_waiting(app, basket_page, regie, user, with_payment_backend):
     item = BasketItem.objects.create(user=user, regie=regie,
                         subject='test_item', amount='10.5',
                         source_url='http://example.org/testitem/')
@@ -594,7 +621,7 @@ def test_payment_callback_waiting(app, basket_page, regie, user):
     assert data['amount'] == '10.50'
 
     # callback with WAITING state
-    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
+    callback_url = get_url(with_payment_backend, 'lingo-callback', regie)
     resp = app.get(callback_url, params=data)
     assert resp.status_code == 200
     assert Transaction.objects.get(order_id=transaction_id).status == eopayment.WAITING
@@ -615,7 +642,10 @@ def test_payment_callback_waiting(app, basket_page, regie, user):
     assert BasketItem.objects.get(id=item.id).payment_date
     assert BasketItem.get_items_to_be_paid(user).count() == 0
 
-def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_payment_no_callback_just_return(
+        caplog, app, basket_page, regie, user, with_payment_backend):
     item = BasketItem.objects.create(user=user, regie=regie,
                         subject='test_item', amount='10.5',
                         source_url='http://example.org/testitem/')
@@ -632,7 +662,7 @@ def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user):
 
     # call return with unsigned POST
     with check_log(caplog, 'received unsigned payment'):
-        return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id})
+        return_url = get_url(with_payment_backend, 'lingo-return', regie)
         with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
             get_resp = app.post(return_url, params=data)
             assert request.call_count == 0
@@ -654,7 +684,7 @@ def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user):
 
     # call return with signed POST
     data['signed'] = True
-    return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id})
+    return_url = get_url(with_payment_backend, 'lingo-return', regie)
     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]
@@ -827,7 +857,9 @@ def test_extra_fees(app, basket_page, key, regie, user):
     assert resp.status_code == 200
     assert Transaction.objects.get(order_id=transaction_id).status == 3
 
-def test_payment_callback_error(app, basket_page, regie, user):
+
+@pytest.mark.parametrize('with_payment_backend', [False, True])
+def test_payment_callback_error(app, basket_page, regie, user, with_payment_backend):
     item = BasketItem.objects.create(user=user, regie=regie,
                         subject='test_item', amount='10.5',
                         source_url='http://example.org/testitem/')
@@ -843,7 +875,7 @@ def test_payment_callback_error(app, basket_page, regie, user):
     assert data['amount'] == '10.50'
 
     # call callback with GET
-    callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
+    callback_url = get_url(with_payment_backend, 'lingo-callback', regie)
     with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
         mock_response = mock.Mock()
         def kaboom():
diff --git a/tests/test_lingo_remote_regie.py b/tests/test_lingo_remote_regie.py
index 6ae7087..b26240d 100644
--- a/tests/test_lingo_remote_regie.py
+++ b/tests/test_lingo_remote_regie.py
@@ -20,7 +20,7 @@ from django.contrib.auth.models import User
 from combo.utils import check_query, aes_hex_encrypt
 from combo.data.models import Page
 from combo.apps.lingo.models import (Regie, ActiveItems, ItemsHistory, SelfDeclaredInvoicePayment,
-                                     Transaction, BasketItem)
+                                     Transaction, BasketItem, PaymentBackend)
 
 pytestmark = pytest.mark.django_db
 
@@ -55,6 +55,11 @@ def admin():
 
 @pytest.fixture
 def remote_regie():
+    try:
+        payment_backend = PaymentBackend.objects.get(slug='test1')
+    except PaymentBackend.DoesNotExist:
+        payment_backend = PaymentBackend.objects.create(
+            label='test1', slug='test1', service='dummy', service_options={'siret': '1234'})
     try:
         regie = Regie.objects.get(slug='remote')
     except Regie.DoesNotExist:
@@ -63,8 +68,7 @@ def remote_regie():
         regie.slug = 'remote'
         regie.description = 'remote'
         regie.payment_min_amount = Decimal(2.0)
-        regie.service = 'dummy'
-        regie.service_options = {'siret': '1234'}
+        regie.payment_backend = payment_backend
         regie.webservice_url = 'http://example.org/regie' # is_remote
         regie.save()
     return regie
@@ -204,7 +208,8 @@ def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remo
             'ok': True, 'reason': 'Paid'}
     # make sure return url is the user return URL
     assert urlparse.urlparse(qs['return_url'][0]).path.startswith(
-        reverse('lingo-return', kwargs={'regie_pk': remote_regie.id}))
+        reverse('lingo-return-payment-backend',
+                kwargs={'payment_backend_pk': remote_regie.payment_backend.id}))
     # simulate successful return URL
     resp = app.get(qs['return_url'][0], params=args)
     assert resp.status_code == 302
@@ -326,7 +331,8 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app,
             'ok': True, 'reason': 'Paid'}
     # make sure return url is the user return URL
     assert urlparse.urlparse(qs['return_url'][0]).path.startswith(
-        reverse('lingo-return', kwargs={'regie_pk': remote_regie.id}))
+        reverse('lingo-return-payment-backend',
+                kwargs={'payment_backend_pk': remote_regie.payment_backend.id}))
     # simulate payment failure
     mock_get.side_effect = ConnectionError('where is my hostname?')
     resp = app.get(qs['return_url'][0], params=args)
-- 
2.20.1

