[ADD] core: block requests calls during tests

Non-external tests should not be performing requests to external
websites or services.

Add a mock to handle such requests:

- if the test is not tagged external
- and the request is not to localhost
- and the request is not to `file:` (it's possible to install a
  FileAdapter in a session to resolve file URLs)
- raise a connection error (and log, both with some details of the
  blocked request for debugging)

The mock is layered on the lowest possible level (`Session.send`), so
tests can either:

- reconfigure the mock to handle cases differently (the mock is
  re-created on every test)
- layer their own mock at a higher level of the library

Eventually we might also built-in a routing / dispatch mechanism so
it's easier to declare external services you want to mock, somewhat
similar to what's available client-side.

Whitelist `file:` because e.g. zeep performs `file:` request, using a
bespoke adapter installed in its session.

Part-of: odoo/odoo#128497
This commit is contained in:
Xavier Morel
2023-07-15 12:00:11 +02:00
parent 5e4dbe2616
commit 4111f02fb3
+29 -1
View File
@@ -43,6 +43,7 @@ from xmlrpc import client as xmlrpclib
import requests
import werkzeug.urls
from lxml import etree, html
from requests import PreparedRequest, Session
import odoo
from odoo import api
@@ -243,7 +244,7 @@ def _normalize_arch_for_assert(arch_string, parser_method="xml"):
arch_string = etree.fromstring(arch_string, parser=parser)
return etree.tostring(arch_string, pretty_print=True, encoding='unicode')
_super_send = requests.Session.send
class BaseCase(case.TestCase, metaclass=MetaCase):
""" Subclass of TestCase for Odoo-specific code. This class is abstract and
expects self.registry, self.cr and self.uid to be initialized by subclasses.
@@ -257,6 +258,33 @@ class BaseCase(case.TestCase, metaclass=MetaCase):
super().__init__(methodName)
self.addTypeEqualityFunc(etree._Element, self.assertTreesEqual)
self.addTypeEqualityFunc(html.HtmlElement, self.assertTreesEqual)
self.request_mock = None
def setUp(self):
super().setUp()
if 'external' not in self.test_tags:
# if the method is passed directly `patch` discards the session
# object which we need
# pylint: disable=unnecessary-lambda
self.patch(
requests.sessions.Session,
'send',
lambda s, r, **kwargs: self._request_handler(s, r, **kwargs),
)
def _request_handler(self, s: Session, r: PreparedRequest, /, **kw):
# allow localhost requests
# TODO: also check port?
url = werkzeug.urls.url_parse(r.url)
if url.host in (HOST, 'localhost'):
return _super_send(s, r, **kw)
if url.scheme == 'file':
return _super_send(s, r, **kw)
_logger.getChild('requests').info(
"Blocking un-mocked external HTTP request %s %s", r.method, r.url)
raise requests.exceptions.ConnectionError(
f"External requests verboten (was {r.method} {r.url})")
def run(self, result):
testMethod = getattr(self, self._testMethodName)