This commit changes the way transactions linked to a document (sales order, invoice...) are created in a payment flow. Rather than receiving and trusting the transaction values from the controller, they are now read from the linked document, and the payment flow is rerouted to use the document's module's controllers instead of that of `payment`. This ensures that no unexpected value can be passed to the `create` method of a transaction, and simplifies the implementation of the payment flows of linked documents. task-3136240 closes odoo/odoo#126425 Related: odoo/enterprise#43212 Related: odoo/upgrade#5124 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
225 lines
7.9 KiB
Python
225 lines
7.9 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
from contextlib import contextmanager
|
|
from uuid import uuid4
|
|
|
|
from lxml import etree, objectify
|
|
from werkzeug import urls
|
|
|
|
from odoo.tests import HttpCase, JsonRpcException
|
|
|
|
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)
|
|
|
|
@contextmanager
|
|
def _assertNotFound(self):
|
|
with self.assertRaises(JsonRpcException) as cm:
|
|
yield
|
|
self.assertEqual(cm.exception.code, 404)
|
|
|
|
def _get_payment_context(self, response):
|
|
"""Extracts the payment context & other form info (provider & token ids)
|
|
from a payment response
|
|
|
|
:param response: http Response, with a payment form as text
|
|
: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(),
|
|
)
|
|
payment_form = html_tree.xpath('//form[@id="o_payment_form"]')[0]
|
|
values = {}
|
|
for key, val in payment_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']")
|
|
token_ids = []
|
|
payment_method_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'] == 'token':
|
|
token_ids.append(int(data['payment-option-id']))
|
|
else: # 'payment_method'
|
|
payment_method_ids.append(int(data['payment-option-id']))
|
|
|
|
values.update({
|
|
'token_ids': token_ids,
|
|
'payment_method_ids': payment_method_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 payment context 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_portal_pay_context(self, **route_kwargs):
|
|
response = self._portal_pay(**route_kwargs)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
return self._get_payment_context(response)
|
|
|
|
# /my/payment_method #
|
|
######################
|
|
|
|
def _portal_payment_method(self):
|
|
"""/my/payment_method payment context 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_portal_payment_method_context(self):
|
|
response = self._portal_payment_method()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
return self._get_payment_context(response)
|
|
|
|
# payment/transaction #
|
|
#######################
|
|
|
|
def _prepare_transaction_values(self, payment_method_id, token_id, flow):
|
|
""" Prepare the basic payment/transaction route values.
|
|
|
|
:param int payment_option_id: The payment option handling the transaction, as a
|
|
`payment.method` id or a `payment.token` id
|
|
:param str flow: The payment flow
|
|
:return: The route values
|
|
:rtype: dict
|
|
"""
|
|
return {
|
|
'provider_id': self.provider.id,
|
|
'payment_method_id': payment_method_id,
|
|
'token_id': token_id,
|
|
'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
|
|
),
|
|
'tokenization_requested': True,
|
|
'landing_route': 'Test',
|
|
'reference_prefix': 'test',
|
|
'is_validation': False,
|
|
'flow': flow,
|
|
}
|
|
|
|
def _portal_transaction(self, tx_route='/payment/transaction', **route_kwargs):
|
|
"""/payment/transaction feedback
|
|
|
|
:return: The response to the json request
|
|
"""
|
|
url = self._build_url(tx_route)
|
|
return self.make_jsonrpc_request(url, route_kwargs)
|
|
|
|
def _get_processing_values(self, **route_kwargs):
|
|
return self._portal_transaction(**route_kwargs)
|