combo/tests/test_lingo_payment.py

1534 lines
66 KiB
Python

from contextlib import contextmanager
import eopayment
import pytest
from datetime import datetime, timedelta
import urllib
from decimal import Decimal
import json
import mock
import uuid
from mellon.models import UserSAMLIdentifier
from django.apps import apps
from django.contrib.auth.models import User
from django.urls 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.utils.timezone import utc
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,
PaymentBackend)
from combo.utils import aes_hex_decrypt, sign_url
from combo.apps.lingo.views import signing_loads, signing_dumps
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)
yield
assert any(message in record.message for record in caplog.records[idx:]), \
'%r not found in log records' % 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:
regie = Regie()
regie.label = 'Test'
regie.slug = 'test'
regie.description = 'test'
regie.payment_min_amount = Decimal(4.5)
regie.payment_backend = payment_backend
regie.save()
return regie
@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:
regie = Regie()
regie.label = 'Remote'
regie.slug = 'remote'
regie.description = 'remote'
regie.payment_min_amount = Decimal(2.0)
regie.payment_backend = payment_backend
regie.webservice_url = 'http://example.org/regie' # is_remote
regie.save()
return regie
@pytest.fixture
def basket_page():
page = Page(title='xxx', slug='test_basket_cell', template_name='standard')
page.save()
cell = LingoBasketCell(page=page, placeholder='content', order=0)
cell.save()
return page
@pytest.fixture
def user():
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
user = User.objects.create_user('admin', password='admin',
email='foo@example.com')
return user
@pytest.fixture
def user_name_id(user):
name_id = '7d6a86ae70f746f4887f22bad212f836'
UserSAMLIdentifier.objects.create(user=user, name_id=name_id)
return name_id
@pytest.fixture(params=['orig', 'sign_key'])
def key(request, settings):
if request.param == 'orig':
key = 'abcde'
settings.KNOWN_SERVICES = {
'wcs': {
'wcs1': {
'url': 'http://example.org/',
'orig': 'http://combo.example.net/',
'verif_orig': 'wcs',
'secret': key,
},
}
}
return key
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 ':' not in query['transaction-id']
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()
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', payment_backend=payment_backend)
regie2.save()
assert bool(regie2.is_default) is False
regie2.is_default = True
regie2.save()
regie2 = Regie.objects.get(id=regie2.id)
assert bool(regie2.is_default) is True
regie1 = Regie.objects.get(id=regie1.id)
assert bool(regie1.is_default) is False
def test_regie_api(app):
resp = app.get(reverse('api-regies'))
assert len(json.loads(resp.text).get('data')) == 0
test_default_regie()
resp = app.get(reverse('api-regies'))
assert len(json.loads(resp.text).get('data')) == 2
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):
items = {'item1': {'amount': '1.5', 'source_url': '/item/1'},
'item2': {'amount': '2.4', 'source_url': '/item/2'}
}
b_items = []
for subject, details in items.items():
b_item = BasketItem.objects.create(user=user, regie=regie,
subject=subject, **details)
b_items.append(b_item.pk)
resp = login(app).get('/test_basket_cell/')
resp = resp.form.submit()
assert resp.status_code == 302
def test_transaction_manual_validation(app, basket_page, user, monkeypatch):
pb = PaymentBackend.objects.create(
label='test1', slug='test1', service='payzen',
service_options={'vads_site_id': '12345678', 'secret_test': 'plkoBdfcx987dhft6'}
)
regie = Regie.objects.create(
label='Test', slug='test', description='test', payment_backend=pb,
transaction_options={'manual_validation': True})
BasketItem.objects.create(
user=user, regie=regie, subject='item1', amount='1.5', source_url='/item/1')
class MockPayment(object):
request = mock.Mock(return_value=(9876, 3, {}))
def get_eopayment_object(*args, **kwargs):
return MockPayment
import combo.apps.lingo.views
monkeypatch.setattr(combo.apps.lingo.views, 'get_eopayment_object', get_eopayment_object)
resp = login(app).get('/test_basket_cell/')
resp = resp.form.submit()
assert MockPayment.request.call_args[1]['manual_validation'] is True
@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'},
'item4': {'amount': '354', 'source_url': 'http://example.org/item/4'}
}
b_items = []
for subject, details in items.items():
b_item = BasketItem.objects.create(user=user, regie=regie,
subject=subject, **details)
b_items.append(b_item.pk)
resp = login(app).get('/test_basket_cell/')
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
assert 'dummy-payment' in location
parsed = urlparse.urlparse(location)
# get return_url and transaction id from location
qs = urlparse.parse_qs(parsed.query)
args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
'ok': True, 'reason': 'Paid'}
# make sure return url is the user return URL
assert urlparse.urlparse(qs['return_url'][0]).path.startswith(
'/lingo/return-payment-backend/%s/' % regie.payment_backend.id)
# simulate successful call to callback URL
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
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)
# redirect to payment status
assert resp.status_code == 302
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_name_id):
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()
data = {'display_name': 'test amount',
'url': 'http://example.com'}
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)
assert 'missing amount parameter' in resp.text
amount = 42
data['amount'] = amount
url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), 'unknown_id')
url = sign_url(url, key)
resp = app.post_json(url, params=data, status=400)
assert 'unknown user' in resp.text
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)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=amount).exists()
assert BasketItem.objects.filter(amount=amount)[0].regie_id == regie.id
resp = app.post_json('%s&amount=10' % url, params=data, status=403) # bad signature
data['extra'] = {'amount': '22.22'}
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)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('64.22')).exists()
data['amount'] = [amount]
data['extra'] = {'amount': ['22.22', '12']}
url = '%s?NameId=%s&orig=wcs&amount=5' % (reverse('api-add-basket-item'), user_name_id)
url = sign_url(url, key)
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('81.22')).exists()
# accept french notation if settings.LANGUAGE_CODE is 'fr-*'
url = '%s?amount=10,00&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)
assert resp.json['err_desc'] == 'invalid value for "amount" in query string'
data['amount'] = '1,10'
url = '%s?amount=10.00&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)
assert resp.json['err_desc'] == 'invalid value for "amount" in payload'
data['amount'] = '1.10'
data['extra'] = {'amount': '0,01'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'invalid value for "amount" in extra payload'
data['amount'] = '1,10'
data['extra'] = {'amount': '0,01'}
url = '%s?amount=10,00&NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
url = sign_url(url, key)
with override_settings(LANGUAGE_CODE='fr-be'):
resp = app.post_json(url, params=data, status=200)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('11.11')).exists()
other_regie.is_default = True
other_regie.save()
data['amount'] = []
data['extra'] = {'amount': '22.23'}
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)
item = BasketItem.objects.get(amount=Decimal('22.23'))
assert resp.status_code == 200
response = json.loads(resp.text)
assert response['result'] == 'success'
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
url = '%s?NameId=%s&regie_id=%s' % (
reverse('api-add-basket-item'), user_name_id, regie.id)
data['extra'] = {'amount': '22.24', 'foo': 'bar'}
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('22.24')).exists()
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].regie_id == regie.id
assert BasketItem.objects.filter(amount=Decimal('22.24'))[0].request_data == data['extra']
url = '%s?NameId=%s&regie_id=%s' % (
reverse('api-add-basket-item'), user_name_id, regie.slug)
data['extra'] = {'amount': '13.67'}
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=Decimal('13.67')).exists()
assert BasketItem.objects.filter(amount=Decimal('13.67'))[0].regie_id == regie.id
url = '%s?NameId=%s&orig=wcs&regie_id=%s' % (reverse('api-add-basket-item'), user_name_id, 'scarecrow')
url = sign_url(url, key)
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'unknown regie'
def test_basket_item_with_capture_date(app, user, user_name_id, regie, basket_page, monkeypatch):
url = '%s?NameId=%s' % (reverse('api-add-basket-item'), user_name_id)
capture_date = timezone.now().date()
data = {
'amount': 10, 'capture_date': capture_date.isoformat(),
'display_name': 'test item'
}
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert BasketItem.objects.all()[0].capture_date == capture_date
resp = login(app).get('/test_basket_cell/')
import eopayment
eopayment_mock = mock.Mock(
return_value=('orderid', eopayment.URL, 'http://dummy-payment.demo.entrouvert.com/'))
monkeypatch.setattr(eopayment.Payment, 'request', eopayment_mock)
resp = resp.form.submit()
assert resp.status_code == 302
location = urlparse.urlparse(resp.location)
assert location.path == '/'
assert location.hostname == 'dummy-payment.demo.entrouvert.com'
items_info = [{'text': 'test item', 'amount': Decimal('10.00'), 'reference_id': ''}]
eopayment_mock.assert_called_once_with(
Decimal(10), email=user.email, first_name=user.first_name, last_name=user.last_name,
capture_date=capture_date, merchant_name='Compte Citoyen', items_info=items_info
)
def test_basket_items_extra_info(app, user, user_name_id, regie, basket_page, monkeypatch):
url = '%s?NameId=%s' % (reverse('api-add-basket-item'), user_name_id)
items_info = [{'text': 'face mask', 'amount': Decimal(10), 'reference_id': '1'},
{'text': 'face mask 2', 'amount': Decimal(15), 'reference_id': '2'}]
items_post_data = [{'display_name': 'face mask', 'amount': 10, 'reference_id': 1},
{'display_name': 'face mask 2', 'amount': 15, 'reference_id': 2}]
global_title = 'FooBar'
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
for item in items_post_data:
resp = app.post_json(url, params=item)
assert resp.status_code == 200
resp = login(app).get('/test_basket_cell/')
import eopayment
eopayment_mock = mock.Mock(
return_value=('orderid', eopayment.URL, 'http://dummy-payment.demo.entrouvert.com/'))
monkeypatch.setattr(eopayment.Payment, 'request', eopayment_mock)
with override_settings(TEMPLATE_VARS={'global_title': global_title}):
resp = resp.form.submit()
eopayment_mock.assert_called_once_with(
Decimal(25), email=user.email, first_name=user.first_name, last_name=user.last_name,
merchant_name=global_title, items_info=items_info
)
def test_basket_items_extra_info_no_basket(app, regie, basket_page, monkeypatch):
# when no user is authenticated, it is still possible to pass an email to eopayment backend
url = reverse('api-add-basket-item')
email = 'test@entrouvert.com'
items_info = [{'text': 'face mask', 'amount': Decimal(10), 'reference_id': ''}]
item = {'display_name': 'face mask', 'amount': 10, 'email': email}
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = app.post_json(url, params=item)
assert resp.status_code == 200
item = BasketItem.objects.first()
assert item.user is None
payment_url = resp.json['payment_url']
import eopayment
eopayment_mock = mock.Mock(
return_value=('orderid', eopayment.URL, 'http://dummy-payment.demo.entrouvert.com/'))
monkeypatch.setattr(eopayment.Payment, 'request', eopayment_mock)
resp = app.get(payment_url)
eopayment_mock.assert_called_once_with(
Decimal(10), email=email, first_name='', last_name='', items_info=items_info,
merchant_name='Compte Citoyen'
)
@pytest.mark.parametrize("invalid_capture_date", [8, '', 'not-a-date'])
def test_add_basket_capture_date_format(app, user_name_id, regie, invalid_capture_date):
url = '%s?NameId=%s' % (reverse('api-add-basket-item'), user_name_id)
data = {'amount': 10, 'display_name': 'test item'}
data['capture_date'] = invalid_capture_date
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'bad format for capture date, it should be yyyy-mm-dd'
def test_add_basket_item_with_remote_regie(app, user_name_id, remote_regie):
data = {'amount': 10, 'display_name': 'test item'}
url = '%s?NameId=%s' % (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 resp.json['err_desc'] == 'can not add a basket item to a remote regie'
def test_add_basket_item_without_display_name(app, user_name_id, regie):
data = {'amount': '42',
'url': 'http://example.com'}
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):
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'
},
'item2': {
'amount': '42', 'capture_date': capture2.isoformat(),
'source_url': 'http://example.org/item/2'},
}
b_items = []
for subject, details in items.items():
b_item = BasketItem.objects.create(
user=user, regie=regie, subject=subject, **details)
b_items.append(b_item.pk)
resp = login(app).get('/test_basket_cell/')
resp = resp.form.submit()
assert resp.status_code == 302
assert urlparse.urlparse(resp.location).path == '/test_basket_cell/'
resp = resp.follow()
assert "Invalid grouping for basket items: different capture dates." in resp.text
def test_pay_single_basket_item(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'}
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)
# check that an unpaid item exists in basket
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists()
payment_data = resp.json
payment_url = payment_data['payment_url']
# check that payment is possible unconnected
app.get(payment_url, status=302)
# and connected with another user (john.doe != admin)
login(app, username='john.doe', password='john.doe')
app.get(payment_url, status=302)
# forbid payment to regie with extra_fees_ws_url
regie.extra_fees_ws_url = 'http://example.com/extra-fees'
regie.save()
app.reset()
login(app)
resp = app.get(payment_url, status=403)
assert 'No item payment allowed as extra fees set.' in resp.text
regie.extra_fees_ws_url = ''
regie.save()
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 Transaction.objects.filter(user__username='admin', status=0).exists()
# make sure the redirection is done to the payment backend
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}
# simulate payment service redirecting the user to /lingo/return/... (eopayment
# 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
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)
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):
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'}
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)
# check that an unpaid item exists in basket
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists()
payment_url = resp.json['payment_url']
# and connected with another user (john.doe != admin)
login(app, username='john.doe', password='john.doe')
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 Transaction.objects.filter(user__username='john.doe', status=0).exists()
# make sure the redirection is done to the payment backend
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}
# simulate payment service redirecting the user to /lingo/return/... (eopayment
# 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
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)
assert Transaction.objects.filter(user__username='john.doe', status=eopayment.PAID).exists()
def test_pay_multiple_regies(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')
page.save()
cell = LingoBasketCell(page=page, placeholder='content', order=0)
cell.save()
resp = login(app).get(page.get_online_url())
resp = resp.forms[0].submit()
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
assert qs['amount'] == ['234.46']
resp = login(app).get(page.get_online_url())
resp = resp.forms[1].submit()
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
assert qs['amount'] == ['22.23']
def test_pay_as_anonymous_user(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')
page.save()
cell = LingoBasketCell(page=page, placeholder='content', order=0)
cell.save()
resp = login(app).get(page.get_online_url())
app.cookiejar.clear(domain='testserver.local', path='/', name='sessionid')
resp = resp.forms[0].submit().follow()
assert 'Payment requires to be logged in.' in resp.text
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'}
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=42, cancellation_date__isnull=True).exists()
basket_item_id = json.loads(resp.text)['id']
data = {'amount': 21, 'display_name': 'test amount', 'url': 'http://example.net/'}
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=42, cancellation_date__isnull=True).exists()
assert BasketItem.objects.filter(amount=21, cancellation_date__isnull=True).exists()
basket_item_id_2 = json.loads(resp.text)['id']
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'missing basket_item_id parameter'
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'basket_item_id': 'eggs', 'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'invalid basket_item_id'
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'basket_item_id': 0, 'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'unknown basket item'
url = '%s?orig=wcs' % (reverse('api-remove-basket-item'))
url = sign_url(url, key)
data = {'basket_item_id': basket_item_id, 'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'no user specified'
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), 'unknown@example.com')
url = sign_url(url, key)
data = {'basket_item_id': basket_item_id, 'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'unknown user'
other_user, _ = User.objects.get_or_create(email='hop@example.com')
other_user_name_id = uuid.uuid4()
UserSAMLIdentifier.objects.get_or_create(user=other_user, name_id=other_user_name_id)
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), other_user_name_id)
url = sign_url(url, key)
data = {'basket_item_id': basket_item_id, 'notify': 'true'}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'user does not own the basket item'
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), 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', u'http://example.com/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()
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'basket_item_id': basket_item_id_2}
resp = app.post_json(url, params=data)
assert request.call_count == 0
assert not BasketItem.objects.filter(amount=42, cancellation_date__isnull=True).exists()
assert not BasketItem.objects.filter(amount=21, cancellation_date__isnull=True).exists()
url = '%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'basket_item_id': basket_item_id}
resp = app.post_json(url, params=data, status=400)
assert resp.json['err_desc'] == 'basket item already cancelled'
def test_cancel_basket_item_from_cell(app, key, regie, user_name_id):
page = Page(title='xxx', slug='test_basket_cell', template_name='standard')
page.save()
cell = LingoBasketCell(page=page, placeholder='content', order=0)
cell.save()
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.org/testitem/'}
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=42, cancellation_date__isnull=True).exists()
basket_item_id = json.loads(resp.text)['id']
# check while not logged in
resp = app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=404)
assert BasketItem.objects.filter(id=basket_item_id).exists()
# check a successful case
app = login(app)
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
resp = app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}))
resp = resp.form.submit()
url = request.call_args[0][1]
assert url.startswith('http://example.org/testitem/jump/trigger/cancelled')
assert BasketItem.objects.filter(id=basket_item_id, cancellation_date__isnull=False).exists()
# check removal of an item that is not cancellable
url = '%s?NameId=%s&cancellable=no&orig=wcs' % (reverse('api-add-basket-item'), user_name_id)
url = sign_url(url, key)
data = {'amount': 21, 'display_name': 'test amount',
'url': 'http://example.org/testitem/'}
resp = app.post_json(url, params=data)
basket_item2_id = json.loads(resp.text)['id']
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=21, cancellation_date__isnull=True).exists()
resp = app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item2_id}))
resp = resp.form.submit()
resp = resp.follow()
assert 'This item cannot be removed.' in resp.text
# check removal of the item of another user
other_user, _ = User.objects.get_or_create(email='hop@example.com')
other_user_name_id = uuid.uuid4()
UserSAMLIdentifier.objects.get_or_create(user=other_user, name_id=other_user_name_id)
url = '%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), other_user_name_id)
url = sign_url(url, key)
data = {'amount': 42, 'display_name': 'test amount', 'url': 'http://example.org/testitem/'}
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
basket_item_id = json.loads(resp.text)['id']
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)
@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,
subject='test_item', amount='10.5',
source_url='http://example.org/testitem/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '10.50'
# call callback with GET
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]
assert url.startswith('http://example.org/testitem/jump/trigger/paid')
assert get_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
item = BasketItem.objects.create(user=user, regie=regie,
subject='test_item', amount='11.5',
source_url='http://example.org/testitem/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
return_url = qs['return_url'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '11.50'
# call callback with POST
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
post_resp = app.post(callback_url, params=data)
assert post_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
# call return view
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
@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/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '10.50'
# call callback with GET
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]
assert url.startswith('http://example.org/testitem/jump/trigger/paid')
assert get_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
item = BasketItem.objects.create(user=user, regie=regie,
subject='test_item', amount='11.5',
source_url='http://example.org/testitem/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '11.50'
@pytest.mark.parametrize('with_payment_backend', [False, True])
def test_payment_return_without_query_string(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,
subject='test_item', amount='10.5',
source_url='http://example.org/testitem/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
return_url = qs['return_url'][0]
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
# payment status is obtained through callback
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)
transaction = Transaction.objects.get(order_id=transaction_id)
assert transaction.status == 3
# then return view is called without any data, which should be expected by the backend
with mock.patch.object(eopayment.dummy.Payment, 'response', autospec=True) as mock_response:
mock_response.return_value = eopayment.common.PaymentResponse(result=transaction.status,
order_id=transaction.order_id)
get_resp = app.get(return_url)
mock_response.assert_called_once_with(
mock.ANY, '', redirect=True, order_id_hint=transaction.order_id,
order_status_hint=transaction.status
)
assert get_resp.status_code == 302
resp = app.get(get_resp['Location'])
assert 'Your payment has been succesfully registered.' in resp.text
@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 = get_url(with_payment_backend, 'lingo-callback', regie)
app.get(callback_url, params=data, status=404)
@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/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'waiting': True}
assert data['amount'] == '10.50'
# callback with WAITING state
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
assert BasketItem.objects.get(id=item.id).waiting_date
assert not BasketItem.objects.get(id=item.id).payment_date
assert BasketItem.get_items_to_be_paid(user).count() == 0
# callback with PAID state
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
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 Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID
assert BasketItem.objects.get(id=item.id).payment_date
assert BasketItem.get_items_to_be_paid(user).count() == 0
@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/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '10.50'
return_url = qs['return_url'][0]
# call return with unsigned POST
with check_log(caplog, 'received unsigned payment'):
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
get_resp = app.post(return_url, params=data)
assert request.call_count == 0
assert get_resp.status_code == 302
assert urlparse.urlparse(get_resp['location']).path == '/lingo/payment-status'
assert Transaction.objects.get(order_id=transaction_id).status == 0 # not paid
# call return with missing data
with check_log(caplog, 'failed to process payment response: missing transaction_id'):
baddata = data.copy()
del baddata['transaction_id']
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
get_resp = app.post(return_url, params=baddata)
assert get_resp.status_code == 302
resp = app.get(get_resp['Location'])
assert 'Your payment has been succesfully registered.' not in resp.text
assert 'the payment service failed to provide a correct answer.' in resp.text
assert Transaction.objects.get(order_id=transaction_id).status == 0 # not paid
# call return with signed POST
data['signed'] = True
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.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()
t1.start_date = timezone.now() - timedelta(hours=2)
t1.save()
t2 = Transaction(status=0)
t2.save()
appconfig = apps.get_app_config('lingo')
appconfig.update_transactions()
assert Transaction.objects.get(id=t1.id).status == EXPIRED
assert Transaction.objects.get(id=t2.id).status == 0
def test_transaction_validate(app, key, regie, user):
t1 = Transaction(regie=regie, bank_data={'bank': 'data'}, amount=12,
status=eopayment.PAID)
t1.save()
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=0'
resp = app.post_json(url, params={}, status=403)
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=0&orig=wcs'
url = sign_url(url, key)
resp = app.post_json(url, params={}, status=404)
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=%s&orig=wcs' % t1.id
url = sign_url(url, key)
resp = app.post_json(url, params={})
assert json.loads(resp.text)['err'] == 0
operations = TransactionOperation.objects.filter(transaction=t1)
assert len(operations) == 1
assert operations[0].amount == 10
with mock.patch.object(eopayment.dummy.Payment, 'validate', autospec=True) as mock_validate:
mock_validate.side_effect = eopayment.ResponseError
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=%s&orig=wcs' % t1.id
url = sign_url(url, key)
resp = app.post_json(url, params={})
assert json.loads(resp.text)['err'] == 1
assert TransactionOperation.objects.filter(transaction=t1).count() == 1
def test_transaction_cancel(app, key, regie, user):
t1 = Transaction(regie=regie, bank_data={'bank': 'data'}, amount=12,
status=eopayment.PAID)
t1.save()
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=0&orig=wcs'
resp = app.post_json(url, params={}, status=403)
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=0&orig=wcs'
url = sign_url(url, key)
resp = app.post(url, params={}, status=404)
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=%s&orig=wcs' % t1.id
url = sign_url(url, key)
resp = app.post_json(url, params={})
assert json.loads(resp.text)['err'] == 0
operations = TransactionOperation.objects.filter(transaction=t1)
assert len(operations) == 1
assert operations[0].amount == 10
with mock.patch.object(eopayment.dummy.Payment, 'cancel', autospec=True) as mock_cancel:
mock_cancel.side_effect = eopayment.ResponseError
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=%s&orig=wcs' % t1.id
url = sign_url(url, key)
resp = app.post_json(url, params={})
assert json.loads(resp.text)['err'] == 1
assert TransactionOperation.objects.filter(transaction=t1).count() == 1
def test_extra_fees(app, basket_page, key, regie, user_name_id):
regie.extra_fees_ws_url = 'http://www.example.net/extra-fees'
regie.save()
amount = 42
data = {'amount': amount, 'display_name': 'test amount'}
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '5'}]}
request.return_value = mock_json
url = sign_url('%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id), key)
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert BasketItem.objects.filter(amount=amount).exists()
assert BasketItem.objects.filter(amount=amount)[0].regie_id == regie.id
assert BasketItem.objects.filter(amount=5, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=5, extra_fee=True)[0].regie_id == regie.id
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '7'}]}
request.return_value = mock_json
data['amount'] = 43
url = sign_url('%s?NameId=%s&orig=wcs' % (reverse('api-add-basket-item'), user_name_id), key)
resp = app.post_json(url, params=data)
assert request.call_args[0] == ('POST', 'http://www.example.net/extra-fees')
assert len(json.loads(request.call_args[1]['data'])['data']) == 2
assert resp.status_code == 200
assert json.loads(resp.text)['result'] == 'success'
assert not BasketItem.objects.filter(amount=5, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=7, extra_fee=True).exists()
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '4'}]}
request.return_value = mock_json
url = sign_url('%s?NameId=%s&orig=wcs' % (reverse('api-remove-basket-item'), user_name_id), key)
data = {'basket_item_id': BasketItem.objects.get(amount=43).id}
resp = app.post_json(url, params=data)
assert resp.status_code == 200
assert not BasketItem.objects.filter(amount=7, extra_fee=True).exists()
assert BasketItem.objects.filter(amount=4, extra_fee=True).exists()
# test payment
app = login(app)
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '2'}]}
request.return_value = mock_json
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '44.00'
# test again, without specifying a regie
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_json = mock.Mock()
mock_json.status_code = 200
mock_json.json.return_value = {'err': 0, 'data': [{'subject': 'Extra Fees', 'amount': '3'}]}
request.return_value = mock_json
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '45.00'
# call callback with GET
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id})
resp = app.get(callback_url, params=data)
assert resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
@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/')
resp = login(app).get(basket_page.get_online_url())
resp = resp.form.submit()
assert resp.status_code == 302
location = resp.location
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '10.50'
# call callback with GET
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():
raise Exception('kaboom')
mock_response.status_code = 500
mock_response.raise_for_status = kaboom
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 get_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
assert BasketItem.objects.get(id=item.id).payment_date
assert not BasketItem.objects.get(id=item.id).notification_date
# too soon
appconfig = apps.get_app_config('lingo')
appconfig.notify_payments()
assert BasketItem.objects.get(id=item.id).payment_date
assert not BasketItem.objects.get(id=item.id).notification_date
# fake delay
basket_item = BasketItem.objects.get(id=item.id)
basket_item.payment_date = timezone.now() - timedelta(hours=1)
basket_item.save()
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
mock_response = mock.Mock()
mock_response.status_code = 200
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 BasketItem.objects.get(id=item.id).payment_date
assert BasketItem.objects.get(id=item.id).notification_date
def test_payment_callback_not_found(app, user, regie):
data = {'transaction_id': 42, 'signed': True,
'amount': 42, 'ok': True}
callback_url = reverse('lingo-callback', kwargs={'regie_pk': 0})
app.get(callback_url, params=data, status=404)
callback_url = reverse('lingo-callback-payment-backend', kwargs={'payment_backend_pk': 0})
app.get(callback_url, params=data, status=404)
@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'
data = {'amount': 10, 'display_name': 'test item', 'url': source_url}
if authenticated:
data['NameId'] = user_name_id
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]
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]
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': ''
}
def test_request_payment_exception(app, basket_page, regie, user):
item = BasketItem.objects.create(user=user, regie=regie,
subject='test_item', amount='10.5',
source_url='http://example.org/testitem/')
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()
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/'
}
url = reverse('api-add-basket-item') + '?orig=wcs'
url = sign_url(url, key)
resp = app.post_json(url, params=data)
# check that an unpaid item exists in basket
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/'})
assert Transaction.objects.count() == 1
transaction = Transaction.objects.get()
order_id = transaction.order_id
# make sure the redirection is done to the payment backend
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
transaction_id = qs['transaction_id'][0],
data = {
'transaction_id': transaction_id,
'ok': True,
'amount': qs['amount'][0],
'signed': True,
}
mock_response = mock.Mock()
mock_response.status_code = 200
# simulate payment service redirecting the user to /lingo/return/... (eopayment
# dummy module put that URL in return_url query string parameter).
with mock.patch('eopayment.dummy.Payment.response') as eopayment_response:
return_value = eopayment_response.return_value
return_value.result = 1 # eopayment.RECEIVED
return_value.order_id = order_id
return_value.bank_data = {'a': 'b'}
return_value.transaction_id = '1234'
return_value.transaction_date = transaction_date
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request', return_value=mock_response) as request:
resp = app.get(qs['return_url'][0], params=data)
assert request.call_count == 0
# check transaction_date was recorded
transaction.refresh_from_db()
assert transaction.bank_transaction_date == transaction_date
if transaction_date is None:
assert 'no transaction date' in caplog.text
# send another transaction_date
with mock.patch('eopayment.dummy.Payment.response') as eopayment_response:
return_value = eopayment_response.return_value
return_value.result = 3 # eopayment.PAID
return_value.order_id = order_id
return_value.bank_data = {'a': 'b'}
return_value.transaction_id = '1234'
return_value.transaction_date = datetime(2020, 1, 2, 12, 0, tzinfo=utc)
with mock.patch('combo.utils.requests_wrapper.RequestsSession.post', return_value=mock_response) as post:
resp = app.get(qs['return_url'][0], params=data)
if transaction_date is None:
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'
transaction.refresh_from_db()
if transaction_date is None:
transaction.refresh_from_db()
assert transaction.bank_transaction_date == datetime(2020, 1, 2, 12, 0, tzinfo=utc)
else:
assert transaction.bank_transaction_date == transaction_date
assert 'new transaction_date for transaction' in caplog.text