Skip to content

Commit 9d5c031

Browse files
authored
Make Mocket work with big requests (#234)
* Make Mocket work with big requests. * Getting rid of old tests using `aiohttp`. * Adding a single `aiohttp` test with timeout. * Skip for Python versions older than 3.11 (looks like aio-libs/aiohttp#5582). --------- Co-authored-by: Giorgio Salluzzo <giorgio.salluzzo@satellogic.com>
1 parent d154be2 commit 9d5c031

7 files changed

Lines changed: 180 additions & 262 deletions

File tree

mocket/mocket.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import socket
1111
import ssl
1212
from datetime import datetime, timedelta
13+
from io import BytesIO
1314
from json.decoder import JSONDecodeError
1415

1516
import urllib3
@@ -26,7 +27,6 @@
2627
from .utils import (
2728
SSL_PROTOCOL,
2829
MocketMode,
29-
MocketSocketCore,
3030
get_mocketize,
3131
hexdump,
3232
hexload,
@@ -175,6 +175,8 @@ class MocketSocket:
175175
_mode = None
176176
_bufsize = None
177177
_secure_socket = False
178+
_did_handshake = False
179+
_sent_non_empty_bytes = False
178180

179181
def __init__(
180182
self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, **kwargs
@@ -186,8 +188,6 @@ def __init__(
186188
self.type = int(type)
187189
self.proto = int(proto)
188190
self._truesocket_recording_dir = None
189-
self._did_handshake = False
190-
self._sent_non_empty_bytes = False
191191
self.kwargs = kwargs
192192

193193
def __str__(self):
@@ -202,7 +202,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
202202
@property
203203
def fd(self):
204204
if self._fd is None:
205-
self._fd = MocketSocketCore()
205+
self._fd = BytesIO()
206206
return self._fd
207207

208208
def gettimeout(self):
@@ -264,12 +264,10 @@ def unwrap(self):
264264
def write(self, data):
265265
return self.send(encode_to_bytes(data))
266266

267-
@staticmethod
268-
def fileno():
269-
if Mocket.r_fd is not None:
270-
return Mocket.r_fd
271-
Mocket.r_fd, Mocket.w_fd = os.pipe()
272-
return Mocket.r_fd
267+
def fileno(self):
268+
if self.true_socket:
269+
return self.true_socket.fileno()
270+
return self.fd.fileno()
273271

274272
def connect(self, address):
275273
self._address = self._host, self._port = address
@@ -317,8 +315,6 @@ def recv_into(self, buffer, buffersize=None, flags=None):
317315
return len(data)
318316

319317
def recv(self, buffersize, flags=None):
320-
if Mocket.r_fd and Mocket.w_fd:
321-
return os.read(Mocket.r_fd, buffersize)
322318
data = self.read(buffersize)
323319
if data:
324320
return data
@@ -436,7 +432,7 @@ def close(self):
436432
self._fd = None
437433

438434
def __getattr__(self, name):
439-
"""Do nothing catchall function, for methods like close() and shutdown()"""
435+
"""Do nothing catchall function, for methods like shutdown()"""
440436

441437
def do_nothing(*args, **kwargs):
442438
pass
@@ -450,8 +446,6 @@ class Mocket:
450446
_requests = []
451447
_namespace = text_type(id(_entries))
452448
_truesocket_recording_dir = None
453-
r_fd = None
454-
w_fd = None
455449

456450
@classmethod
457451
def register(cls, *entries):
@@ -473,12 +467,6 @@ def collect(cls, data):
473467

474468
@classmethod
475469
def reset(cls):
476-
if cls.r_fd is not None:
477-
os.close(cls.r_fd)
478-
cls.r_fd = None
479-
if cls.w_fd is not None:
480-
os.close(cls.w_fd)
481-
cls.w_fd = None
482470
cls._entries = collections.defaultdict(list)
483471
cls._requests = []
484472

mocket/utils.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import annotations
22

33
import binascii
4-
import io
5-
import os
64
import ssl
75
from typing import TYPE_CHECKING, Any, Callable, ClassVar
86

@@ -12,24 +10,10 @@
1210
if TYPE_CHECKING: # pragma: no cover
1311
from typing import NoReturn
1412

15-
from _typeshed import ReadableBuffer
1613

1714
SSL_PROTOCOL = ssl.PROTOCOL_TLSv1_2
1815

1916

20-
class MocketSocketCore(io.BytesIO):
21-
def write( # type: ignore[override] # BytesIO returns int
22-
self,
23-
content: ReadableBuffer,
24-
) -> None:
25-
super().write(content)
26-
27-
from mocket import Mocket
28-
29-
if Mocket.r_fd and Mocket.w_fd:
30-
os.write(Mocket.w_fd, content)
31-
32-
3317
def hexdump(binary_string: bytes) -> str:
3418
r"""
3519
>>> hexdump(b"bar foobar foo") == decode_from_bytes(encode_to_bytes("62 61 72 20 66 6F 6F 62 61 72 20 66 6F 6F"))

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ test = [
4848
"pook",
4949
"flake8>5",
5050
"xxhash",
51-
"aiohttp;python_version<'3.12'",
5251
"httpx",
5352
"pipfile",
5453
"build",
5554
"twine",
5655
"fastapi",
56+
"aiohttp",
5757
"wait-for-it",
5858
"mypy",
5959
"types-decorator",
Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
import glob
33
import json
44
import socket
5+
import sys
56
import tempfile
67

7-
from mocket import Mocketizer
8+
import aiohttp
9+
import pytest
10+
11+
from mocket import Mocketizer, async_mocketize
12+
from mocket.mockhttp import Entry
813

914

1015
def test_asyncio_record_replay(event_loop):
@@ -37,3 +42,27 @@ async def test_asyncio_connection():
3742
responses = json.load(f)
3843

3944
assert len(responses["google.com"]["80"].keys()) == 1
45+
46+
47+
@pytest.mark.asyncio
48+
@pytest.mark.skipif(
49+
sys.version_info < (3, 11),
50+
reason="Looks like https://github.com/aio-libs/aiohttp/issues/5582",
51+
)
52+
@async_mocketize
53+
async def test_aiohttp():
54+
url = "https://bar.foo/"
55+
data = {"message": "Hello"}
56+
57+
Entry.single_register(
58+
Entry.GET,
59+
url,
60+
body=json.dumps(data),
61+
headers={"content-type": "application/json"},
62+
)
63+
64+
async with aiohttp.ClientSession(
65+
timeout=aiohttp.ClientTimeout(total=3)
66+
) as session, session.get(url) as response:
67+
response = await response.json()
68+
assert response == data

tests/main/test_httpx.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import datetime
12
import json
23

34
import httpx
45
import pytest
56
from asgiref.sync import async_to_sync
7+
from fastapi import FastAPI
8+
from fastapi.testclient import TestClient
69

7-
from mocket.mocket import Mocket, mocketize
10+
from mocket import Mocket, Mocketizer, async_mocketize, mocketize
811
from mocket.mockhttp import Entry
912
from mocket.plugins.httpretty import httprettified, httpretty
1013

@@ -55,3 +58,139 @@ async def perform_async_transactions():
5558

5659
perform_async_transactions()
5760
assert len(httpretty.latest_requests) == 1
61+
62+
63+
@mocketize(strict_mode=True)
64+
def test_sync_case():
65+
test_uri = "https://abc.de/testdata/"
66+
base_timestamp = int(datetime.datetime.now().timestamp())
67+
response = [
68+
{"timestamp": base_timestamp + i, "value": 1337 + 42 * i} for i in range(30_000)
69+
]
70+
Entry.single_register(
71+
method=Entry.POST,
72+
uri=test_uri,
73+
body=json.dumps(
74+
response,
75+
),
76+
headers={"content-type": "application/json"},
77+
)
78+
79+
with httpx.Client() as client:
80+
response = client.post(test_uri)
81+
82+
assert len(response.json())
83+
84+
85+
@pytest.mark.asyncio
86+
@async_mocketize(strict_mode=True)
87+
async def test_async_case_low_number():
88+
test_uri = "https://abc.de/testdata/"
89+
base_timestamp = int(datetime.datetime.now().timestamp())
90+
response = [
91+
{"timestamp": base_timestamp + i, "value": 1337 + 42 * i} for i in range(100)
92+
]
93+
Entry.single_register(
94+
method=Entry.POST,
95+
uri=test_uri,
96+
body=json.dumps(
97+
response,
98+
),
99+
headers={"content-type": "application/json"},
100+
)
101+
102+
async with httpx.AsyncClient() as client:
103+
response = await client.post(test_uri)
104+
105+
assert len(response.json())
106+
107+
108+
@pytest.mark.asyncio
109+
@async_mocketize(strict_mode=True)
110+
async def test_async_case_high_number():
111+
test_uri = "https://abc.de/testdata/"
112+
base_timestamp = int(datetime.datetime.now().timestamp())
113+
response = [
114+
{"timestamp": base_timestamp + i, "value": 1337 + 42 * i} for i in range(30_000)
115+
]
116+
Entry.single_register(
117+
method=Entry.POST,
118+
uri=test_uri,
119+
body=json.dumps(
120+
response,
121+
),
122+
headers={"content-type": "application/json"},
123+
)
124+
125+
async with httpx.AsyncClient() as client:
126+
response = await client.post(test_uri)
127+
128+
assert len(response.json())
129+
130+
131+
def create_app() -> FastAPI:
132+
app = FastAPI()
133+
134+
@app.get("/")
135+
async def read_main() -> dict:
136+
async with httpx.AsyncClient() as client:
137+
r = await client.get("https://example.org/")
138+
return r.json()
139+
140+
return app
141+
142+
143+
@mocketize
144+
def test_call_from_fastapi() -> None:
145+
app = create_app()
146+
client = TestClient(app)
147+
148+
Entry.single_register(Entry.GET, "https://example.org/", body='{"id": 1}')
149+
150+
response = client.get("/")
151+
152+
assert response.status_code == 200
153+
assert response.json() == {"id": 1}
154+
155+
156+
@pytest.mark.asyncio
157+
@async_mocketize
158+
async def test_httpx_decorator():
159+
url = "https://bar.foo/"
160+
data = {"message": "Hello"}
161+
162+
Entry.single_register(
163+
Entry.GET,
164+
url,
165+
body=json.dumps(data),
166+
headers={"content-type": "application/json"},
167+
)
168+
169+
async with httpx.AsyncClient() as client:
170+
response = await client.get(url)
171+
172+
assert response.json() == data
173+
174+
175+
@pytest.fixture
176+
def httpx_client() -> httpx.AsyncClient:
177+
with Mocketizer():
178+
yield httpx.AsyncClient()
179+
180+
181+
@pytest.mark.asyncio
182+
async def test_httpx_fixture(httpx_client):
183+
url = "https://foo.bar/"
184+
data = {"message": "Hello"}
185+
186+
Entry.single_register(
187+
Entry.GET,
188+
url,
189+
body=json.dumps(data),
190+
headers={"content-type": "application/json"},
191+
)
192+
193+
async with httpx_client as client:
194+
response = await client.get(url)
195+
196+
assert response.json() == data

0 commit comments

Comments
 (0)