mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 01:32:02 +02:00
payment
This commit is contained in:
parent
12c29a983b
commit
95fcc8bd63
189 changed files with 170858 additions and 0 deletions
9
odoo-bringout-oca-ocb-payment/payment/tests/__init__.py
Normal file
9
odoo-bringout-oca-ocb-payment/payment/tests/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import http_common
|
||||
from . import test_flows
|
||||
from . import test_multicompany_flows
|
||||
from . import test_payment_provider
|
||||
from . import test_payment_token
|
||||
from . import test_payment_transaction
|
||||
243
odoo-bringout-oca-ocb-payment/payment/tests/common.py
Normal file
243
odoo-bringout-oca-ocb-payment/payment/tests/common.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from lxml import objectify
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools.misc import hmac as hmac_tool
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentCommon(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.currency_euro = cls._prepare_currency('EUR')
|
||||
cls.currency_usd = cls._prepare_currency('USD')
|
||||
|
||||
cls.country_belgium = cls.env.ref('base.be')
|
||||
cls.country_france = cls.env.ref('base.fr')
|
||||
cls.europe = cls.env.ref('base.europe')
|
||||
|
||||
cls.group_user = cls.env.ref('base.group_user')
|
||||
cls.group_portal = cls.env.ref('base.group_portal')
|
||||
cls.group_public = cls.env.ref('base.group_public')
|
||||
|
||||
cls.admin_user = cls.env.ref('base.user_admin')
|
||||
cls.internal_user = cls.env['res.users'].create({
|
||||
'name': 'Internal User (Test)',
|
||||
'login': 'internal',
|
||||
'password': 'internal',
|
||||
'groups_id': [Command.link(cls.group_user.id)]
|
||||
})
|
||||
cls.portal_user = cls.env['res.users'].create({
|
||||
'name': 'Portal User (Test)',
|
||||
'login': 'payment_portal',
|
||||
'password': 'payment_portal',
|
||||
'groups_id': [Command.link(cls.group_portal.id)]
|
||||
})
|
||||
cls.public_user = cls.env.ref('base.public_user')
|
||||
|
||||
cls.admin_partner = cls.admin_user.partner_id
|
||||
cls.internal_partner = cls.internal_user.partner_id
|
||||
cls.portal_partner = cls.portal_user.partner_id
|
||||
cls.default_partner = cls.env['res.partner'].create({
|
||||
'name': 'Norbert Buyer',
|
||||
'lang': 'en_US',
|
||||
'email': 'norbert.buyer@example.com',
|
||||
'street': 'Huge Street',
|
||||
'street2': '2/543',
|
||||
'phone': '0032 12 34 56 78',
|
||||
'city': 'Sin City',
|
||||
'zip': '1000',
|
||||
'country_id': cls.country_belgium.id,
|
||||
})
|
||||
|
||||
# Create a dummy provider to allow basic tests without any specific provider implementation
|
||||
arch = """
|
||||
<form action="dummy" method="post">
|
||||
<input type="hidden" name="view_id" t-att-value="viewid"/>
|
||||
<input type="hidden" name="user_id" t-att-value="user_id.id"/>
|
||||
</form>
|
||||
""" # We exploit the default values `viewid` and `user_id` from QWeb's rendering context
|
||||
redirect_form = cls.env['ir.ui.view'].create({
|
||||
'name': "Dummy Redirect Form",
|
||||
'type': 'qweb',
|
||||
'arch': arch,
|
||||
})
|
||||
|
||||
cls.dummy_provider = cls.env['payment.provider'].create({
|
||||
'name': "Dummy Provider",
|
||||
'code': 'none',
|
||||
'state': 'test',
|
||||
'is_published': True,
|
||||
'allow_tokenization': True,
|
||||
'redirect_form_view_id': redirect_form.id,
|
||||
})
|
||||
|
||||
cls.provider = cls.dummy_provider
|
||||
cls.amount = 1111.11
|
||||
cls.company = cls.env.company
|
||||
cls.company_id = cls.company.id
|
||||
cls.currency = cls.currency_euro
|
||||
cls.partner = cls.default_partner
|
||||
cls.reference = "Test Transaction"
|
||||
|
||||
account_payment_module = cls.env['ir.module.module']._get('account_payment')
|
||||
cls.account_payment_installed = account_payment_module.state in ('installed', 'to upgrade')
|
||||
cls.enable_reconcile_after_done_patcher = True
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if self.account_payment_installed and self.enable_reconcile_after_done_patcher:
|
||||
# disable account payment generation if account_payment is installed
|
||||
# because the accounting setup of providers is not managed in this common
|
||||
self.reconcile_after_done_patcher = patch(
|
||||
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done',
|
||||
)
|
||||
self.startPatcher(self.reconcile_after_done_patcher)
|
||||
|
||||
#=== Utils ===#
|
||||
|
||||
@classmethod
|
||||
def _prepare_currency(cls, currency_code):
|
||||
currency = cls.env['res.currency'].with_context(active_test=False).search(
|
||||
[('name', '=', currency_code.upper())]
|
||||
)
|
||||
currency.action_unarchive()
|
||||
return currency
|
||||
|
||||
@classmethod
|
||||
def _prepare_provider(cls, code='none', company=None, update_values=None):
|
||||
""" Prepare and return the first provider matching the given provider and company.
|
||||
|
||||
If no provider is found in the given company, we duplicate the one from the base company.
|
||||
|
||||
All other providers belonging to the same company are disabled to avoid any interferences.
|
||||
|
||||
:param str code: The code of the provider to prepare
|
||||
:param recordset company: The company of the provider to prepare, as a `res.company` record
|
||||
:param dict update_values: The values used to update the provider
|
||||
:return: The provider to prepare, if found
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
company = company or cls.env.company
|
||||
update_values = update_values or {}
|
||||
|
||||
provider = cls.env['payment.provider'].sudo().search(
|
||||
[('code', '=', code), ('company_id', '=', company.id)], limit=1
|
||||
)
|
||||
if not provider:
|
||||
base_provider = cls.env['payment.provider'].sudo().search(
|
||||
[('code', '=', code)], limit=1
|
||||
)
|
||||
if not base_provider:
|
||||
_logger.error("no payment.provider found for code %s", code)
|
||||
return cls.env['payment.provider']
|
||||
else:
|
||||
provider = base_provider.copy({'company_id': company.id})
|
||||
|
||||
update_values['state'] = 'test'
|
||||
provider.write(update_values)
|
||||
return provider
|
||||
|
||||
@classmethod
|
||||
def _prepare_user(cls, user, group_xmlid):
|
||||
user.groups_id = [Command.link(cls.env.ref(group_xmlid).id)]
|
||||
# Flush and invalidate the cache to allow checking access rights.
|
||||
user.flush_recordset()
|
||||
user.invalidate_recordset()
|
||||
return user
|
||||
|
||||
def _create_transaction(self, flow, sudo=True, **values):
|
||||
default_values = {
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency.id,
|
||||
'provider_id': self.provider.id,
|
||||
'reference': self.reference,
|
||||
'operation': f'online_{flow}',
|
||||
'partner_id': self.partner.id,
|
||||
}
|
||||
return self.env['payment.transaction'].sudo(sudo).create(dict(default_values, **values))
|
||||
|
||||
def _create_token(self, sudo=True, **values):
|
||||
default_values = {
|
||||
'payment_details': "1234",
|
||||
'provider_id': self.provider.id,
|
||||
'partner_id': self.partner.id,
|
||||
'provider_ref': "provider Ref (TEST)",
|
||||
'active': True,
|
||||
}
|
||||
return self.env['payment.token'].sudo(sudo).create(dict(default_values, **values))
|
||||
|
||||
def _get_tx(self, reference):
|
||||
return self.env['payment.transaction'].sudo().search([
|
||||
('reference', '=', reference),
|
||||
])
|
||||
|
||||
def _generate_test_access_token(self, *values):
|
||||
""" Generate an access token based on the provided values for testing purposes.
|
||||
|
||||
This methods returns a token identical to that generated by
|
||||
payment.utils.generate_access_token but uses the test class environment rather than the
|
||||
environment of odoo.http.request.
|
||||
|
||||
See payment.utils.generate_access_token for additional details.
|
||||
|
||||
:param list values: The values to use for the generation of the token
|
||||
:return: The generated access token
|
||||
:rtype: str
|
||||
"""
|
||||
token_str = '|'.join(str(val) for val in values)
|
||||
access_token = hmac_tool(self.env(su=True), 'generate_access_token', token_str)
|
||||
return access_token
|
||||
|
||||
def _extract_values_from_html_form(self, html_form):
|
||||
""" Extract the transaction rendering values from an HTML form.
|
||||
|
||||
:param str html_form: The HTML form
|
||||
:return: The extracted information (action & inputs)
|
||||
:rtype: dict[str:str]
|
||||
"""
|
||||
html_tree = objectify.fromstring(html_form)
|
||||
if hasattr(html_tree, 'input'):
|
||||
inputs = {input_.get('name'): input_.get('value') for input_ in html_tree.input}
|
||||
else:
|
||||
inputs = {}
|
||||
return {
|
||||
'action': html_tree.get('action'),
|
||||
'method': html_tree.get('method'),
|
||||
'inputs': inputs,
|
||||
}
|
||||
|
||||
def _assert_does_not_raise(self, exception_class, func, *args, **kwargs):
|
||||
""" Fail if an exception of the provided class is raised when calling the function.
|
||||
|
||||
If an exception of any other class is raised, it is caught and silently ignored.
|
||||
|
||||
This method cannot be used with functions that make requests. Any exception raised in the
|
||||
scope of the new request will not be caught and will make the test fail.
|
||||
|
||||
:param class exception_class: The class of the exception to monitor.
|
||||
:param function fun: The function to call when monitoring for exceptions.
|
||||
:param list args: The positional arguments passed as-is to the called function.
|
||||
:param dict kwargs: The keyword arguments passed as-is to the called function.
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except exception_class:
|
||||
self.fail(f"{func.__name__} should not raise error of class {exception_class.__name__}")
|
||||
except Exception:
|
||||
pass # Any exception whose class is not monitored is caught and ignored.
|
||||
|
||||
def _skip_if_account_payment_is_not_installed(self):
|
||||
""" Skip current test if `account_payment` module is not installed. """
|
||||
if not self.account_payment_installed:
|
||||
self.skipTest("account_payment module is not installed")
|
||||
240
odoo-bringout-oca-ocb-payment/payment/tests/http_common.py
Normal file
240
odoo-bringout-oca-ocb-payment/payment/tests/http_common.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
from lxml import etree, objectify
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo.tests import HttpCase
|
||||
|
||||
from odoo.addons.payment.tests.common import PaymentCommon
|
||||
|
||||
|
||||
class PaymentHttpCommon(PaymentCommon, HttpCase):
|
||||
""" HttpCase common to build and simulate requests going through payment controllers.
|
||||
|
||||
Only use if you effectively want to test controllers.
|
||||
If you only want to test 'models' code, the PaymentCommon should be sufficient.
|
||||
"""
|
||||
|
||||
# Helpers #
|
||||
###########
|
||||
|
||||
def _build_url(self, route):
|
||||
return urls.url_join(self.base_url(), route)
|
||||
|
||||
def _make_http_get_request(self, url, params=None):
|
||||
""" Make an HTTP GET request to the provided URL.
|
||||
|
||||
:param str url: The URL to make the request to
|
||||
:param dict params: The parameters to be sent in the query string
|
||||
:return: The response of the request
|
||||
:rtype: :class:`requests.models.Response`
|
||||
"""
|
||||
formatted_params = self._format_http_request_payload(payload=params)
|
||||
return self.opener.get(url, params=formatted_params)
|
||||
|
||||
def _make_http_post_request(self, url, data=None):
|
||||
""" Make an HTTP POST request to the provided URL.
|
||||
|
||||
:param str url: The URL to make the request to
|
||||
:param dict data: The data to be send in the request body
|
||||
:return: The response of the request
|
||||
:rtype: :class:`requests.models.Response`
|
||||
"""
|
||||
formatted_data = self._format_http_request_payload(payload=data)
|
||||
return self.opener.post(url, data=formatted_data)
|
||||
|
||||
def _format_http_request_payload(self, payload=None):
|
||||
""" Format a request payload to replace float values by their string representation.
|
||||
|
||||
:param dict payload: The payload to format
|
||||
:return: The formatted payload
|
||||
:rtype: dict
|
||||
"""
|
||||
formatted_payload = {}
|
||||
if payload is not None:
|
||||
for k, v in payload.items():
|
||||
formatted_payload[k] = str(v) if isinstance(v, float) else v
|
||||
return formatted_payload
|
||||
|
||||
def _make_json_request(self, url, data=None):
|
||||
""" Make a JSON request to the provided URL.
|
||||
|
||||
:param str url: The URL to make the request to
|
||||
:param dict data: The data to be send in the request body in JSON format
|
||||
:return: The response of the request
|
||||
:rtype: :class:`requests.models.Response`
|
||||
"""
|
||||
return self.opener.post(url, json=data)
|
||||
|
||||
def _make_json_rpc_request(self, url, data=None):
|
||||
""" Make a JSON-RPC request to the provided URL.
|
||||
|
||||
:param str url: The URL to make the request to
|
||||
:param dict data: The data to be send in the request body in JSON-RPC 2.0 format
|
||||
:return: The response of the request
|
||||
:rtype: :class:`requests.models.Response`
|
||||
"""
|
||||
return self.opener.post(url, json={
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'call',
|
||||
'id': str(uuid4()),
|
||||
'params': data,
|
||||
})
|
||||
|
||||
def _get_tx_context(self, response, form_name):
|
||||
"""Extracts txContext & other form info (provider & token ids)
|
||||
from a payment response (with manage/checkout html form)
|
||||
|
||||
:param response: http Response, with a payment form as text
|
||||
:param str form_name: o_payment_manage / o_payment_checkout
|
||||
:return: Transaction context (+ provider_ids & token_ids)
|
||||
:rtype: dict
|
||||
"""
|
||||
# Need to specify an HTML parser as parser
|
||||
# Otherwise void elements (<img>, <link> without a closing / tag)
|
||||
# are considered wrong and trigger a lxml.etree.XMLSyntaxError
|
||||
html_tree = objectify.fromstring(
|
||||
response.text,
|
||||
parser=etree.HTMLParser(),
|
||||
)
|
||||
checkout_form = html_tree.xpath(f"//form[@name='{form_name}']")[0]
|
||||
values = {}
|
||||
for key, val in checkout_form.items():
|
||||
if key.startswith("data-"):
|
||||
formatted_key = key[5:].replace('-', '_')
|
||||
if formatted_key.endswith('_id'):
|
||||
formatted_val = int(val)
|
||||
elif formatted_key == 'amount':
|
||||
formatted_val = float(val)
|
||||
else:
|
||||
formatted_val = val
|
||||
values[formatted_key] = formatted_val
|
||||
|
||||
payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']")
|
||||
provider_ids = []
|
||||
token_ids = []
|
||||
for p_o_input in payment_options_inputs:
|
||||
data = dict()
|
||||
for key, val in p_o_input.items():
|
||||
if key.startswith('data-'):
|
||||
data[key[5:]] = val
|
||||
if data['payment-option-type'] == 'provider':
|
||||
provider_ids.append(int(data['payment-option-id']))
|
||||
else:
|
||||
token_ids.append(int(data['payment-option-id']))
|
||||
|
||||
values.update({
|
||||
'provider_ids': provider_ids,
|
||||
'token_ids': token_ids,
|
||||
})
|
||||
|
||||
return values
|
||||
|
||||
# payment/pay #
|
||||
###############
|
||||
|
||||
def _prepare_pay_values(self, amount=0.0, currency=None, reference='', partner=None):
|
||||
"""Prepare basic payment/pay route values
|
||||
|
||||
NOTE: needs PaymentCommon to enable fallback values.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
amount = amount or self.amount
|
||||
currency = currency or self.currency
|
||||
reference = reference or self.reference
|
||||
partner = partner or self.partner
|
||||
return {
|
||||
'amount': amount,
|
||||
'currency_id': currency.id,
|
||||
'reference': reference,
|
||||
'partner_id': partner.id,
|
||||
'access_token': self._generate_test_access_token(partner.id, amount, currency.id),
|
||||
}
|
||||
|
||||
def _portal_pay(self, **route_kwargs):
|
||||
"""/payment/pay txContext feedback
|
||||
|
||||
NOTE: must be authenticated before calling method.
|
||||
Or an access_token should be specified in route_kwargs
|
||||
"""
|
||||
uri = '/payment/pay'
|
||||
url = self._build_url(uri)
|
||||
return self._make_http_get_request(url, route_kwargs)
|
||||
|
||||
def _get_tx_checkout_context(self, **route_kwargs):
|
||||
response = self._portal_pay(**route_kwargs)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
return self._get_tx_context(response, 'o_payment_checkout')
|
||||
|
||||
# /my/payment_method #
|
||||
######################
|
||||
|
||||
def _portal_payment_method(self):
|
||||
"""/my/payment_method txContext feedback
|
||||
|
||||
NOTE: must be authenticated before calling method
|
||||
validation flow is restricted to logged users
|
||||
"""
|
||||
uri = '/my/payment_method'
|
||||
url = self._build_url(uri)
|
||||
return self._make_http_get_request(url, {})
|
||||
|
||||
def _get_tx_manage_context(self):
|
||||
response = self._portal_payment_method()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
return self._get_tx_context(response, 'o_payment_manage')
|
||||
|
||||
# payment/transaction #
|
||||
#######################
|
||||
|
||||
def _prepare_transaction_values(self, payment_option_id, flow):
|
||||
""" Prepare the basic payment/transaction route values.
|
||||
|
||||
:param int payment_option_id: The payment option handling the transaction, as a
|
||||
`payment.provider` id or a `payment.token` id
|
||||
:param str flow: The payment flow
|
||||
:return: The route values
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency.id,
|
||||
'partner_id': self.partner.id,
|
||||
'access_token': self._generate_test_access_token(
|
||||
self.partner.id, self.amount, self.currency.id
|
||||
),
|
||||
'payment_option_id': payment_option_id,
|
||||
'reference_prefix': 'test',
|
||||
'tokenization_requested': True,
|
||||
'landing_route': 'Test',
|
||||
'is_validation': False,
|
||||
'flow': flow,
|
||||
}
|
||||
|
||||
def _portal_transaction(self, **route_kwargs):
|
||||
"""/payment/transaction feedback
|
||||
|
||||
:return: The response to the json request
|
||||
"""
|
||||
uri = '/payment/transaction'
|
||||
url = self._build_url(uri)
|
||||
response = self._make_json_rpc_request(url, route_kwargs)
|
||||
self.assertEqual(response.status_code, 200) # Check the request went through.
|
||||
|
||||
return response
|
||||
|
||||
def _get_processing_values(self, **route_kwargs):
|
||||
response = self._portal_transaction(**route_kwargs)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
resp_content = json.loads(response.content)
|
||||
return resp_content['result']
|
||||
394
odoo-bringout-oca-ocb-payment/payment/tests/test_flows.py
Normal file
394
odoo-bringout-oca-ocb-payment/payment/tests/test_flows.py
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.payment.controllers.portal import PaymentPortal
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestFlows(PaymentHttpCommon):
|
||||
|
||||
def _test_flow(self, flow):
|
||||
""" Simulate the given online payment flow and tests the tx values at each step.
|
||||
|
||||
:param str flow: The online payment flow to test ('direct', 'redirect', or 'token')
|
||||
:return: The transaction created by the payment flow
|
||||
:rtype: recordset of `payment.transaction`
|
||||
"""
|
||||
self.reference = f"Test Transaction ({flow} - {self.partner.name})"
|
||||
route_values = self._prepare_pay_values()
|
||||
|
||||
# /payment/pay
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
for key, val in tx_context.items():
|
||||
if key in route_values:
|
||||
self.assertEqual(val, route_values[key])
|
||||
|
||||
self.assertIn(self.provider.id, tx_context['provider_ids'])
|
||||
|
||||
# Route values are taken from tx_context result of /pay route to correctly simulate the flow
|
||||
route_values = {
|
||||
k: tx_context[k]
|
||||
for k in [
|
||||
'amount',
|
||||
'currency_id',
|
||||
'reference_prefix',
|
||||
'partner_id',
|
||||
'access_token',
|
||||
'landing_route',
|
||||
]
|
||||
}
|
||||
route_values.update({
|
||||
'flow': flow,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
|
||||
if flow == 'token':
|
||||
route_values['payment_option_id'] = self._create_token().id
|
||||
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
# Tx values == given values
|
||||
self.assertEqual(tx_sudo.provider_id.id, self.provider.id)
|
||||
self.assertEqual(tx_sudo.amount, self.amount)
|
||||
self.assertEqual(tx_sudo.currency_id.id, self.currency.id)
|
||||
self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
|
||||
self.assertEqual(tx_sudo.reference, self.reference)
|
||||
|
||||
# processing_values == given values
|
||||
self.assertEqual(processing_values['provider_id'], self.provider.id)
|
||||
self.assertEqual(processing_values['amount'], self.amount)
|
||||
self.assertEqual(processing_values['currency_id'], self.currency.id)
|
||||
self.assertEqual(processing_values['partner_id'], self.partner.id)
|
||||
self.assertEqual(processing_values['reference'], self.reference)
|
||||
|
||||
# Verify computed values not provided, but added during the flow
|
||||
self.assertIn("tx_id=", tx_sudo.landing_route)
|
||||
self.assertIn("access_token=", tx_sudo.landing_route)
|
||||
|
||||
if flow == 'redirect':
|
||||
# In redirect flow, we verify the rendering of the dummy test form
|
||||
redirect_form_info = self._extract_values_from_html_form(
|
||||
processing_values['redirect_form_html'])
|
||||
|
||||
# Test content of rendered dummy redirect form
|
||||
self.assertEqual(redirect_form_info['action'], 'dummy')
|
||||
# Public user since we didn't authenticate with a specific user
|
||||
self.assertEqual(
|
||||
redirect_form_info['inputs']['user_id'],
|
||||
str(self.user.id))
|
||||
self.assertEqual(
|
||||
redirect_form_info['inputs']['view_id'],
|
||||
str(self.dummy_provider.redirect_form_view_id.id))
|
||||
|
||||
return tx_sudo
|
||||
|
||||
def test_10_direct_checkout_public(self):
|
||||
# No authentication needed, automatic fallback on public user
|
||||
self.user = self.public_user
|
||||
# Make sure the company considered in payment/pay
|
||||
# doesn't fall back on the public user main company (not the test one)
|
||||
self.partner.company_id = self.env.company.id
|
||||
self._test_flow('direct')
|
||||
|
||||
def test_11_direct_checkout_portal(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.user = self.portal_user
|
||||
self.partner = self.portal_partner
|
||||
self._test_flow('direct')
|
||||
|
||||
def test_12_direct_checkout_internal(self):
|
||||
self.authenticate(self.internal_user.login, self.internal_user.login)
|
||||
self.user = self.internal_user
|
||||
self.partner = self.internal_partner
|
||||
self._test_flow('direct')
|
||||
|
||||
def test_20_redirect_checkout_public(self):
|
||||
self.user = self.public_user
|
||||
# Make sure the company considered in payment/pay
|
||||
# doesn't fall back on the public user main company (not the test one)
|
||||
self.partner.company_id = self.env.company.id
|
||||
self._test_flow('redirect')
|
||||
|
||||
def test_21_redirect_checkout_portal(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.user = self.portal_user
|
||||
self.partner = self.portal_partner
|
||||
self._test_flow('redirect')
|
||||
|
||||
def test_22_redirect_checkout_internal(self):
|
||||
self.authenticate(self.internal_user.login, self.internal_user.login)
|
||||
self.user = self.internal_user
|
||||
self.partner = self.internal_partner
|
||||
self._test_flow('redirect')
|
||||
|
||||
# Payment by token #
|
||||
####################
|
||||
|
||||
# NOTE: not tested as public user because a public user cannot save payment details
|
||||
|
||||
def test_31_tokenize_portal(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self.user = self.portal_user
|
||||
self._test_flow('token')
|
||||
|
||||
def test_32_tokenize_internal(self):
|
||||
self.authenticate(self.internal_user.login, self.internal_user.login)
|
||||
self.partner = self.internal_partner
|
||||
self.user = self.internal_user
|
||||
self._test_flow('token')
|
||||
|
||||
# VALIDATION #
|
||||
##############
|
||||
|
||||
# NOTE: not tested as public user because the validation flow is only available when logged in
|
||||
|
||||
# freeze time for consistent singularize_prefix behavior during the test
|
||||
@freeze_time("2011-11-02 12:00:21")
|
||||
def _test_validation(self, flow):
|
||||
# Fixed with freezegun
|
||||
expected_reference = 'V-20111102120021'
|
||||
|
||||
validation_amount = self.provider._get_validation_amount()
|
||||
validation_currency = self.provider._get_validation_currency()
|
||||
|
||||
tx_context = self._get_tx_manage_context()
|
||||
expected_values = {
|
||||
'partner_id': self.partner.id,
|
||||
'access_token': self._generate_test_access_token(self.partner.id, None, None),
|
||||
'reference_prefix': expected_reference
|
||||
}
|
||||
for key, val in tx_context.items():
|
||||
if key in expected_values:
|
||||
self.assertEqual(val, expected_values[key])
|
||||
|
||||
transaction_values = {
|
||||
'amount': None,
|
||||
'currency_id': None,
|
||||
'partner_id': tx_context['partner_id'],
|
||||
'access_token': tx_context['access_token'],
|
||||
'flow': flow,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': True,
|
||||
'reference_prefix': tx_context['reference_prefix'],
|
||||
'landing_route': tx_context['landing_route'],
|
||||
'is_validation': True,
|
||||
}
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**transaction_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
# Tx values == given values
|
||||
self.assertEqual(tx_sudo.provider_id.id, self.provider.id)
|
||||
self.assertEqual(tx_sudo.amount, validation_amount)
|
||||
self.assertEqual(tx_sudo.currency_id.id, validation_currency.id)
|
||||
self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
|
||||
self.assertEqual(tx_sudo.reference, expected_reference)
|
||||
# processing_values == given values
|
||||
self.assertEqual(processing_values['provider_id'], self.provider.id)
|
||||
self.assertEqual(processing_values['amount'], validation_amount)
|
||||
self.assertEqual(processing_values['currency_id'], validation_currency.id)
|
||||
self.assertEqual(processing_values['partner_id'], self.partner.id)
|
||||
self.assertEqual(processing_values['reference'], expected_reference)
|
||||
|
||||
def test_51_validation_direct_portal(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self._test_validation(flow='direct')
|
||||
|
||||
def test_52_validation_direct_internal(self):
|
||||
self.authenticate(self.internal_user.login, self.internal_user.login)
|
||||
self.partner = self.internal_partner
|
||||
self._test_validation(flow='direct')
|
||||
|
||||
def test_61_validation_redirect_portal(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self._test_validation(flow='direct')
|
||||
|
||||
def test_62_validation_redirect_internal(self):
|
||||
self.authenticate(self.internal_user.login, self.internal_user.login)
|
||||
self.partner = self.internal_partner
|
||||
self._test_validation(flow='direct')
|
||||
|
||||
# Specific flows #
|
||||
##################
|
||||
|
||||
def test_pay_redirect_if_no_partner_exist(self):
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values.pop('partner_id')
|
||||
|
||||
# Pay without a partner specified --> redirection to login page
|
||||
response = self._portal_pay(**route_values)
|
||||
url = urlparse(response.url)
|
||||
self.assertEqual(url.path, '/web/login')
|
||||
self.assertIn('redirect', parse_qs(url.query))
|
||||
|
||||
# Pay without a partner specified (but logged) --> pay with the partner of current user.
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
self.assertEqual(tx_context['partner_id'], self.portal_partner.id)
|
||||
|
||||
def test_pay_no_token(self):
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values.pop('partner_id')
|
||||
route_values.pop('access_token')
|
||||
|
||||
# Pay without a partner specified --> redirection to login page
|
||||
response = self._portal_pay(**route_values)
|
||||
url = urlparse(response.url)
|
||||
self.assertEqual(url.path, '/web/login')
|
||||
self.assertIn('redirect', parse_qs(url.query))
|
||||
|
||||
# Pay without a partner specified (but logged) --> pay with the partner of current user.
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
self.assertEqual(tx_context['partner_id'], self.portal_partner.id)
|
||||
|
||||
def test_pay_wrong_token(self):
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['access_token'] = "abcde"
|
||||
|
||||
# Pay with a wrong access token --> Not found (404)
|
||||
response = self._portal_pay(**route_values)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_pay_wrong_currency(self):
|
||||
# Pay with a wrong currency --> Not found (404)
|
||||
self.currency = self.env['res.currency'].browse(self.env['res.currency'].search([], order='id desc', limit=1).id + 1000)
|
||||
route_values = self._prepare_pay_values()
|
||||
response = self._portal_pay(**route_values)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Pay with an inactive currency --> Not found (404)
|
||||
self.currency = self.env['res.currency'].search([('active', '=', False)], limit=1)
|
||||
route_values = self._prepare_pay_values()
|
||||
response = self._portal_pay(**route_values)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_transaction_wrong_flow(self):
|
||||
transaction_values = self._prepare_pay_values()
|
||||
transaction_values.update({
|
||||
'flow': 'this flow does not exist',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'reference_prefix': 'whatever',
|
||||
'landing_route': 'whatever',
|
||||
})
|
||||
# Transaction step with a wrong flow --> UserError
|
||||
with mute_logger('odoo.http'):
|
||||
response = self._portal_transaction(**transaction_values)
|
||||
self.assertIn(
|
||||
"odoo.exceptions.UserError: The payment should either be direct, with redirection, or made by a token.",
|
||||
response.text)
|
||||
|
||||
def test_transaction_wrong_token(self):
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['access_token'] = "abcde"
|
||||
|
||||
# Transaction step with a wrong access token --> ValidationError
|
||||
with mute_logger('odoo.http'):
|
||||
response = self._portal_transaction(**route_values)
|
||||
self.assertIn(
|
||||
"odoo.exceptions.ValidationError: The access token is invalid.",
|
||||
response.text)
|
||||
|
||||
def test_access_disabled_providers_tokens(self):
|
||||
self.partner = self.portal_partner
|
||||
|
||||
# Log in as user from Company A
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
token = self._create_token()
|
||||
provider_b = self.provider.copy()
|
||||
provider_b.state = 'test'
|
||||
token_b = self._create_token(provider_id=provider_b.id)
|
||||
|
||||
# User must see both enabled providers and tokens
|
||||
manage_context = self._get_tx_manage_context()
|
||||
self.assertEqual(manage_context['partner_id'], self.partner.id)
|
||||
self.assertIn(self.provider.id, manage_context['provider_ids'])
|
||||
self.assertIn(provider_b.id, manage_context['provider_ids'])
|
||||
self.assertIn(token.id, manage_context['token_ids'])
|
||||
self.assertIn(token_b.id, manage_context['token_ids'])
|
||||
|
||||
# Token of disabled provider(s) & disabled providers should not be shown
|
||||
self.provider.state = 'disabled'
|
||||
manage_context = self._get_tx_manage_context()
|
||||
self.assertEqual(manage_context['partner_id'], self.partner.id)
|
||||
self.assertEqual(manage_context['provider_ids'], [provider_b.id])
|
||||
self.assertEqual(manage_context['token_ids'], [token_b.id])
|
||||
|
||||
# Archived tokens must be hidden from the user
|
||||
token_b.active = False
|
||||
manage_context = self._get_tx_manage_context()
|
||||
self.assertEqual(manage_context['partner_id'], self.partner.id)
|
||||
self.assertEqual(manage_context['provider_ids'], [provider_b.id])
|
||||
self.assertEqual(manage_context['token_ids'], [])
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
def test_direct_payment_triggers_no_payment_request(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self.user = self.portal_user
|
||||
with patch(
|
||||
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
|
||||
'._send_payment_request'
|
||||
) as patched:
|
||||
self._portal_transaction(
|
||||
**self._prepare_transaction_values(self.provider.id, 'direct')
|
||||
)
|
||||
self.assertEqual(patched.call_count, 0)
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
def test_payment_with_redirect_triggers_no_payment_request(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self.user = self.portal_user
|
||||
with patch(
|
||||
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
|
||||
'._send_payment_request'
|
||||
) as patched:
|
||||
self._portal_transaction(
|
||||
**self._prepare_transaction_values(self.provider.id, 'redirect')
|
||||
)
|
||||
self.assertEqual(patched.call_count, 0)
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
def test_payment_by_token_triggers_exactly_one_payment_request(self):
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self.partner = self.portal_partner
|
||||
self.user = self.portal_user
|
||||
with patch(
|
||||
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
|
||||
'._send_payment_request'
|
||||
) as patched:
|
||||
self._portal_transaction(
|
||||
**self._prepare_transaction_values(self._create_token().id, 'token')
|
||||
)
|
||||
self.assertEqual(patched.call_count, 1)
|
||||
|
||||
def test_tokenization_input_is_show_to_logged_in_users(self):
|
||||
self.provider.allow_tokenization = True
|
||||
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
|
||||
self.provider, logged_in=True
|
||||
)
|
||||
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
|
||||
|
||||
def test_tokenization_input_is_hidden_for_logged_out_users(self):
|
||||
self.provider.allow_tokenization = False
|
||||
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
|
||||
self.provider, logged_in=True
|
||||
)
|
||||
self.assertDictEqual(show_tokenize_input, {self.provider.id: False})
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestMultiCompanyFlows(PaymentHttpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.company_a = cls.env.company # cls.company_data['company']
|
||||
cls.company_b = cls.env.company.create({'name': "Payment Test Company"}) # cls.company_data_2['company']
|
||||
|
||||
cls.user_company_a = cls.internal_user
|
||||
cls.user_company_b = cls.env['res.users'].create({
|
||||
'name': f"{cls.company_b.name} User (TEST)",
|
||||
'login': 'user_company_b',
|
||||
'password': 'user_company_b',
|
||||
'company_id': cls.company_b.id,
|
||||
'company_ids': [Command.set(cls.company_b.ids)],
|
||||
'groups_id': [Command.link(cls.group_user.id)],
|
||||
})
|
||||
cls.user_multi_company = cls.env['res.users'].create({
|
||||
'name': "Multi Company User (TEST)",
|
||||
'login': 'user_multi_company',
|
||||
'password': 'user_multi_company',
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [Command.set([cls.company_a.id, cls.company_b.id])],
|
||||
'groups_id': [Command.link(cls.group_user.id)],
|
||||
})
|
||||
|
||||
cls.provider_company_b = cls._prepare_provider(company=cls.company_b)
|
||||
|
||||
def test_pay_logged_in_another_company(self):
|
||||
"""User pays for an amount in another company."""
|
||||
# for another res.partner than the user's one
|
||||
route_values = self._prepare_pay_values(partner=self.user_company_b.partner_id)
|
||||
|
||||
# Log in as user from Company A
|
||||
self.authenticate(self.user_company_a.login, self.user_company_a.login)
|
||||
|
||||
# Pay in company B
|
||||
route_values['company_id'] = self.company_b.id
|
||||
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
for key, val in tx_context.items():
|
||||
if key in route_values:
|
||||
if key == 'access_token':
|
||||
continue # access_token was modified due to the change of partner.
|
||||
elif key == 'partner_id':
|
||||
# The partner is replaced by the partner of the user paying.
|
||||
self.assertEqual(val, self.user_company_a.partner_id.id)
|
||||
else:
|
||||
self.assertEqual(val, route_values[key])
|
||||
|
||||
available_providers = self.env['payment.provider'].sudo().browse(tx_context['provider_ids'])
|
||||
self.assertIn(self.provider_company_b, available_providers)
|
||||
self.assertEqual(available_providers.company_id, self.company_b)
|
||||
|
||||
validation_values = {
|
||||
k: tx_context[k]
|
||||
for k in [
|
||||
'amount',
|
||||
'currency_id',
|
||||
'reference_prefix',
|
||||
'partner_id',
|
||||
'access_token',
|
||||
'landing_route',
|
||||
]
|
||||
}
|
||||
validation_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider_company_b.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**validation_values)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
# Tx values == given values
|
||||
self.assertEqual(tx_sudo.provider_id.id, self.provider_company_b.id)
|
||||
self.assertEqual(tx_sudo.amount, self.amount)
|
||||
self.assertEqual(tx_sudo.currency_id.id, self.currency.id)
|
||||
self.assertEqual(tx_sudo.partner_id.id, self.user_company_a.partner_id.id)
|
||||
self.assertEqual(tx_sudo.reference, self.reference)
|
||||
self.assertEqual(tx_sudo.company_id, self.company_b)
|
||||
# processing_values == given values
|
||||
self.assertEqual(processing_values['provider_id'], self.provider_company_b.id)
|
||||
self.assertEqual(processing_values['amount'], self.amount)
|
||||
self.assertEqual(processing_values['currency_id'], self.currency.id)
|
||||
self.assertEqual(processing_values['partner_id'], self.user_company_a.partner_id.id)
|
||||
self.assertEqual(processing_values['reference'], self.reference)
|
||||
|
||||
def test_full_access_to_partner_tokens(self):
|
||||
self.partner = self.portal_partner
|
||||
|
||||
# Log in as user from Company A
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
token = self._create_token()
|
||||
token_company_b = self._create_token(provider_id=self.provider_company_b.id)
|
||||
|
||||
# A partner should see all his tokens on the /my/payment_method route,
|
||||
# even if they are in other companies otherwise he won't ever see them.
|
||||
manage_context = self._get_tx_manage_context()
|
||||
self.assertEqual(manage_context['partner_id'], self.partner.id)
|
||||
self.assertEqual(manage_context['provider_ids'], self.provider.ids)
|
||||
self.assertIn(token.id, manage_context['token_ids'])
|
||||
self.assertIn(token_company_b.id, manage_context['token_ids'])
|
||||
|
||||
def test_archive_token_logged_in_another_company(self):
|
||||
"""User archives his token from another company."""
|
||||
# get user's token from company A
|
||||
token = self._create_token(partner_id=self.portal_partner.id)
|
||||
|
||||
# assign user to another company
|
||||
company_b = self.env['res.company'].create({'name': 'Company B'})
|
||||
self.portal_user.write({'company_ids': [company_b.id], 'company_id': company_b.id})
|
||||
|
||||
# Log in as portal user
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
# Archive token in company A
|
||||
url = self._build_url('/payment/archive_token')
|
||||
self._make_json_rpc_request(url, {'token_id': token.id})
|
||||
|
||||
self.assertFalse(token.active)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.payment.tests.common import PaymentCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPaymentprovider(PaymentCommon):
|
||||
|
||||
def test_published_provider_compatible_with_all_users(self):
|
||||
""" Test that a published provider is always available to all users. """
|
||||
for user in (self.public_user, self.portal_user):
|
||||
self.env = self.env(user=user)
|
||||
|
||||
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount
|
||||
)
|
||||
self.assertIn(self.provider, compatible_providers)
|
||||
|
||||
def test_unpublished_provider_compatible_with_internal_user(self):
|
||||
""" Test that an unpublished provider is still available to internal users. """
|
||||
self.provider.is_published = False
|
||||
|
||||
compatible_providers = self.env['payment.provider']._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount
|
||||
)
|
||||
self.assertIn(self.provider, compatible_providers)
|
||||
|
||||
def test_unpublished_provider_not_compatible_with_non_internal_user(self):
|
||||
""" Test that an unpublished provider is not available to non-internal users. """
|
||||
self.provider.is_published = False
|
||||
for user in (self.public_user, self.portal_user):
|
||||
self.env = self.env(user=user)
|
||||
|
||||
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount
|
||||
)
|
||||
self.assertNotIn(self.provider, compatible_providers)
|
||||
|
||||
def test_provider_compatible_when_maximum_amount_is_zero(self):
|
||||
""" Test that the maximum amount has no effect on the provider's compatibility when it is
|
||||
set to 0. """
|
||||
self.provider.maximum_amount = 0.
|
||||
|
||||
compatible_providers = self.env['payment.provider']._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
|
||||
)
|
||||
self.assertIn(self.provider, compatible_providers)
|
||||
|
||||
def test_provider_compatible_when_payment_below_maximum_amount(self):
|
||||
""" Test that a provider is compatible when the payment amount is less than the maximum
|
||||
amount. """
|
||||
self.provider.maximum_amount = self.amount + 10.0
|
||||
|
||||
compatible_providers = self.env['payment.provider']._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
|
||||
)
|
||||
self.assertIn(self.provider, compatible_providers)
|
||||
|
||||
def test_provider_not_compatible_when_payment_above_maximum_amount(self):
|
||||
""" Test that a provider is not compatible when the payment amount is more than the maximum
|
||||
amount. """
|
||||
self.provider.maximum_amount = self.amount - 10.0
|
||||
|
||||
compatible_providers = self.env['payment.provider']._get_compatible_providers(
|
||||
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
|
||||
)
|
||||
self.assertNotIn(self.provider, compatible_providers)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.payment.tests.common import PaymentCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPaymentToken(PaymentCommon):
|
||||
|
||||
def test_token_cannot_be_unarchived(self):
|
||||
""" Test that unarchiving disabled tokens is forbidden. """
|
||||
token = self._create_token(active=False)
|
||||
with self.assertRaises(UserError):
|
||||
token.active = True
|
||||
|
||||
def test_display_name_is_padded(self):
|
||||
""" Test that the display name is built by padding the payment details. """
|
||||
token = self._create_token()
|
||||
self.assertEqual(token._build_display_name(), '•••• 1234')
|
||||
|
||||
@freeze_time('2024-1-31 10:00:00')
|
||||
def test_display_name_for_empty_payment_details(self):
|
||||
""" Test that the display name is still built for token without payment details. """
|
||||
token = self._create_token(payment_details='')
|
||||
self.env.cr.execute(
|
||||
'UPDATE payment_token SET create_date = %s WHERE id = %s',
|
||||
params=(date.today(), token.id),
|
||||
)
|
||||
token.invalidate_recordset(fnames=['create_date'])
|
||||
self.assertEqual(
|
||||
token._build_display_name(),
|
||||
f"Payment details saved on {date.today().strftime('%Y/%m/%d')}",
|
||||
)
|
||||
|
||||
def test_display_name_is_shortened_to_max_length(self):
|
||||
""" Test that the display name is not fully padded when a `max_length` is passed. """
|
||||
token = self._create_token()
|
||||
self.assertEqual(token._build_display_name(max_length=6), '• 1234')
|
||||
|
||||
def test_display_name_is_not_padded(self):
|
||||
""" Test that the display name is not padded when `should_pad` is `False`. """
|
||||
token = self._create_token()
|
||||
self.assertEqual(token._build_display_name(should_pad=False), '1234')
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.payment.tests.common import PaymentCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPaymentTransaction(PaymentCommon):
|
||||
|
||||
def test_capture_allowed_for_authorized_users(self):
|
||||
""" Test that users who have access to a transaction can capture it. """
|
||||
if 'account' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest("account module is not installed")
|
||||
self.provider.support_manual_capture = True
|
||||
tx = self._create_transaction('redirect', state='authorized')
|
||||
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
|
||||
self._assert_does_not_raise(AccessError, tx.with_user(user).action_capture)
|
||||
|
||||
def test_void_allowed_for_authorized_users(self):
|
||||
""" Test that users who have access to a transaction can void it. """
|
||||
if 'account' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest("account module is not installed")
|
||||
self.provider.support_manual_capture = True
|
||||
tx = self._create_transaction('redirect', state='authorized')
|
||||
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
|
||||
self._assert_does_not_raise(AccessError, tx.with_user(user).action_void)
|
||||
|
||||
def test_refund_allowed_for_authorized_users(self):
|
||||
""" Test that users who have access to a transaction can refund it. """
|
||||
if 'account' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest("account module is not installed")
|
||||
self.provider.support_refund = 'full_only'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
|
||||
self._assert_does_not_raise(AccessError, tx.with_user(user).action_refund)
|
||||
|
||||
def test_capture_blocked_for_unauthorized_user(self):
|
||||
""" Test that users who don't have access to a transaction cannot capture it. """
|
||||
self.provider.support_manual_capture = True
|
||||
tx = self._create_transaction('redirect', state='authorized')
|
||||
self.assertRaises(AccessError, tx.with_user(self.internal_user).action_capture)
|
||||
|
||||
def test_void_blocked_for_unauthorized_user(self):
|
||||
""" Test that users who don't have access to a transaction cannot void it. """
|
||||
self.provider.support_manual_capture = True
|
||||
tx = self._create_transaction('redirect', state='authorized')
|
||||
self.assertRaises(AccessError, tx.with_user(self.internal_user).action_void)
|
||||
|
||||
def test_refund_blocked_for_unauthorized_user(self):
|
||||
""" Test that users who don't have access to a transaction cannot refund it. """
|
||||
self.provider.support_refund = 'full_only'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
self.assertRaises(AccessError, tx.with_user(self.internal_user).action_refund)
|
||||
|
||||
def test_refunds_count(self):
|
||||
self.provider.support_refund = 'full_only' # Should simply not be False
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
for reference_index, operation in enumerate(
|
||||
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
|
||||
):
|
||||
self._create_transaction(
|
||||
'dummy',
|
||||
reference=f'R-{tx.reference}-{reference_index + 1}',
|
||||
state='done',
|
||||
operation=operation, # Override the computed flow
|
||||
source_transaction_id=tx.id,
|
||||
)._reconcile_after_done()
|
||||
|
||||
self.assertEqual(
|
||||
tx.refunds_count,
|
||||
1,
|
||||
msg="The refunds count should only consider transactions with operation 'refund'."
|
||||
)
|
||||
|
||||
def test_refund_transaction_values(self):
|
||||
self.provider.support_refund = 'partial'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
|
||||
# Test the default values of a full refund transaction
|
||||
refund_tx = tx._create_refund_transaction()
|
||||
self.assertEqual(
|
||||
refund_tx.reference,
|
||||
f'R-{tx.reference}',
|
||||
msg="The reference of the refund transaction should be the prefixed reference of the "
|
||||
"source transaction."
|
||||
)
|
||||
self.assertLess(
|
||||
refund_tx.amount, 0, msg="The amount of a refund transaction should always be negative."
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
refund_tx.amount,
|
||||
-tx.amount,
|
||||
places=2,
|
||||
msg="The amount of the refund transaction should be taken from the amount of the "
|
||||
"source transaction."
|
||||
)
|
||||
self.assertEqual(
|
||||
refund_tx.currency_id,
|
||||
tx.currency_id,
|
||||
msg="The currency of the refund transaction should that of the source transaction."
|
||||
)
|
||||
self.assertEqual(
|
||||
refund_tx.operation,
|
||||
'refund',
|
||||
msg="The operation of the refund transaction should be 'refund'."
|
||||
)
|
||||
self.assertEqual(
|
||||
tx,
|
||||
refund_tx.source_transaction_id,
|
||||
msg="The refund transaction should be linked to the source transaction."
|
||||
)
|
||||
self.assertEqual(
|
||||
refund_tx.partner_id,
|
||||
tx.partner_id,
|
||||
msg="The partner of the refund transaction should that of the source transaction."
|
||||
)
|
||||
|
||||
# Test the values of a partial refund transaction with custom refund amount
|
||||
partial_refund_tx = tx._create_refund_transaction(amount_to_refund=11.11)
|
||||
self.assertAlmostEqual(
|
||||
partial_refund_tx.amount,
|
||||
-11.11,
|
||||
places=2,
|
||||
msg="The amount of the refund transaction should be the negative value of the amount "
|
||||
"to refund."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue