From 4111f02fb3cc4cdf3dd94adae0ca80cfe874170c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 13 Jul 2023 07:21:56 +0000 Subject: [PATCH] [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 --- odoo/tests/common.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 2fa3849de7f..6c6644a4f70 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -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)