Skip to content

Commit 3419f1c

Browse files
authored
Fix for nested Mocketizer's decorators (#327)
* Fix for nested Mocketizer's decorators. * Coverage back to 100%.
1 parent d1bd1d6 commit 3419f1c

8 files changed

Lines changed: 186 additions & 16 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ repos:
1515
exclude: helm/
1616
args: [ --unsafe ]
1717
- repo: https://github.com/charliermarsh/ruff-pre-commit
18-
rev: "v0.15.18"
18+
rev: "v0.16.4"
1919
hooks:
2020
- id: ruff
2121
args: [--fix, --exit-non-zero-on-fix]
2222
- id: ruff-format
2323
- repo: https://github.com/rstcheck/rstcheck
24-
rev: v6.2.5
24+
rev: v6.3.0
2525
hooks:
2626
- id: rstcheck

‎mocket/decorators/mocketizer.py‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ def __init__(
3232
self.instance = instance
3333
self.truesocket_recording_dir = truesocket_recording_dir
3434
self.namespace = namespace or str(id(self))
35-
MocketMode.STRICT = strict_mode
36-
if strict_mode:
37-
MocketMode.STRICT_ALLOWED = strict_mode_allowed or []
38-
elif strict_mode_allowed:
35+
if not strict_mode and strict_mode_allowed:
3936
raise ValueError(
4037
"Allowed locations are only accepted when STRICT mode is active."
4138
)
39+
self._previous_strict_mode = MocketMode.STRICT
40+
self._previous_strict_mode_allowed = MocketMode.STRICT_ALLOWED
41+
MocketMode.STRICT = strict_mode
42+
if strict_mode:
43+
MocketMode.STRICT_ALLOWED = strict_mode_allowed or []
4244

4345
def enter(self) -> None:
4446
"""Enter the Mocketizer context (enable Mocket)."""
@@ -60,10 +62,15 @@ def __enter__(self) -> Mocketizer:
6062

6163
def exit(self) -> None:
6264
"""Exit the Mocketizer context (disable Mocket)."""
63-
if self.instance:
64-
self.check_and_call("mocketize_teardown")
65-
66-
Mocket.disable()
65+
try:
66+
if self.instance:
67+
self.check_and_call("mocketize_teardown")
68+
finally:
69+
try:
70+
Mocket.disable()
71+
finally:
72+
MocketMode.STRICT = self._previous_strict_mode
73+
MocketMode.STRICT_ALLOWED = self._previous_strict_mode_allowed
6774

6875
def __exit__(self, type: Any, value: Any, tb: Any) -> None:
6976
"""Exit context manager.

‎mocket/inject.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import contextlib
66
import socket
77
import ssl
8+
import threading
89
from types import ModuleType
910
from typing import Any
1011

1112
import urllib3
1213

1314
_patches_restore: dict[tuple[ModuleType, str], Any] = {}
15+
_enable_depth = 0
16+
_enable_lock = threading.Lock()
1417

1518

1619
def _patch(module: ModuleType, name: str, patched_value: Any) -> None:
@@ -39,6 +42,8 @@ def _restore(module: ModuleType, name: str) -> None:
3942

4043
def enable() -> None:
4144
"""Enable Mocket by patching socket, ssl, and urllib3 modules."""
45+
global _enable_depth
46+
4247
from mocket.socket import (
4348
MocketSocket,
4449
mock_create_connection,
@@ -77,8 +82,15 @@ def enable() -> None:
7782
(urllib3.util.ssl_, "wrap_socket"): mock_urllib3_ssl_wrap_socket, # urllib3 < 2
7883
}
7984

80-
for (module, name), new_value in patches.items():
81-
_patch(module, name, new_value)
85+
with _enable_lock:
86+
if _enable_depth > 0:
87+
_enable_depth += 1
88+
return
89+
90+
for (module, name), new_value in patches.items():
91+
_patch(module, name, new_value)
92+
93+
_enable_depth += 1
8294

8395
with contextlib.suppress(ImportError):
8496
from urllib3.contrib.pyopenssl import extract_from_urllib3
@@ -88,8 +100,17 @@ def enable() -> None:
88100

89101
def disable() -> None:
90102
"""Disable Mocket by restoring all patched modules."""
91-
for module, name in list(_patches_restore.keys()):
92-
_restore(module, name)
103+
global _enable_depth
104+
with _enable_lock:
105+
if _enable_depth == 0:
106+
return
107+
108+
_enable_depth -= 1
109+
if _enable_depth > 0:
110+
return
111+
112+
for module, name in list(_patches_restore.keys()):
113+
_restore(module, name)
93114

94115
with contextlib.suppress(ImportError):
95116
from urllib3.contrib.pyopenssl import inject_into_urllib3

‎mocket/mocket.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# NOTE this is here for backwards-compat to keep old import-paths working
1515
# from mocket.socket import MocketSocket as MocketSocket
1616

17-
if TYPE_CHECKING:
17+
if TYPE_CHECKING: # pragma: no cover
1818
from mocket.entry import MocketEntry
1919
from mocket.types import Address
2020

‎tests/test_inject.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import sys
2+
import types
3+
4+
from mocket import inject
5+
6+
7+
def test_disable_calls_pyopenssl_inject_when_available(monkeypatch):
8+
calls: list[str] = []
9+
pyopenssl_module = types.ModuleType("urllib3.contrib.pyopenssl")
10+
11+
def fake_inject_into_urllib3():
12+
calls.append("called")
13+
14+
pyopenssl_module.inject_into_urllib3 = fake_inject_into_urllib3
15+
monkeypatch.setitem(sys.modules, "urllib3.contrib.pyopenssl", pyopenssl_module)
16+
monkeypatch.setattr(inject, "_patches_restore", {})
17+
monkeypatch.setattr(inject, "_enable_depth", 1)
18+
19+
inject.disable()
20+
21+
assert calls == ["called"]
22+
23+
24+
def test_enable_calls_pyopenssl_extract_when_available(monkeypatch):
25+
calls: list[str] = []
26+
pyopenssl_module = types.ModuleType("urllib3.contrib.pyopenssl")
27+
28+
def fake_extract_from_urllib3():
29+
calls.append("called")
30+
31+
def fake_inject_into_urllib3():
32+
pass
33+
34+
pyopenssl_module.extract_from_urllib3 = fake_extract_from_urllib3
35+
pyopenssl_module.inject_into_urllib3 = fake_inject_into_urllib3
36+
monkeypatch.setitem(sys.modules, "urllib3.contrib.pyopenssl", pyopenssl_module)
37+
monkeypatch.setattr(inject, "_patches_restore", {})
38+
monkeypatch.setattr(inject, "_enable_depth", 0)
39+
40+
inject.enable()
41+
inject.disable()
42+
43+
assert calls == ["called"]

‎tests/test_mode.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import socket
2+
3+
import httpx
14
import pytest
25
import requests
36

@@ -71,3 +74,28 @@ def test_strict_mode_allowed_or_not(strict_mode_on):
7174
with Mocketizer(strict_mode=strict_mode_on):
7275
assert MocketMode.is_allowed("foobar.com") is not strict_mode_on
7376
assert MocketMode.is_allowed(("foobar.com", 443)) is not strict_mode_on
77+
78+
79+
def test_mocketize_strict_mode_does_not_leak_after_outer_context():
80+
with Mocketizer(strict_mode=False):
81+
82+
@mocketize(strict_mode=True)
83+
def strict_test():
84+
assert MocketMode.STRICT is True
85+
86+
strict_test()
87+
assert MocketMode.STRICT is False
88+
89+
90+
@pytest.mark.skipif('os.getenv("SKIP_TRUE_HTTP", False)')
91+
def test_mocketize_twice_nested():
92+
original_socket = socket.socket
93+
original_strict_mode = MocketMode.STRICT
94+
95+
with Mocketizer(strict_mode=True), Mocketizer(strict_mode=True):
96+
pass
97+
98+
assert socket.socket is original_socket
99+
assert MocketMode.STRICT is original_strict_mode
100+
url = "http://httpbin.local/ip"
101+
assert httpx.get(url, timeout=5.0).status_code == 200

‎tests/test_recording.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from mocket.recording import MocketRecord, MocketRecordStorage, _hash_request_fallback
2+
3+
4+
def test_get_records_returns_all_records_for_address(tmp_path):
5+
storage = MocketRecordStorage(directory=tmp_path, namespace="recording-get-records")
6+
address = ("example.org", 80)
7+
signature = "signature"
8+
storage._records[address][signature] = MocketRecord(
9+
host=address[0],
10+
port=address[1],
11+
request=b"GET / HTTP/1.1\r\nHost: example.org\r\n\r\n",
12+
response=b"HTTP/1.1 200 OK\r\n\r\nok",
13+
)
14+
15+
records = storage.get_records(address)
16+
17+
assert len(records) == 1
18+
assert records[0].response == b"HTTP/1.1 200 OK\r\n\r\nok"
19+
20+
21+
def test_put_record_updates_fallback_signature_without_saving(tmp_path):
22+
storage = MocketRecordStorage(
23+
directory=tmp_path, namespace="recording-put-record-fallback"
24+
)
25+
address = ("example.org", 80)
26+
request = b"GET / HTTP/1.1\r\nHost: example.org\r\n\r\n"
27+
fallback_signature = _hash_request_fallback(request)
28+
29+
storage._records[address][fallback_signature] = MocketRecord(
30+
host=address[0],
31+
port=address[1],
32+
request=request,
33+
response=b"HTTP/1.1 200 OK\r\n\r\nold",
34+
)
35+
36+
storage.put_record(
37+
address=address,
38+
request=request,
39+
response=b"HTTP/1.1 200 OK\r\n\r\nnew",
40+
)
41+
42+
assert storage._records[address][fallback_signature].response.endswith(b"new")
43+
assert not storage.file.exists()

‎tests/test_socket.py‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from mocket import Mocket, MocketEntry, Mocketizer, mocketize
1010
from mocket.mockhttp import Entry
1111
from mocket.socket import MocketSocket
12-
from mocket.ssl.context import MocketSSLContext
12+
from mocket.ssl.context import MocketSSLContext, mock_wrap_socket
1313
from mocket.ssl.socket import MocketSSLSocket
14+
from mocket.urllib3 import mock_match_hostname
1415

1516

1617
@pytest.mark.parametrize("blocking", (False, True))
@@ -163,6 +164,33 @@ def test_wrap_bio_preserves_empty_server_hostname_on_getpeercert(monkeypatch):
163164
assert ssl_obj._address == ("", 443)
164165

165166

167+
def test_wrap_bio_with_invalid_mocket_address(monkeypatch):
168+
monkeypatch.setattr(Mocket, "_address", "invalid-address")
169+
ssl_obj = MocketSSLContext().wrap_bio(
170+
incoming=None,
171+
outgoing=None,
172+
server_hostname=None,
173+
)
174+
175+
assert ssl_obj._host is None
176+
assert ssl_obj._port is None
177+
178+
179+
def test_mock_wrap_socket_delegates_to_context(monkeypatch):
180+
expected = MocketSSLSocket()
181+
182+
def fake_wrap_socket(self, sock, *args, **kwargs):
183+
return expected
184+
185+
monkeypatch.setattr(MocketSSLContext, "wrap_socket", fake_wrap_socket)
186+
187+
assert mock_wrap_socket(MocketSocket()) is expected
188+
189+
190+
def test_mock_match_hostname_returns_none():
191+
assert mock_match_hostname("example.org", object()) is None
192+
193+
166194
def test_getpeercert_does_not_overwrite_empty_host_when_port_missing(monkeypatch):
167195
monkeypatch.setattr(Mocket, "_address", ("httpbin.local", 443))
168196
ssl_obj = MocketSSLSocket()

0 commit comments

Comments
 (0)