From 53c2812f4e882dba3e0006f1243ebecd75bb51fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20NO=C3=8BL?= <tnoel@entrouvert.com>
Date: Fri, 29 Oct 2021 12:37:29 +0200
Subject: [PATCH] lingo: refuse payment if an item cannot be trigged (#58210)

---
 combo/apps/lingo/models.py  |   9 ++
 combo/apps/lingo/views.py   |   6 +
 tests/test_lingo_payment.py | 212 ++++++++++++++++++++++++++----------
 3 files changed, 172 insertions(+), 55 deletions(-)

diff --git a/combo/apps/lingo/models.py b/combo/apps/lingo/models.py
index 8784420c..6d3913a4 100644
--- a/combo/apps/lingo/models.py
+++ b/combo/apps/lingo/models.py
@@ -650,6 +650,15 @@ class BasketItem(models.Model):
                     transaction.poll_backend()
         return qs
 
+    def is_notifiable(self, status='paid'):
+        if not self.source_url:
+            return True
+        url = self.source_url + 'jump/trigger/%s' % status
+        req = requests.get(url, remote_service='auto', timeout=5)
+        if req.status_code == 404:
+            return False
+        return True
+
     def notify(self, status):
         if not self.source_url:
             return
diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py
index 06f94125..72ec34ad 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -393,6 +393,12 @@ class PayMixin:
             if item.regie != regie:
                 messages.error(request, _('Invalid grouping for basket items.'))
                 return HttpResponseRedirect(next_url)
+
+        for item in items:
+            if not item.is_notifiable():
+                messages.error(request, _('At least one item is not linked to a payable form.'))
+                return HttpResponseRedirect(next_url)
+
         user = request.user if request.user.is_authenticated else None
         transaction = Transaction()
         if user:
diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py
index 46aa6c67..5f0da380 100644
--- a/tests/test_lingo_payment.py
+++ b/tests/test_lingo_payment.py
@@ -177,7 +177,11 @@ def test_regie_api(app):
     assert json.loads(resp.text).get('data')[0]['id'] == Regie.objects.get(is_default=True).slug
 
 
-def test_payment_min_amount(app, basket_page, regie, user):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_payment_min_amount(mock_trigger_request, app, basket_page, regie, user):
+    mock_response = mock.Mock()
+    mock_response.status_code = 403
+    mock_trigger_request.return_value = mock_response
     items = {
         'item1': {'amount': '1.5', 'source_url': '/item/1'},
         'item2': {'amount': '2.4', 'source_url': '/item/2'},
@@ -189,9 +193,14 @@ def test_payment_min_amount(app, basket_page, regie, user):
     resp = login(app).get('/test_basket_cell/')
     resp = resp.form.submit()
     assert resp.status_code == 302
+    assert mock_trigger_request.call_count == 0
 
 
-def test_transaction_manual_validation(app, basket_page, user, monkeypatch):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_transaction_manual_validation(mock_trigger_request, app, basket_page, user, monkeypatch):
+    mock_response = mock.Mock()
+    mock_response.status_code = 403
+    mock_trigger_request.return_value = mock_response
     pb = PaymentBackend.objects.create(
         label='test1',
         slug='test1',
@@ -206,22 +215,31 @@ def test_transaction_manual_validation(app, basket_page, user, monkeypatch):
         transaction_options={'manual_validation': True},
         can_pay_only_one_basket_item=False,
     )
-    BasketItem.objects.create(user=user, regie=regie, subject='item1', amount='1.5', source_url='/item/1')
+    BasketItem.objects.create(
+        user=user, regie=regie, subject='item1', amount='1.5', source_url='http://example.org/item/1/'
+    )
 
     with mock.patch('eopayment.Payment') as MockPayment:
         MockPayment.return_value.request.return_value = (9876, 3, {})
         resp = login(app).get('/test_basket_cell/')
         resp = resp.form.submit()
         assert MockPayment.return_value.request.call_args[1]['manual_validation'] is True
+    assert mock_trigger_request.call_count == 1
+    assert mock_trigger_request.call_args[0][0] == 'GET'
+    assert mock_trigger_request.call_args[0][1].startswith('http://example.org/item/1/jump/trigger/paid')
 
 
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
 @pytest.mark.parametrize('with_payment_backend', [False, True])
-def test_successfull_items_payment(app, basket_page, regie, user, with_payment_backend):
+def test_successfull_items_payment(mock_trigger_request, app, basket_page, regie, user, with_payment_backend):
+    mock_response = mock.Mock()
+    mock_response.status_code = 403
+    mock_trigger_request.return_value = mock_response
     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'},
-        'item4': {'amount': '354', 'source_url': 'http://example.org/item/4'},
+        '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/'},
+        'item4': {'amount': '354', 'source_url': 'http://example.org/item/4/'},
     }
     b_items = []
     for subject, details in items.items():
