From fa5933ccb984a4f3fdddfa3a49cfe2f9337e968c Mon Sep 17 00:00:00 2001 From: ento Date: Fri, 15 Dec 2023 23:41:33 -0800 Subject: [PATCH 1/7] Allow requests to specified host/ports to go through in strict mode --- mocket/async_mocket.py | 11 +++++++++-- mocket/mocket.py | 23 +++++++++++++++++++---- mocket/utils.py | 12 ++++++++++++ tests/main/test_mode.py | 11 +++++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/mocket/async_mocket.py b/mocket/async_mocket.py index 5ebe7348..936ec22d 100644 --- a/mocket/async_mocket.py +++ b/mocket/async_mocket.py @@ -3,9 +3,16 @@ async def wrapper( - test, truesocket_recording_dir=None, strict_mode=False, *args, **kwargs + test, + truesocket_recording_dir=None, + strict_mode=False, + strict_mode_allowed=None, + *args, + **kwargs ): - async with Mocketizer.factory(test, truesocket_recording_dir, strict_mode, args): + async with Mocketizer.factory( + test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args + ): return await test(*args, **kwargs) diff --git a/mocket/mocket.py b/mocket/mocket.py index 2b4d0c0f..95825b37 100644 --- a/mocket/mocket.py +++ b/mocket/mocket.py @@ -311,7 +311,10 @@ def recv(self, buffersize, flags=None): def true_sendall(self, data, *args, **kwargs): if MocketMode().STRICT: - raise StrictMocketException("Mocket tried to use the real `socket` module.") + if not MocketMode().allowed((self._host, self._port)): + raise StrictMocketException( + "Mocket tried to use the real `socket` module." + ) req = decode_from_bytes(data) # make request unique again @@ -639,11 +642,13 @@ def __init__( namespace=None, truesocket_recording_dir=None, strict_mode=False, + strict_mode_allowed=None, ): self.instance = instance self.truesocket_recording_dir = truesocket_recording_dir self.namespace = namespace or text_type(id(self)) MocketMode().STRICT = strict_mode + MocketMode().STRICT_ALLOWED = strict_mode_allowed def enter(self): Mocket.enable( @@ -678,7 +683,7 @@ def check_and_call(self, method_name): method() @staticmethod - def factory(test, truesocket_recording_dir, strict_mode, args): + def factory(test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args): instance = args[0] if args else None namespace = None if truesocket_recording_dir: @@ -695,11 +700,21 @@ def factory(test, truesocket_recording_dir, strict_mode, args): namespace=namespace, truesocket_recording_dir=truesocket_recording_dir, strict_mode=strict_mode, + strict_mode_allowed=strict_mode_allowed, ) -def wrapper(test, truesocket_recording_dir=None, strict_mode=False, *args, **kwargs): - with Mocketizer.factory(test, truesocket_recording_dir, strict_mode, args): +def wrapper( + test, + truesocket_recording_dir=None, + strict_mode=False, + strict_mode_allowed=None, + *args, + **kwargs +): + with Mocketizer.factory( + test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args + ): return test(*args, **kwargs) diff --git a/mocket/utils.py b/mocket/utils.py index 64a2c18e..88d7868c 100644 --- a/mocket/utils.py +++ b/mocket/utils.py @@ -47,6 +47,18 @@ def get_mocketize(wrapper_): class MocketMode: __shared_state = {} STRICT = None + STRICT_ALLOWED = None def __init__(self): self.__dict__ = self.__shared_state + + def allowed(self, location): + if not self.STRICT_ALLOWED: + return False + host, port = location + for allowed in self.STRICT_ALLOWED: + if isinstance(allowed, str) and host == allowed: + return True + elif location == allowed: + return True + return False diff --git a/tests/main/test_mode.py b/tests/main/test_mode.py index b104589c..0e9a7893 100644 --- a/tests/main/test_mode.py +++ b/tests/main/test_mode.py @@ -26,3 +26,14 @@ def test_intermittent_strict_mode(): with Mocketizer(strict_mode=False): requests.get(url) + + +@pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)') +def test_strict_mode_exceptions(): + url = "http://httpbin.local/ip" + + with Mocketizer(strict_mode=True, strict_mode_allowed=["httpbin.local"]): + requests.get(url) + + with Mocketizer(strict_mode=True, strict_mode_allowed=[("httpbin.local", 80)]): + requests.get(url) From cca9d00852a98dbcd5351c9da5da55f7ce9d2dcd Mon Sep 17 00:00:00 2001 From: ento Date: Mon, 18 Dec 2023 10:41:18 -0800 Subject: [PATCH 2/7] More verbose error message for StrictMocketException --- mocket/mocket.py | 16 +++++++++++++++- mocket/mockhttp.py | 12 ++++++++++++ tests/main/test_mode.py | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/mocket/mocket.py b/mocket/mocket.py index 95825b37..2100590e 100644 --- a/mocket/mocket.py +++ b/mocket/mocket.py @@ -312,8 +312,19 @@ def recv(self, buffersize, flags=None): def true_sendall(self, data, *args, **kwargs): if MocketMode().STRICT: if not MocketMode().allowed((self._host, self._port)): + current_entries = [ + (location, "\n ".join(map(str, entries))) + for location, entries in Mocket._entries.items() + ] + formatted_entries = "\n".join( + [ + f" {location}:\n {entries}" + for location, entries in current_entries + ] + ) raise StrictMocketException( - "Mocket tried to use the real `socket` module." + "Mocket tried to use the real `socket` module while strict mode is active.\n" + f"Registered entries:\n{formatted_entries}" ) req = decode_from_bytes(data) @@ -614,6 +625,9 @@ def __init__(self, location, responses): r = self.response_cls(r) self.responses.append(r) + def __repr__(self): + return "{}(location={})".format(self.__class__.__name__, self.location) + @staticmethod def can_handle(data): return True diff --git a/mocket/mockhttp.py b/mocket/mockhttp.py index 8cb5cdc6..60b134a1 100644 --- a/mocket/mockhttp.py +++ b/mocket/mockhttp.py @@ -175,6 +175,18 @@ def __init__(self, uri, method, responses, match_querystring=True): self._sent_data = b"" self._match_querystring = match_querystring + def __repr__(self): + return ( + "{}(method={!r}, schema={!r}, location={!r}, path={!r}, query={!r})".format( + self.__class__.__name__, + self.method, + self.schema, + self.location, + self.path, + self.query, + ) + ) + def collect(self, data): consume_response = True diff --git a/tests/main/test_mode.py b/tests/main/test_mode.py index 0e9a7893..e3a909f3 100644 --- a/tests/main/test_mode.py +++ b/tests/main/test_mode.py @@ -3,6 +3,7 @@ from mocket import Mocketizer, mocketize from mocket.exceptions import StrictMocketException +from mocket.mockhttp import Entry, Response @mocketize(strict_mode=True) @@ -37,3 +38,22 @@ def test_strict_mode_exceptions(): with Mocketizer(strict_mode=True, strict_mode_allowed=[("httpbin.local", 80)]): requests.get(url) + + +def test_strict_mode_error_message(): + url = "http://httpbin.local/ip" + + Entry.register(Entry.GET, "http://httpbin.local/user.agent", Response(status=404)) + + with Mocketizer(strict_mode=True): + with pytest.raises(StrictMocketException) as exc_info: + requests.get(url) + assert ( + str(exc_info.value) + == """ +Mocket tried to use the real `socket` module while strict mode is active. +Registered entries: + ('httpbin.local', 80): + Entry(method='GET', schema='http', location=('httpbin.local', 80), path='/user.agent', query='') +""".strip() + ) From 72534fb2a006d86113bd69d78100f1b9d0000255 Mon Sep 17 00:00:00 2001 From: ento Date: Fri, 12 Jan 2024 13:48:19 -0800 Subject: [PATCH 3/7] Add usage example to README --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index f6e027c7..d8732c62 100644 --- a/README.rst +++ b/README.rst @@ -169,6 +169,20 @@ NEW!!! Sometimes you just want your tests to fail when they attempt to use the n with pytest.raises(StrictMocketException): requests.get("https://duckduckgo.com/") +You can specify exceptions as a list of hosts or host-port pairs. + +.. code-block:: python + + with Mocketizer(strict_mode=True, strict_mode_allowed=["localhost", ("intake.ourmetrics.net", 443)]): + ... + + # OR + + @mocketize(strict_mode=True, strict_mode_allowed=["localhost", ("intake.ourmetrics.net", 443)]) + def test_get(): + ... + + How to be sure that all the Entry instances have been served? ============================================================= Add this instruction at the end of the test execution: From 1b730ef01efc9a8ba6d807021fb0628be672a434 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Mon, 5 Feb 2024 11:49:07 +0100 Subject: [PATCH 4/7] Refactoring the new feature from @ento before merging it. --- mocket/async_mocket.py | 2 +- mocket/mocket.py | 28 +++++++-------------- mocket/mockhttp.py | 8 +++--- mocket/plugins/httpretty/__init__.py | 4 +-- mocket/utils.py | 37 ++++++++++++++++++++-------- tests/main/test_mode.py | 14 ++++++++++- 6 files changed, 56 insertions(+), 37 deletions(-) diff --git a/mocket/async_mocket.py b/mocket/async_mocket.py index 936ec22d..2970e0f4 100644 --- a/mocket/async_mocket.py +++ b/mocket/async_mocket.py @@ -8,7 +8,7 @@ async def wrapper( strict_mode=False, strict_mode_allowed=None, *args, - **kwargs + **kwargs, ): async with Mocketizer.factory( test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args diff --git a/mocket/mocket.py b/mocket/mocket.py index 966ebc76..c2c065cf 100644 --- a/mocket/mocket.py +++ b/mocket/mocket.py @@ -22,7 +22,6 @@ urllib3_wrap_socket = None from .compat import basestring, byte_type, decode_from_bytes, encode_to_bytes, text_type -from .exceptions import StrictMocketException from .utils import ( SSL_PROTOCOL, MocketMode, @@ -333,22 +332,8 @@ def recv(self, buffersize, flags=None): raise exc def true_sendall(self, data, *args, **kwargs): - if MocketMode().STRICT: - if not MocketMode().allowed((self._host, self._port)): - current_entries = [ - (location, "\n ".join(map(str, entries))) - for location, entries in Mocket._entries.items() - ] - formatted_entries = "\n".join( - [ - f" {location}:\n {entries}" - for location, entries in current_entries - ] - ) - raise StrictMocketException( - "Mocket tried to use the real `socket` module while strict mode is active.\n" - f"Registered entries:\n{formatted_entries}" - ) + if not MocketMode().is_allowed((self._host, self._port)): + MocketMode.raise_not_allowed() req = decode_from_bytes(data) # make request unique again @@ -693,7 +678,12 @@ def __init__( self.truesocket_recording_dir = truesocket_recording_dir self.namespace = namespace or text_type(id(self)) MocketMode().STRICT = strict_mode - MocketMode().STRICT_ALLOWED = strict_mode_allowed + if strict_mode: + MocketMode().STRICT_ALLOWED = strict_mode_allowed or [] + elif strict_mode_allowed: + raise ValueError( + "Allowed locations are only accepted when STRICT mode is active." + ) def enter(self): Mocket.enable( @@ -755,7 +745,7 @@ def wrapper( strict_mode=False, strict_mode_allowed=None, *args, - **kwargs + **kwargs, ): with Mocketizer.factory( test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args diff --git a/mocket/mockhttp.py b/mocket/mockhttp.py index 60b134a1..4ab3345d 100644 --- a/mocket/mockhttp.py +++ b/mocket/mockhttp.py @@ -65,9 +65,11 @@ def headers(self): @property def querystring(self): parts = self._protocol.url.split("?", 1) - if len(parts) == 2: - return parse_qs(unquote(parts[1]), keep_blank_values=True) - return {} + return ( + parse_qs(unquote(parts[1]), keep_blank_values=True) + if len(parts) == 2 + else {} + ) @property def body(self): diff --git a/mocket/plugins/httpretty/__init__.py b/mocket/plugins/httpretty/__init__.py index 5aaebeb1..bf1e7e21 100644 --- a/mocket/plugins/httpretty/__init__.py +++ b/mocket/plugins/httpretty/__init__.py @@ -70,9 +70,8 @@ def register_uri( responses=None, match_querystring=False, priority=0, - **headers + **headers, ): - headers = httprettifier_headers(headers) if adding_headers is not None: @@ -101,7 +100,6 @@ def force_headers(self): class MocketHTTPretty: - Response = Response def __getattr__(self, name): diff --git a/mocket/utils.py b/mocket/utils.py index 88d7868c..2f17838b 100644 --- a/mocket/utils.py +++ b/mocket/utils.py @@ -2,8 +2,10 @@ import io import os import ssl +from typing import Tuple, Union from .compat import decode_from_bytes, encode_to_bytes +from .exceptions import StrictMocketException SSL_PROTOCOL = ssl.PROTOCOL_TLSv1_2 @@ -52,13 +54,28 @@ class MocketMode: def __init__(self): self.__dict__ = self.__shared_state - def allowed(self, location): - if not self.STRICT_ALLOWED: - return False - host, port = location - for allowed in self.STRICT_ALLOWED: - if isinstance(allowed, str) and host == allowed: - return True - elif location == allowed: - return True - return False + def is_allowed(self, location: Union[str, Tuple[str, int]]) -> bool: + """ + Checks if (`host`, `port`) or at least `host` + are allowed locationsto perform real `socket` calls + """ + if not self.STRICT: + return True + host, _ = location + return location in self.STRICT_ALLOWED or host in self.STRICT_ALLOWED + + @staticmethod + def raise_not_allowed(): + from .mocket import Mocket + + current_entries = [ + (location, "\n ".join(map(str, entries))) + for location, entries in Mocket._entries.items() + ] + formatted_entries = "\n".join( + [f" {location}:\n {entries}" for location, entries in current_entries] + ) + raise StrictMocketException( + "Mocket tried to use the real `socket` module while STRICT mode was active.\n" + f"Registered entries:\n{formatted_entries}" + ) diff --git a/tests/main/test_mode.py b/tests/main/test_mode.py index e3a909f3..0d2d2e7c 100644 --- a/tests/main/test_mode.py +++ b/tests/main/test_mode.py @@ -4,6 +4,7 @@ from mocket import Mocketizer, mocketize from mocket.exceptions import StrictMocketException from mocket.mockhttp import Entry, Response +from mocket.utils import MocketMode @mocketize(strict_mode=True) @@ -51,9 +52,20 @@ def test_strict_mode_error_message(): assert ( str(exc_info.value) == """ -Mocket tried to use the real `socket` module while strict mode is active. +Mocket tried to use the real `socket` module while STRICT mode was active. Registered entries: ('httpbin.local', 80): Entry(method='GET', schema='http', location=('httpbin.local', 80), path='/user.agent', query='') """.strip() ) + + +def test_strict_mode_false_with_allowed_hosts(): + with pytest.raises(ValueError): + Mocketizer(strict_mode=False, strict_mode_allowed=["foobar.local"]) + + +def test_strict_mode_false_always_allowed(): + with Mocketizer(strict_mode=False): + assert MocketMode().is_allowed("foobar.com") + assert MocketMode().is_allowed(("foobar.com", 443)) From 346e3df5e1776175abc190babba5ed9228307a74 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Mon, 5 Feb 2024 11:53:23 +0100 Subject: [PATCH 5/7] Make SonarCloud happy again. --- tests/main/test_mode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/main/test_mode.py b/tests/main/test_mode.py index 0d2d2e7c..445da7b3 100644 --- a/tests/main/test_mode.py +++ b/tests/main/test_mode.py @@ -9,7 +9,7 @@ @mocketize(strict_mode=True) def test_strict_mode_fails(): - url = "http://httpbin.local/ip" + url = "https://httpbin.local/ip" with pytest.raises(StrictMocketException): requests.get(url) @@ -17,7 +17,7 @@ def test_strict_mode_fails(): @pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)') def test_intermittent_strict_mode(): - url = "http://httpbin.local/ip" + url = "https://httpbin.local/ip" with Mocketizer(strict_mode=False): requests.get(url) @@ -32,7 +32,7 @@ def test_intermittent_strict_mode(): @pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)') def test_strict_mode_exceptions(): - url = "http://httpbin.local/ip" + url = "https://httpbin.local/ip" with Mocketizer(strict_mode=True, strict_mode_allowed=["httpbin.local"]): requests.get(url) @@ -42,9 +42,9 @@ def test_strict_mode_exceptions(): def test_strict_mode_error_message(): - url = "http://httpbin.local/ip" + url = "https://httpbin.local/ip" - Entry.register(Entry.GET, "http://httpbin.local/user.agent", Response(status=404)) + Entry.register(Entry.GET, "https://httpbin.local/user.agent", Response(status=404)) with Mocketizer(strict_mode=True): with pytest.raises(StrictMocketException) as exc_info: From 51818b58e2c73b0256871e5f68e845c609f72200 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Mon, 5 Feb 2024 12:04:41 +0100 Subject: [PATCH 6/7] Switch to Compose v2. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d519ef75..5bb32812 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: make services-up - name: Setup hostname run: | - export CONTAINER_ID=$(docker-compose ps -q proxy) + export CONTAINER_ID=$(docker compose ps -q proxy) export CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $CONTAINER_ID) echo "$CONTAINER_IP httpbin.local" | sudo tee -a /etc/hosts - name: Test From 324845fe659ebda203ac2660b518c498684cd551 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Mon, 5 Feb 2024 12:08:44 +0100 Subject: [PATCH 7/7] Failed to make SonarCloud happy again. --- tests/main/test_mode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/main/test_mode.py b/tests/main/test_mode.py index 445da7b3..0d2d2e7c 100644 --- a/tests/main/test_mode.py +++ b/tests/main/test_mode.py @@ -9,7 +9,7 @@ @mocketize(strict_mode=True) def test_strict_mode_fails(): - url = "https://httpbin.local/ip" + url = "http://httpbin.local/ip" with pytest.raises(StrictMocketException): requests.get(url) @@ -17,7 +17,7 @@ def test_strict_mode_fails(): @pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)') def test_intermittent_strict_mode(): - url = "https://httpbin.local/ip" + url = "http://httpbin.local/ip" with Mocketizer(strict_mode=False): requests.get(url) @@ -32,7 +32,7 @@ def test_intermittent_strict_mode(): @pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)') def test_strict_mode_exceptions(): - url = "https://httpbin.local/ip" + url = "http://httpbin.local/ip" with Mocketizer(strict_mode=True, strict_mode_allowed=["httpbin.local"]): requests.get(url) @@ -42,9 +42,9 @@ def test_strict_mode_exceptions(): def test_strict_mode_error_message(): - url = "https://httpbin.local/ip" + url = "http://httpbin.local/ip" - Entry.register(Entry.GET, "https://httpbin.local/user.agent", Response(status=404)) + Entry.register(Entry.GET, "http://httpbin.local/user.agent", Response(status=404)) with Mocketizer(strict_mode=True): with pytest.raises(StrictMocketException) as exc_info: