Skip to content

Commit 94dede1

Browse files
Fix aiohttp session match (#157)
* Add tests proving bug * Match on aiohttp client base url and headers * Fix small aiohttp edge case bugs and typing * Ensure examples tests always run with the current interpreter * Bump to 2.1.2
1 parent b1d4a10 commit 94dede1

File tree

5 files changed

+67
-28
lines changed

5 files changed

+67
-28
lines changed

History.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
History
22
=======
33

4-
vX.Y.Z / 20xx-xx-xx
4+
v2.1.2 / 2024-11-21
55
-------------------------
66

77
* Return the correct type of ``headers`` object for standard library urllib by @sarayourfriend in https://github.com/h2non/pook/pull/154.
88
* Support ``Sequence[tuple[str, str]]`` header input with aiohttp by @sarayourfriend in https://github.com/h2non/pook/pull/154.
9+
* Fix network filters when multiple filters are active by @rsmeral in https://github.com/h2non/pook/pull/155.
10+
* Fix aiohttp matching not working with session base URL or headers by @sarayourfriend in https://github.com/h2non/pook/pull/157.
11+
* Add support for Python 3.13 by @sarayourfriend in https://github.com/h2non/pook/pull/149.
912

1013
v2.1.1 / 2024-10-15
1114
-------------------------

src/pook/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@
4444
__license__ = "MIT"
4545

4646
# Current version
47-
__version__ = "2.1.1"
47+
__version__ = "2.1.2"

src/pook/interceptors/aiohttp.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
22
from http.client import responses as http_reasons
3+
from typing import Callable, Optional
34
from unittest import mock
45
from urllib.parse import urlencode, urlunparse
56
from collections.abc import Mapping
67

8+
import aiohttp
79
from aiohttp.helpers import TimerNoop
810
from aiohttp.streams import EmptyStreamReader
911

@@ -29,13 +31,8 @@ async def read(self, n=-1):
2931
return self.content
3032

3133

32-
def HTTPResponse(*args, **kw):
33-
# Dynamically load package
34-
module = __import__(RESPONSE_PATH, fromlist=(RESPONSE_CLASS,))
35-
ClientResponse = getattr(module, RESPONSE_CLASS)
36-
37-
# Return response instance
38-
return ClientResponse(
34+
def HTTPResponse(session: aiohttp.ClientSession, *args, **kw):
35+
return session._response_class(
3936
*args,
4037
request_info=mock.Mock(),
4138
writer=None,
@@ -53,22 +50,17 @@ class AIOHTTPInterceptor(BaseInterceptor):
5350
aiohttp HTTP client traffic interceptor.
5451
"""
5552

56-
def _url(self, url):
53+
def _url(self, url) -> Optional[yarl.URL]:
5754
return yarl.URL(url) if yarl else None
5855

59-
async def _on_request(
60-
self, _request, session, method, url, data=None, headers=None, **kw
61-
):
62-
# Create request contract based on incoming params
63-
req = Request(method)
64-
56+
def set_headers(self, req, headers) -> None:
6557
# aiohttp's interface allows various mappings, as well as an iterable of key/value tuples
6658
# ``pook.request`` only allows a dict, so we need to map the iterable to the matchable interface
6759
if headers:
6860
if isinstance(headers, Mapping):
6961
req.headers = headers
7062
else:
71-
req_headers = {}
63+
req_headers: dict[str, str] = {}
7264
# If it isn't a mapping, then its an Iterable[Tuple[Union[str, istr], str]]
7365
for req_header, req_header_value in headers:
7466
normalised_header = req_header.lower()
@@ -79,17 +71,37 @@ async def _on_request(
7971

8072
req.headers = req_headers
8173

74+
async def _on_request(
75+
self,
76+
_request: Callable,
77+
session: aiohttp.ClientSession,
78+
method: str,
79+
url: str,
80+
data=None,
81+
headers=None,
82+
**kw,
83+
) -> aiohttp.ClientResponse:
84+
# Create request contract based on incoming params
85+
req = Request(method)
86+
87+
self.set_headers(req, headers)
88+
self.set_headers(req, session.headers)
89+
8290
req.body = data
8391

8492
# Expose extra variadic arguments
8593
req.extra = kw
8694

95+
full_url = session._build_url(url)
96+
8797
# Compose URL
8898
if not kw.get("params"):
89-
req.url = str(url)
99+
req.url = str(full_url)
90100
else:
91101
req.url = (
92-
str(url) + "?" + urlencode([(x, y) for x, y in kw["params"].items()])
102+
str(full_url)
103+
+ "?"
104+
+ urlencode([(x, y) for x, y in kw["params"].items()])
93105
)
94106

95107
# If a json payload is provided, serialize it for JSONMatcher support
@@ -122,13 +134,12 @@ async def _on_request(
122134
headers.append((key, res._headers[key]))
123135

124136
# Create mock equivalent HTTP response
125-
_res = HTTPResponse(req.method, self._url(urlunparse(req.url)))
137+
_res = HTTPResponse(session, req.method, self._url(urlunparse(req.url)))
126138

127139
# response status
128-
_res.version = (1, 1)
140+
_res.version = aiohttp.HttpVersion(1, 1)
129141
_res.status = res._status
130142
_res.reason = http_reasons.get(res._status)
131-
_res._should_close = False
132143

133144
# Add response headers
134145
_res._raw_headers = tuple(headers)
@@ -144,7 +155,7 @@ async def _on_request(
144155
# Return response based on mock definition
145156
return _res
146157

147-
def _patch(self, path):
158+
def _patch(self, path: str) -> None:
148159
# If not able to import aiohttp dependencies, skip
149160
if not yarl or not multidict:
150161
return None
@@ -170,16 +181,18 @@ async def handler(session, method, url, data=None, headers=None, **kw):
170181
else:
171182
self.patchers.append(patcher)
172183

173-
def activate(self):
184+
def activate(self) -> None:
174185
"""
175186
Activates the traffic interceptor.
176187
This method must be implemented by any interceptor.
177188
"""
178-
[self._patch(path) for path in PATCHES]
189+
for path in PATCHES:
190+
self._patch(path)
179191

180-
def disable(self):
192+
def disable(self) -> None:
181193
"""
182194
Disables the traffic interceptor.
183195
This method must be implemented by any interceptor.
184196
"""
185-
[patch.stop() for patch in self.patchers]
197+
for patch in self.patchers:
198+
patch.stop()

tests/integration/examples_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import platform
22
import subprocess
33
from pathlib import Path
4+
import sys
45

56
import pytest
67

@@ -16,6 +17,6 @@
1617

1718
@pytest.mark.parametrize("example", examples)
1819
def test_examples(example):
19-
result = subprocess.run(["python", f"examples/{example}"], check=False)
20+
result = subprocess.run([sys.executable, f"examples/{example}"], check=False)
2021

2122
assert result.returncode == 0, result.stdout

tests/unit/interceptors/aiohttp_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,25 @@ async def test_json_matcher_json_payload(URL):
6464
async with aiohttp.ClientSession() as session:
6565
req = await session.post(URL, json=payload)
6666
assert await req.read() == BINARY_FILE
67+
68+
69+
@pytest.mark.asyncio
70+
async def test_client_base_url(httpbin):
71+
"""Client base url should be matched."""
72+
pook.get(httpbin + "/status/404").reply(200).body("hello from pook")
73+
async with aiohttp.ClientSession(base_url=httpbin.url) as session:
74+
res = await session.get("/status/404")
75+
assert res.status == 200
76+
assert await res.read() == b"hello from pook"
77+
78+
79+
@pytest.mark.asyncio
80+
async def test_client_headers(httpbin):
81+
"""Headers set on the client should be matched."""
82+
pook.get(httpbin + "/status/404").header("x-pook", "hello").reply(200).body(
83+
"hello from pook"
84+
)
85+
async with aiohttp.ClientSession(headers={"x-pook": "hello"}) as session:
86+
res = await session.get(httpbin + "/status/404")
87+
assert res.status == 200
88+
assert await res.read() == b"hello from pook"

0 commit comments

Comments
 (0)