Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recording: allow for storing recorded responses in human-readable form #270

Closed
wants to merge 12 commits into from
8 changes: 7 additions & 1 deletion mocket/async_mocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ async def wrapper(
truesocket_recording_dir=None,
strict_mode=False,
strict_mode_allowed=None,
use_hex_encoding=True,
*args,
**kwargs,
):
async with Mocketizer.factory(
test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args
test,
truesocket_recording_dir,
strict_mode,
strict_mode_allowed,
use_hex_encoding,
args,
):
return await test(*args, **kwargs)

Expand Down
2 changes: 2 additions & 0 deletions mocket/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
def enable(
namespace: str | None = None,
truesocket_recording_dir: str | None = None,
use_hex_encoding=True,
) -> None:
from mocket.mocket import Mocket
from mocket.socket import (
Expand All @@ -33,6 +34,7 @@ def enable(

Mocket._namespace = namespace
Mocket._truesocket_recording_dir = truesocket_recording_dir
Mocket._use_hex_encoding = use_hex_encoding

if truesocket_recording_dir and not os.path.isdir(truesocket_recording_dir):
# JSON dumps will be saved here
Expand Down
5 changes: 5 additions & 0 deletions mocket/mocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Mocket:
_requests: ClassVar[list] = []
_namespace: ClassVar[str] = str(id(_entries))
_truesocket_recording_dir: ClassVar[str | None] = None
_use_hex_encoding: ClassVar[bool] = True

enable = mocket.inject.enable
disable = mocket.inject.disable
Expand Down Expand Up @@ -96,6 +97,10 @@ def get_namespace(cls) -> str:
def get_truesocket_recording_dir(cls) -> str | None:
return cls._truesocket_recording_dir

@classmethod
def get_use_hex_encoding(cls) -> bool:
return cls._use_hex_encoding

@classmethod
def assert_fail_if_entries_not_served(cls) -> None:
"""Mocket checks that all entries have been served at least once."""
Expand Down
21 changes: 19 additions & 2 deletions mocket/mocketizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ def __init__(
instance=None,
namespace=None,
truesocket_recording_dir=None,
use_hex_encoding=True,
strict_mode=False,
strict_mode_allowed=None,
):
self.instance = instance
self.truesocket_recording_dir = truesocket_recording_dir
self.use_hex_encoding = use_hex_encoding
self.namespace = namespace or str(id(self))
MocketMode().STRICT = strict_mode
if strict_mode:
Expand All @@ -27,6 +29,7 @@ def enter(self):
Mocket.enable(
namespace=self.namespace,
truesocket_recording_dir=self.truesocket_recording_dir,
use_hex_encoding=self.use_hex_encoding,
)
if self.instance:
self.check_and_call("mocketize_setup")
Expand Down Expand Up @@ -57,7 +60,14 @@ def check_and_call(self, method_name):
method()

@staticmethod
def factory(test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args):
def factory(
test,
truesocket_recording_dir,
strict_mode,
strict_mode_allowed,
use_hex_encoding,
args,
):
instance = args[0] if args else None
namespace = None
if truesocket_recording_dir:
Expand All @@ -74,6 +84,7 @@ def factory(test, truesocket_recording_dir, strict_mode, strict_mode_allowed, ar
namespace=namespace,
truesocket_recording_dir=truesocket_recording_dir,
strict_mode=strict_mode,
use_hex_encoding=use_hex_encoding,
strict_mode_allowed=strict_mode_allowed,
)

Expand All @@ -83,11 +94,17 @@ def wrapper(
truesocket_recording_dir=None,
strict_mode=False,
strict_mode_allowed=None,
use_hex_encoding=True,
*args,
**kwargs,
):
with Mocketizer.factory(
test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args
test,
truesocket_recording_dir,
strict_mode,
strict_mode_allowed,
use_hex_encoding,
args,
):
return test(*args, **kwargs)

Expand Down
31 changes: 28 additions & 3 deletions mocket/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import contextlib
import errno
import gzip
import hashlib
import json
import os
Expand All @@ -14,7 +15,7 @@
import urllib3.connection
from typing_extensions import Self

from mocket.compat import decode_from_bytes, encode_to_bytes
from mocket.compat import decode_from_bytes, encode_to_bytes, ENCODING
from mocket.entry import MocketEntry
from mocket.io import MocketSocketIO
from mocket.mocket import Mocket
Expand Down Expand Up @@ -291,7 +292,20 @@ def true_sendall(self, data: ReadableBuffer, *args: Any, **kwargs: Any) -> int:

# try to get the response from the dictionary
try:
encoded_response = hexload(response_dict["response"])
response = response_dict["response"]

if Mocket.get_use_hex_encoding():
encoded_response = hexload(response)
else:
headers, body = response.split("\r\n\r\n", 1)

headers_bytes = headers.encode(ENCODING)
body_bytes = body.encode(ENCODING)

if "content-encoding: gzip" in headers.lower():
body_bytes = gzip.compress(body_bytes)

encoded_response = headers_bytes + b"\r\n\r\n" + body_bytes
# if not available, call the real sendall
except KeyError:
host, port = self._host, self._port
Expand All @@ -316,7 +330,18 @@ def true_sendall(self, data: ReadableBuffer, *args: Any, **kwargs: Any) -> int:
if Mocket.get_truesocket_recording_dir():
# update the dictionary with request and response lines
response_dict["request"] = req
response_dict["response"] = hexdump(encoded_response)

if Mocket.get_use_hex_encoding():
response_dict["response"] = hexdump(encoded_response)
else:
headers, body = encoded_response.split(b"\r\n\r\n", 1)

if b"content-encoding: gzip" in headers.lower():
body = gzip.decompress(body)

response_dict["response"] = (headers + b"\r\n\r\n" + body).decode(
ENCODING
)

with open(path, mode="w") as f:
f.write(
Expand Down
52 changes: 52 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,58 @@ def test_truesendall_with_chunk_recording(self):

assert len(responses["httpbin.local"]["80"].keys()) == 1

def test_truesendall_with_recording_without_hex_encoding(self):
with tempfile.TemporaryDirectory() as temp_dir, Mocketizer(
truesocket_recording_dir=temp_dir, use_hex_encoding=False
):
url = "http://httpbin.local/ip"

requests.get(url)
resp = requests.get(url)
self.assertEqual(resp.status_code, 200)

dump_filename = os.path.join(
Mocket.get_truesocket_recording_dir(),
Mocket.get_namespace() + ".json",
)
with open(dump_filename) as f:
responses = json.load(f)

for _, value in responses["httpbin.local"]["80"].items():
self.assertIn("HTTP/1.1 200", value["response"])

def test_truesendall_with_gzip_recording_without_hex_encoding(self):
with tempfile.TemporaryDirectory() as temp_dir, Mocketizer(
truesocket_recording_dir=temp_dir, use_hex_encoding=False
):
url = "http://httpbin.local/gzip"
headers = {
"Accept-Encoding": "gzip, deflate, zstd",
}

requests.get(
url,
headers=headers,
)

dump_filename = os.path.join(
Mocket.get_truesocket_recording_dir(),
Mocket.get_namespace() + ".json",
)

with open(dump_filename) as f:
responses = json.load(f)

for _, value in responses["httpbin.local"]["80"].items():
self.assertIn("HTTP/1.1 200", value["response"])
self.assertIn("gzip, deflate, zstd", value["response"])

resp = requests.get(
url,
headers=headers,
)
self.assertEqual(resp.status_code, 200)

@mocketize
def test_wrongpath_truesendall(self):
Entry.register(
Expand Down
Loading