@@ -230,6 +248,7 @@ def test_successfull_items_payment(app, basket_page, regie, user, with_payment_b
 
     resp = login(app).get('/test_basket_cell/')
     resp = resp.form.submit()
+    assert mock_trigger_request.call_count == 4  # source_url/jump/trigger/paid items had been verified
     assert resp.status_code == 302
     location = resp.location
     assert 'dummy-payment' in location
@@ -264,7 +283,7 @@ def test_add_amount_to_basket(app, key, regie, user_name_id):
     )
     other_regie.save()
 
-    data = {'display_name': 'test amount', 'url': 'http://example.com'}
+    data = {'display_name': 'test amount', 'url': 'http://example.com/form/42/'}
     url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
     url = sign_url(url, key)
     resp = app.post_json(url, params=data, status=400)
@@ -494,26 +513,27 @@ def test_add_basket_item_with_remote_regie(app, user_name_id, remote_regie):
 
 
 def test_add_basket_item_without_display_name(app, user_name_id, regie):
-    data = {'amount': '42', 'url': 'http://example.com'}
+    data = {'amount': '42', 'url': 'http://example.com/form/id/'}
     url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
     url = sign_url(url, settings.LINGO_API_SIGN_KEY)
     resp = app.post_json(url, params=data, status=400)
     assert 'missing display_name parameter' in resp.text
 
 
-def test_cant_pay_if_different_capture_date(app, basket_page, regie, user):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_cant_pay_if_different_capture_date(mock_trigger_request, app, basket_page, regie, user):
     capture1 = (timezone.now() + timedelta(days=1)).date()
     capture2 = (timezone.now() + timedelta(days=2)).date()
     items = {
         'item1': {
             'amount': '10.5',
             'capture_date': capture1.isoformat(),
-            'source_url': 'http://example.org/item/1',
+            'source_url': 'http://example.org/item/1/',
         },
         'item2': {
             'amount': '42',
             'capture_date': capture2.isoformat(),
-            'source_url': 'http://example.org/item/2',
+            'source_url': 'http://example.org/item/2/',
         },
     }
     b_items = []
@@ -527,16 +547,18 @@ def test_cant_pay_if_different_capture_date(app, basket_page, regie, user):
     assert urllib.parse.urlparse(resp.location).path == '/test_basket_cell/'
     resp = resp.follow()
     assert "Invalid grouping for basket items: different capture dates." in resp.text
+    assert mock_trigger_request.call_count == 0
 
 
-def test_pay_single_basket_item(app, key, regie, user_name_id, john_doe):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_pay_single_basket_item(mock_trigger_request, app, key, regie, user_name_id, john_doe):
     page = Page(title='xxx', slug='index', template_name='standard')
     page.save()
     cell = LingoBasketCell(page=page, placeholder='content', order=0)
     cell.save()
 
     amount = 12
-    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com'}
+    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com/form/42/'}
     url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
     url = sign_url(url, key)
     resp = app.post_json(url, params=data)
@@ -547,10 +569,12 @@ def test_pay_single_basket_item(app, key, regie, user_name_id, john_doe):
 
     # check that payment is possible unconnected
     app.get(payment_url, status=302)
+    assert mock_trigger_request.call_count == 1
 
     # and connected with another user (john.doe != admin)
     login(app, username='john.doe', password='john.doe')
     app.get(payment_url, status=302)
+    assert mock_trigger_request.call_count == 2
 
     # forbid payment to regie with extra_fees_ws_url
     regie.extra_fees_ws_url = 'http://example.com/extra-fees'
@@ -563,8 +587,29 @@ def test_pay_single_basket_item(app, key, regie, user_name_id, john_doe):
     regie.extra_fees_ws_url = ''
     regie.save()
 
