File: http_common.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (224 lines) | stat: -rw-r--r-- 8,048 bytes parent folder | download
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)