1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
# 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)
|