+    # forbid payment if payment trigger is impossible (404)
+    app.reset()
+    login(app)
+    mock_trigger_request.reset_mock()
+    mock_response = mock.Mock()
+    mock_response.status_code = 404
+    mock_trigger_request.return_value = mock_response
+    resp = app.get(payment_url, status=302)
+    assert mock_trigger_request.call_count == 1
+    assert mock_trigger_request.call_args[0][0] == 'GET'
+    assert mock_trigger_request.call_args[0][1].startswith('http://example.com/form/42/jump/trigger/paid')
+    assert 'At least one item is not linked to a payable form.' in app.session['_messages']
+
+    # now launch a good payment
+    mock_trigger_request.reset_mock()
+    mock_response = mock.Mock()
+    mock_response.status_code = 403
+    mock_trigger_request.return_value = mock_response
     assert not Transaction.objects.filter(user__username='admin', status=0).exists()
     resp = app.get(payment_url, params={'next_url': 'http://example.net/form/id/'})
+    assert mock_trigger_request.call_count == 1
+    assert mock_trigger_request.call_args[0][0] == 'GET'
+    assert mock_trigger_request.call_args[0][1].startswith('http://example.com/form/42/jump/trigger/paid')
     assert Transaction.objects.filter(user__username='admin', status=0).exists()
 
     # make sure the redirection is done to the payment backend
@@ -583,14 +628,15 @@ def test_pay_single_basket_item(app, key, regie, user_name_id, john_doe):
     assert Transaction.objects.filter(user__username='admin', status=eopayment.PAID).exists()
 
 
-def test_pay_single_basket_item_another_user(app, key, regie, user_name_id, john_doe):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_pay_single_basket_item_another_user(mock_trigger_request, app, key, regie, user_name_id, john_doe):
     page = Page(title='xxx', slug='index', template_name='standard')
     page.save()
     cell = LingoBasketCell(page=page, placeholder='content', order=0)
     cell.save()
 
     amount = 12
-    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com'}
+    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com/form/id/'}
     url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
     url = sign_url(url, key)
     resp = app.post_json(url, params=data)
@@ -603,6 +649,7 @@ def test_pay_single_basket_item_another_user(app, key, regie, user_name_id, john
 
     assert not Transaction.objects.filter(user__username='john.doe', status=0).exists()
     resp = app.get(payment_url, params={'next_url': 'http://example.net/form/id/'})
+    assert mock_trigger_request.call_count == 1
     assert Transaction.objects.filter(user__username='john.doe', status=0).exists()
 
     # make sure the redirection is done to the payment backend
@@ -621,7 +668,8 @@ def test_pay_single_basket_item_another_user(app, key, regie, user_name_id, john
     assert Transaction.objects.filter(user__username='john.doe', status=eopayment.PAID).exists()
 
 
-def test_pay_multiple_regies(app, key, regie, user_name_id):
+@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
+def test_pay_multiple_regies(mocked_trigger_request, app, key, regie, user_name_id):
     test_add_amount_to_basket(app, key, regie, user_name_id)
 
     page = Page(title='xxx', slug='test_basket_cell', template_name='standard')
@@ -630,13 +678,17 @@ def test_pay_multiple_regies(app, key, regie, user_name_id):
     cell.save()
 
     resp = login(app).get(page.get_online_url())
+    assert mocked_trigger_request.call_count == 0
     resp = resp.forms[0].submit()
+    assert mocked_trigger_request.call_count == 6  # trigger/paid verification, first regie
     assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
     qs = urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query)
     assert qs['amount'] == ['234.46']
 
     resp = login(app).get(page.get_online_url())
+    mocked_trigger_request.reset_mock()
     resp = resp.forms[1].submit()
+    assert mocked_trigger_request.call_count == 1  # trigger/paid verification, other regie
     qs = urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query)
     assert qs['amount'] == ['22.23']
 
@@ -658,7 +710,12 @@ def test_pay_as_anonymous_user(app, key, regie, user_name_id):
 def test_cancel_basket_item(app, key, regie, user_name_id):
     url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
     url = sign_url(url, key)
-    data = {'amount': 42, 'display_name': 'test amount', 'url': 'http://example.com/', 'notify': 'true'}
+    data = {
+        'amount': 42,
+        'display_name': 'test amount',
+        'url': 'http://example.com/form/id/',
+        'notify': 'true',
+    }
     resp = app.post_json(url, params=data)
     assert resp.status_code == 200
     assert json.loads(resp.text)['result'] == 'success'
@@ -723,7 +780,7 @@ def test_cancel_basket_item(app, key, regie, user_name_id):
         url = sign_url(url, key)
         data = {'basket_item_id': basket_item_id, 'notify': 'true'}
         resp = app.post_json(url, params=data)
-        assert request.call_args[0] == ('POST', 'http://example.com/jump/trigger/cancelled')
+        assert request.call_args[0] == ('POST', 'http://example.com/form/id/jump/trigger/cancelled')
         assert not BasketItem.objects.filter(amount=42, cancellation_date__isnull=True).exists()
         assert BasketItem.objects.filter(amount=21, cancellation_date__isnull=True).exists()
 
@@ -807,10 +864,12 @@ def test_payment_callback(app, basket_page, regie, user, with_payment_backend):
     page = Page(title='xxx', slug='index', template_name='standard')
     page.save()
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/id/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -824,15 +883,17 @@ def test_payment_callback(app, basket_page, regie, user, with_payment_backend):
     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]
-        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+        assert url.startswith('http://example.org/test/id/jump/trigger/paid')
     assert get_resp.status_code == 200
     assert Transaction.objects.get(order_id=transaction_id).status == 3
 
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='11.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='11.5', source_url='http://example.org/test/id/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -858,10 +919,14 @@ def test_payment_callback(app, basket_page, regie, user, with_payment_backend):
 @pytest.mark.parametrize('with_payment_backend', [False, True])
 def test_payment_callback_no_regie(app, basket_page, regie, user, with_payment_backend):
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/1/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/1/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -874,16 +939,21 @@ def test_payment_callback_no_regie(app, basket_page, regie, user, with_payment_b
     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)
+        assert request.call_args[0][0] == 'POST'
         url = request.call_args[0][1]
-        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+        assert url.startswith('http://example.org/test/1/jump/trigger/paid')
     assert get_resp.status_code == 200
     assert Transaction.objects.get(order_id=transaction_id).status == 3
 
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='11.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='11.5', source_url='http://example.org/test/2/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/2/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -898,10 +968,14 @@ def test_payment_return_without_query_string(app, basket_page, regie, user, with
     page = Page(title='xxx', slug='index', template_name='standard')
     page.save()
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/3/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/3/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -948,10 +1022,14 @@ def test_nonexisting_transaction(app, regie, user, with_payment_backend):
 @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/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/4/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/4/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -975,7 +1053,7 @@ def test_payment_callback_waiting(app, basket_page, regie, user, with_payment_ba
         resp = app.get(callback_url, params=data)
         assert resp.status_code == 200
         url = request.call_args[0][1]
-        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+        assert url.startswith('http://example.org/test/4/jump/trigger/paid')
 
     assert Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID
     assert BasketItem.objects.get(id=item.id).payment_date
@@ -985,10 +1063,14 @@ def test_payment_callback_waiting(app, basket_page, regie, user, with_payment_ba
 @pytest.mark.parametrize('with_payment_backend', [False, True])
 def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user, with_payment_backend):
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/5/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/5/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -1024,7 +1106,7 @@ def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user,
     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')
+        assert url.startswith('http://example.org/test/5/jump/trigger/paid')
     # redirect to payment status
     assert get_resp.status_code == 302
     assert urllib.parse.urlparse(get_resp.url).path.startswith('/lingo/payment-status')
@@ -1382,10 +1464,14 @@ def test_extra_fees(app, basket_page, key, regie, user_name_id):
 @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/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/6/'
     )
     resp = login(app).get(basket_page.get_online_url())
-    resp = resp.form.submit()
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = resp.form.submit()
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/test/6/jump/trigger/paid')
     assert resp.status_code == 302
     location = resp.location
     parsed = urllib.parse.urlparse(location)
@@ -1407,7 +1493,7 @@ def test_payment_callback_error(app, basket_page, regie, user, with_payment_back
         request.return_value = mock_response
         get_resp = app.get(callback_url, params=data)
         url = request.call_args[0][1]
-        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+        assert url.startswith('http://example.org/test/6/jump/trigger/paid')
     assert get_resp.status_code == 200
     assert Transaction.objects.get(order_id=transaction_id).status == 3
     assert BasketItem.objects.get(id=item.id).payment_date
@@ -1430,7 +1516,7 @@ def test_payment_callback_error(app, basket_page, regie, user, with_payment_back
         request.return_value = mock_response
         appconfig.notify_payments()
         url = request.call_args[0][1]
-        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+        assert url.startswith('http://example.org/test/6/jump/trigger/paid')
     assert BasketItem.objects.get(id=item.id).payment_date
     assert BasketItem.objects.get(id=item.id).notification_date
 
@@ -1448,7 +1534,7 @@ def test_payment_callback_not_found(app, user, regie):
 @pytest.mark.parametrize("authenticated", [True, False])
 def test_payment_no_basket(app, user_name_id, regie, authenticated):
     url = reverse('api-add-basket-item')
-    source_url = 'http://example.org/item/1'
+    source_url = 'http://example.org/item/1/'
     data = {'amount': 10, 'display_name': 'test item', 'url': source_url}
     if authenticated:
         data['NameId'] = user_name_id
@@ -1484,7 +1570,11 @@ def test_payment_no_basket(app, user_name_id, regie, authenticated):
     # amount ok, redirection to payment backend
     item.amount = Decimal('10.00')
     item.save()
-    resp = app.get(payment_url)
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = app.get(payment_url)
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/item/1/jump/trigger/paid')
     assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
     qs = urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query)
     assert qs['amount'] == ['10.00']
@@ -1494,12 +1584,16 @@ def test_payment_no_basket(app, user_name_id, regie, authenticated):
         assert 'email' not in qs
 
     # mail can be specified here for anonymous user
-    resp = app.get(
-        payment_url,
-        params={
-            'email': 'foo@localhost',
-        },
-    )
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = app.get(
+            payment_url,
+            params={
+                'email': 'foo@localhost',
+            },
+        )
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.org/item/1/jump/trigger/paid')
     assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
     qs = urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query)
     assert qs['amount'] == ['10.00']
@@ -1612,20 +1706,24 @@ def test_transaction_status_api(app, regie, user):
 
 def test_request_payment_exception(app, basket_page, regie, user):
     BasketItem.objects.create(
-        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/'
+        user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/test/7/'
     )
 
     with mock.patch('eopayment.dummy.Payment.request', autospec=True) as mock_request:
         mock_request.side_effect = eopayment.PaymentException
         resp = login(app).get(basket_page.get_online_url())
-        resp = resp.form.submit().follow()
+        with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+            resp = resp.form.submit().follow()
+            assert trigger_request.call_count == 1
+            assert trigger_request.call_args[0][0] == 'GET'
+            assert trigger_request.call_args[0][1].startswith('http://example.org/test/7/jump/trigger/paid')
         assert 'Failed to initiate payment request' in resp.text
 
 
 @pytest.mark.parametrize('transaction_date', [None, datetime(2020, 1, 1, 12, 00, 00, tzinfo=utc)])
 def test_bank_transaction_date(app, key, regie, user, john_doe, caplog, transaction_date):
     amount = 12
-    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com/'}
+    data = {'amount': amount, 'display_name': 'test amount', 'url': 'http://example.com/form/id/'}
     url = reverse('api-add-basket-item') + '?orig=wcs'
     url = sign_url(url, key)
     resp = app.post_json(url, params=data)
@@ -1633,7 +1731,11 @@ def test_bank_transaction_date(app, key, regie, user, john_doe, caplog, transact
     assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists()
 
     payment_url = resp.json['payment_url']
-    resp = app.get(payment_url, params={'next_url': 'http://example.net/form/id/'})
+    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as trigger_request:
+        resp = app.get(payment_url, params={'next_url': 'http://example.net/form/id/'})
+        assert trigger_request.call_count == 1
+        assert trigger_request.call_args[0][0] == 'GET'
+        assert trigger_request.call_args[0][1].startswith('http://example.com/form/id/jump/trigger/paid')
 
     assert Transaction.objects.count() == 1
     transaction = Transaction.objects.get()
@@ -1689,7 +1791,7 @@ def test_bank_transaction_date(app, key, regie, user, john_doe, caplog, transact
                 assert json.loads(post.call_args[1]['data'])['bank_transaction_date'] == '2020-01-02T12:00:00'
             else:
                 assert json.loads(post.call_args[1]['data'])['bank_transaction_date'] == '2020-01-01T12:00:00'
-            assert post.call_args[0][0] == 'http://example.com/jump/trigger/paid'
+            assert post.call_args[0][0] == 'http://example.com/form/id/jump/trigger/paid'
     transaction.refresh_from_db()
     if transaction_date is None:
         transaction.refresh_from_db()
-- 
2.30.2

