From 840f87d862ea8eb2783cb500b986043daf665e19 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 7 Jul 2025 16:13:55 +0100 Subject: [PATCH 1/5] Use a locally hosted httpbin for tests --- .github/workflows/ci.yml | 5 +++++ tests/async_/test_async_transport.py | 8 ++++++-- tests/async_/test_httpbin.py | 25 +++++++++++++++---------- tests/conftest.py | 8 +++++--- tests/test_httpbin.py | 27 ++++++++++++++++----------- tests/test_logging.py | 7 +++---- 6 files changed, 50 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e679e8..79eb9640 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: runs-on: ${{ matrix.os }} name: test-${{ matrix.python-version }} ${{ matrix.nox-session }} continue-on-error: ${{ matrix.experimental }} + services: + httpbin: + image: kennethreitz/httpbin + ports: + - 8080:80 steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/tests/async_/test_async_transport.py b/tests/async_/test_async_transport.py index 2872e2e3..811931dd 100644 --- a/tests/async_/test_async_transport.py +++ b/tests/async_/test_async_transport.py @@ -52,11 +52,15 @@ async def test_async_transport_httpbin(httpbin_node_config): assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "https://httpbin.org/anything?key=value" + assert data["url"] == "http://localhost:8080/anything?key=value" assert data["args"] == {"key": "value"} data["headers"].pop("X-Amzn-Trace-Id", None) - assert data["headers"] == {"User-Agent": DEFAULT_USER_AGENT, "Host": "httpbin.org"} + assert data["headers"] == { + "User-Agent": DEFAULT_USER_AGENT, + "Connection": "keep-alive", + "Host": "localhost:8080", + } @pytest.mark.skipif( diff --git a/tests/async_/test_httpbin.py b/tests/async_/test_httpbin.py index c400efb5..c8c6f97c 100644 --- a/tests/async_/test_httpbin.py +++ b/tests/async_/test_httpbin.py @@ -38,7 +38,7 @@ async def test_simple_request(httpbin_node_config): ) assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "https://httpbin.org/anything?key[]=1&key[]=2&q1&q2=" + assert data["url"] == "http://localhost:8080/anything?key[]=1&key[]=2&q1&q2=" # httpbin makes no-value query params into '' assert data["args"] == { @@ -53,7 +53,8 @@ async def test_simple_request(httpbin_node_config): "Content-Type": "application/json", "Content-Length": "15", "Custom": "headeR", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", } assert all(v == data["headers"][k] for k, v in request_headers.items()) @@ -69,11 +70,12 @@ def new_node(**kwargs): parsed = parse_httpbin(data) assert parsed == { "headers": { - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } node = new_node(http_compress=True) @@ -83,11 +85,12 @@ def new_node(**kwargs): assert parsed == { "headers": { "Accept-Encoding": "gzip", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } resp, data = await node.perform_request("GET", "/anything", body=b"hello, world!") @@ -99,11 +102,12 @@ def new_node(**kwargs): "Content-Encoding": "gzip", "Content-Type": "application/octet-stream", "Content-Length": "33", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } resp, data = await node.perform_request( @@ -120,9 +124,10 @@ def new_node(**kwargs): "Content-Encoding": "gzip", "Content-Length": "36", "Content-Type": "application/json", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "POST", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } diff --git a/tests/conftest.py b/tests/conftest.py index cec6eb4f..02e5b300 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,12 +86,14 @@ def httpbin_cert_fingerprint(request) -> str: @pytest.fixture(scope="session") def httpbin_node_config() -> NodeConfig: try: - sock = socket.create_connection(("httpbin.org", 443)) + sock = socket.create_connection(("localhost", 8080)) except Exception as e: - pytest.skip(f"Couldn't connect to httpbin.org, internet not connected? {e}") + pytest.skip( + f"Couldn't connect to localhost:8080, not running httpbin image? {e}" + ) sock.close() return NodeConfig( - "https", "httpbin.org", 443, verify_certs=False, ssl_show_warn=False + "http", "localhost", 8080, verify_certs=False, ssl_show_warn=False ) diff --git a/tests/test_httpbin.py b/tests/test_httpbin.py index f88d8909..a8c43e3c 100644 --- a/tests/test_httpbin.py +++ b/tests/test_httpbin.py @@ -37,7 +37,7 @@ def test_simple_request(node_class, httpbin_node_config): ) assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "https://httpbin.org/anything?key[]=1&key[]=2&q1&q2=" + assert data["url"] == "http://localhost:8080/anything?key[]=1&key[]=2&q1&q2=" # httpbin makes no-value query params into '' assert data["args"] == { @@ -52,7 +52,8 @@ def test_simple_request(node_class, httpbin_node_config): "Content-Type": "application/json", "Content-Length": "15", "Custom": "headeR", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", } assert all(v == data["headers"][k] for k, v in request_headers.items()) @@ -71,11 +72,12 @@ def new_node(**kwargs): assert parsed == { "headers": { "Accept-Encoding": "identity", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } node = new_node(http_compress=True) @@ -85,11 +87,12 @@ def new_node(**kwargs): assert parsed == { "headers": { "Accept-Encoding": "gzip", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } resp, data = node.perform_request("GET", "/anything", body=b"hello, world!") @@ -100,11 +103,12 @@ def new_node(**kwargs): "Accept-Encoding": "gzip", "Content-Encoding": "gzip", "Content-Length": "33", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } resp, data = node.perform_request( @@ -121,16 +125,17 @@ def new_node(**kwargs): "Content-Encoding": "gzip", "Content-Length": "36", "Content-Type": "application/json", - "Host": "httpbin.org", + "Connection": "keep-alive", + "Host": "localhost:8080", "User-Agent": DEFAULT_USER_AGENT, }, "method": "POST", - "url": "https://httpbin.org/anything", + "url": "http://localhost:8080/anything", } def parse_httpbin(value): - """Parses a response from httpbin.org/anything by stripping all the variable things""" + """Parses a response from httpbin's /anything by stripping all the variable things""" if isinstance(value, bytes): value = json.loads(value) else: diff --git a/tests/test_logging.py b/tests/test_logging.py index dfb2a398..86ee9532 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -70,7 +70,6 @@ async def test_debug_logging(node_class, httpbin_node_config): "< HTTP/1.1 200 OK", "< Access-Control-Allow-Credentials: true", "< Access-Control-Allow-Origin: *", - "< Connection: close", "< Content-Type: application/json", "< {", ' "args": {}, ', @@ -79,14 +78,14 @@ async def test_debug_logging(node_class, httpbin_node_config): ' "form": {}, ', ' "headers": {', ' "Content-Type": "application/json", ', - ' "Host": "httpbin.org", ', - f' "User-Agent": "{DEFAULT_USER_AGENT}", ', + ' "Host": "localhost:8080", ', + f' "User-Agent": "{DEFAULT_USER_AGENT}"', " }, ", ' "json": {', ' "key": "value"', " }, ", ' "method": "GET", ', - ' "url": "https://httpbin.org/anything"', + ' "url": "http://localhost:8080/anything"', "}", ]: assert line in lines From 8254da5f68d6d34a45c85d10c304bf4f3f44ff46 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 8 Jul 2025 14:33:32 +0100 Subject: [PATCH 2/5] use a www.elastic.co instead of httpbin.org for SSL tests --- .github/workflows/ci.yml | 6 ++++ tests/conftest.py | 6 ++-- tests/node/test_http_aiohttp.py | 38 +++++++++++++++----------- tests/node/test_http_httpx.py | 8 +++--- tests/node/test_urllib3_chain_certs.py | 17 ++++++------ 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79eb9640..dc085315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,12 @@ jobs: image: kennethreitz/httpbin ports: - 8080:80 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:9.0.2 + env: + discovery.type: single-node + ports: + - 9200:9200 steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/tests/conftest.py b/tests/conftest.py index 02e5b300..76d10102 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,9 +67,9 @@ async def perform_request(self, *args, **kwargs): @pytest.fixture(scope="session", params=[True, False]) -def httpbin_cert_fingerprint(request) -> str: - """Gets the SHA256 fingerprint of the certificate for 'httpbin.org'""" - sock = socket.create_connection(("httpbin.org", 443)) +def cert_fingerprint(request) -> str: + """Gets the SHA256 fingerprint of the certificate for localhost:9200""" + sock = socket.create_connection(("localhost", 9200)) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE diff --git a/tests/node/test_http_aiohttp.py b/tests/node/test_http_aiohttp.py index a52e379f..84373cc5 100644 --- a/tests/node/test_http_aiohttp.py +++ b/tests/node/test_http_aiohttp.py @@ -290,40 +290,44 @@ async def test_head_workaround(self, aiohttp_fixed_head_bug): @pytest.mark.asyncio -async def test_ssl_assert_fingerprint(httpbin_cert_fingerprint): +async def test_ssl_assert_fingerprint(cert_fingerprint): with warnings.catch_warnings(record=True) as w: node = AiohttpHttpNode( NodeConfig( scheme="https", - host="httpbin.org", - port=443, - ssl_assert_fingerprint=httpbin_cert_fingerprint, + host="localhost", + port=9200, + ssl_assert_fingerprint=cert_fingerprint, ) ) resp, _ = await node.perform_request("GET", "/") - assert resp.status == 200 + assert resp.status == 401 assert [str(x.message) for x in w if x.category != DeprecationWarning] == [] @pytest.mark.asyncio async def test_default_headers(): - node = AiohttpHttpNode(NodeConfig(scheme="https", host="httpbin.org", port=443)) + node = AiohttpHttpNode(NodeConfig(scheme="http", host="localhost", port=8080)) resp, data = await node.perform_request("GET", "/anything") assert resp.status == 200 headers = json.loads(data)["headers"] headers.pop("X-Amzn-Trace-Id", None) - assert headers == {"Host": "httpbin.org", "User-Agent": DEFAULT_USER_AGENT} + assert headers == { + "Connection": "keep-alive", + "Host": "localhost:8080", + "User-Agent": DEFAULT_USER_AGENT, + } @pytest.mark.asyncio async def test_custom_headers(): node = AiohttpHttpNode( NodeConfig( - scheme="https", - host="httpbin.org", - port=443, + scheme="http", + host="localhost", + port=8080, headers={"accept-encoding": "gzip", "Content-Type": "application/json"}, ) ) @@ -341,8 +345,9 @@ async def test_custom_headers(): headers.pop("X-Amzn-Trace-Id", None) assert headers == { "Accept-Encoding": "gzip", + "Connection": "keep-alive", "Content-Type": "application/x-ndjson", - "Host": "httpbin.org", + "Host": "localhost:8080", "User-Agent": "custom-agent/1.2.3", } @@ -351,9 +356,9 @@ async def test_custom_headers(): async def test_custom_user_agent(): node = AiohttpHttpNode( NodeConfig( - scheme="https", - host="httpbin.org", - port=443, + scheme="http", + host="localhost", + port=8080, headers={ "accept-encoding": "gzip", "Content-Type": "application/json", @@ -371,8 +376,9 @@ async def test_custom_user_agent(): headers.pop("X-Amzn-Trace-Id", None) assert headers == { "Accept-Encoding": "gzip", + "Connection": "keep-alive", "Content-Type": "application/json", - "Host": "httpbin.org", + "Host": "localhost:8080", "User-Agent": "custom-agent/1.2.3", } @@ -385,7 +391,7 @@ def test_repr(): @pytest.mark.asyncio async def test_head(): node = AiohttpHttpNode( - NodeConfig(scheme="https", host="httpbin.org", port=443, http_compress=True) + NodeConfig(scheme="http", host="localhost", port=8080, http_compress=True) ) resp, data = await node.perform_request("HEAD", "/anything") diff --git a/tests/node/test_http_httpx.py b/tests/node/test_http_httpx.py index 588c074b..9896e3c8 100644 --- a/tests/node/test_http_httpx.py +++ b/tests/node/test_http_httpx.py @@ -145,13 +145,13 @@ async def test_merge_headers(self): assert request.headers["h3"] == "v3" -def test_ssl_assert_fingerprint(httpbin_cert_fingerprint): +def test_ssl_assert_fingerprint(cert_fingerprint): with pytest.raises(ValueError, match="httpx does not support certificate pinning"): HttpxAsyncHttpNode( NodeConfig( scheme="https", - host="httpbin.org", - port=443, - ssl_assert_fingerprint=httpbin_cert_fingerprint, + host="localhost", + port=9200, + ssl_assert_fingerprint=cert_fingerprint, ) ) diff --git a/tests/node/test_urllib3_chain_certs.py b/tests/node/test_urllib3_chain_certs.py index 01bada27..bd4422d9 100644 --- a/tests/node/test_urllib3_chain_certs.py +++ b/tests/node/test_urllib3_chain_certs.py @@ -35,8 +35,8 @@ def test_ssl_assert_fingerprint_invalid_length(node_cls): node_cls( NodeConfig( "https", - "httpbin.org", - 443, + "localhost", + 9200, ssl_assert_fingerprint="0000", ) ) @@ -52,9 +52,9 @@ def test_ssl_assert_fingerprint_invalid_length(node_cls): @pytest.mark.parametrize( "ssl_assert_fingerprint", [ - "8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e", - "8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e", - "8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", + "18efbd94dda87e3598a1251f9440cd2f4fd1dbf08be007c1012e992e830ca262", + "18:EF:BD:94:DD:A8:7E:35:98:A1:25:1F:94:40:CD:2F:4F:D1:DB:F0:8B:E0:07:C1:01:2E:99:2E:83:0C:A2:62", + "18EFBD94DDA87E3598A1251F9440CD2F4FD1DBF08BE007C1012E992E830CA262", ], ) def test_assert_fingerprint_in_cert_chain(node_cls, ssl_assert_fingerprint): @@ -62,7 +62,7 @@ def test_assert_fingerprint_in_cert_chain(node_cls, ssl_assert_fingerprint): node = node_cls( NodeConfig( "https", - "httpbin.org", + "www.elastic.co", 443, ssl_assert_fingerprint=ssl_assert_fingerprint, ) @@ -79,7 +79,7 @@ def test_assert_fingerprint_in_cert_chain_failure(node_cls): node = node_cls( NodeConfig( "https", - "httpbin.org", + "www.elastic.co", 443, ssl_assert_fingerprint="0" * 64, ) @@ -89,6 +89,7 @@ def test_assert_fingerprint_in_cert_chain_failure(node_cls): node.perform_request("GET", "/") err = str(e.value) + print(err) assert "Fingerprints did not match." in err # This is the bad value we "expected" assert ( @@ -96,4 +97,4 @@ def test_assert_fingerprint_in_cert_chain_failure(node_cls): in err ) # This is the root CA for httpbin.org with a leading comma to denote more than one cert was listed. - assert ', "8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e"' in err + assert '"18efbd94dda87e3598a1251f9440cd2f4fd1dbf08be007c1012e992e830ca262"' in err From deaf3615066c85e251746ed10a57a1e1dfd76fad Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 9 Jul 2025 16:40:35 +0100 Subject: [PATCH 3/5] use pytest-httpbin instead of a local service --- .github/workflows/ci.yml | 4 ---- setup.py | 1 + tests/async_/test_async_transport.py | 6 ++--- tests/async_/test_httpbin.py | 24 +++++++++---------- tests/conftest.py | 11 ++------- tests/node/test_http_aiohttp.py | 30 +++++++++++++---------- tests/test_httpbin.py | 24 +++++++++---------- tests/test_logging.py | 36 ++++++++++++++-------------- 8 files changed, 65 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc085315..68025701 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,10 +57,6 @@ jobs: name: test-${{ matrix.python-version }} ${{ matrix.nox-session }} continue-on-error: ${{ matrix.experimental }} services: - httpbin: - image: kennethreitz/httpbin - ports: - - 8080:80 elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:9.0.2 env: diff --git a/setup.py b/setup.py index b317cd42..21832718 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ "pytest-cov", "pytest-mock", "pytest-asyncio", + "pytest-httpbin", "pytest-httpserver", "trustme", "requests", diff --git a/tests/async_/test_async_transport.py b/tests/async_/test_async_transport.py index 811931dd..24a869ce 100644 --- a/tests/async_/test_async_transport.py +++ b/tests/async_/test_async_transport.py @@ -46,20 +46,20 @@ @pytest.mark.asyncio -async def test_async_transport_httpbin(httpbin_node_config): +async def test_async_transport_httpbin(httpbin_node_config, httpbin): t = AsyncTransport([httpbin_node_config], meta_header=False) resp, data = await t.perform_request("GET", "/anything?key=value") assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "http://localhost:8080/anything?key=value" + assert data["url"] == f"{httpbin.url}/anything?key=value" assert data["args"] == {"key": "value"} data["headers"].pop("X-Amzn-Trace-Id", None) assert data["headers"] == { "User-Agent": DEFAULT_USER_AGENT, "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", } diff --git a/tests/async_/test_httpbin.py b/tests/async_/test_httpbin.py index c8c6f97c..f6cc747b 100644 --- a/tests/async_/test_httpbin.py +++ b/tests/async_/test_httpbin.py @@ -27,7 +27,7 @@ @pytest.mark.asyncio -async def test_simple_request(httpbin_node_config): +async def test_simple_request(httpbin_node_config, httpbin): t = AsyncTransport([httpbin_node_config]) resp, data = await t.perform_request( @@ -38,7 +38,7 @@ async def test_simple_request(httpbin_node_config): ) assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "http://localhost:8080/anything?key[]=1&key[]=2&q1&q2=" + assert data["url"] == f"{httpbin.url}/anything?key[]=1&key[]=2&q1&q2=" # httpbin makes no-value query params into '' assert data["args"] == { @@ -54,13 +54,13 @@ async def test_simple_request(httpbin_node_config): "Content-Length": "15", "Custom": "headeR", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", } assert all(v == data["headers"][k] for k, v in request_headers.items()) @pytest.mark.asyncio -async def test_node(httpbin_node_config): +async def test_node(httpbin_node_config, httpbin): def new_node(**kwargs): return AiohttpHttpNode(dataclasses.replace(httpbin_node_config, **kwargs)) @@ -71,11 +71,11 @@ def new_node(**kwargs): assert parsed == { "headers": { "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } node = new_node(http_compress=True) @@ -86,11 +86,11 @@ def new_node(**kwargs): "headers": { "Accept-Encoding": "gzip", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } resp, data = await node.perform_request("GET", "/anything", body=b"hello, world!") @@ -103,11 +103,11 @@ def new_node(**kwargs): "Content-Type": "application/octet-stream", "Content-Length": "33", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } resp, data = await node.perform_request( @@ -125,9 +125,9 @@ def new_node(**kwargs): "Content-Length": "36", "Content-Type": "application/json", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "POST", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } diff --git a/tests/conftest.py b/tests/conftest.py index 76d10102..0a2d8fcd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,16 +84,9 @@ def cert_fingerprint(request) -> str: @pytest.fixture(scope="session") -def httpbin_node_config() -> NodeConfig: - try: - sock = socket.create_connection(("localhost", 8080)) - except Exception as e: - pytest.skip( - f"Couldn't connect to localhost:8080, not running httpbin image? {e}" - ) - sock.close() +def httpbin_node_config(httpbin) -> NodeConfig: return NodeConfig( - "http", "localhost", 8080, verify_certs=False, ssl_show_warn=False + "http", httpbin.host, httpbin.port, verify_certs=False, ssl_show_warn=False ) diff --git a/tests/node/test_http_aiohttp.py b/tests/node/test_http_aiohttp.py index 84373cc5..e3df4ea1 100644 --- a/tests/node/test_http_aiohttp.py +++ b/tests/node/test_http_aiohttp.py @@ -307,8 +307,10 @@ async def test_ssl_assert_fingerprint(cert_fingerprint): @pytest.mark.asyncio -async def test_default_headers(): - node = AiohttpHttpNode(NodeConfig(scheme="http", host="localhost", port=8080)) +async def test_default_headers(httpbin): + node = AiohttpHttpNode( + NodeConfig(scheme="http", host=httpbin.host, port=httpbin.port) + ) resp, data = await node.perform_request("GET", "/anything") assert resp.status == 200 @@ -316,18 +318,18 @@ async def test_default_headers(): headers.pop("X-Amzn-Trace-Id", None) assert headers == { "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, } @pytest.mark.asyncio -async def test_custom_headers(): +async def test_custom_headers(httpbin): node = AiohttpHttpNode( NodeConfig( scheme="http", - host="localhost", - port=8080, + host=httpbin.host, + port=httpbin.port, headers={"accept-encoding": "gzip", "Content-Type": "application/json"}, ) ) @@ -347,18 +349,18 @@ async def test_custom_headers(): "Accept-Encoding": "gzip", "Connection": "keep-alive", "Content-Type": "application/x-ndjson", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": "custom-agent/1.2.3", } @pytest.mark.asyncio -async def test_custom_user_agent(): +async def test_custom_user_agent(httpbin): node = AiohttpHttpNode( NodeConfig( scheme="http", - host="localhost", - port=8080, + host=httpbin.host, + port=httpbin.port, headers={ "accept-encoding": "gzip", "Content-Type": "application/json", @@ -378,7 +380,7 @@ async def test_custom_user_agent(): "Accept-Encoding": "gzip", "Connection": "keep-alive", "Content-Type": "application/json", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": "custom-agent/1.2.3", } @@ -389,9 +391,11 @@ def test_repr(): @pytest.mark.asyncio -async def test_head(): +async def test_head(httpbin): node = AiohttpHttpNode( - NodeConfig(scheme="http", host="localhost", port=8080, http_compress=True) + NodeConfig( + scheme="http", host=httpbin.host, port=httpbin.port, http_compress=True + ) ) resp, data = await node.perform_request("HEAD", "/anything") diff --git a/tests/test_httpbin.py b/tests/test_httpbin.py index a8c43e3c..c2ba9227 100644 --- a/tests/test_httpbin.py +++ b/tests/test_httpbin.py @@ -26,7 +26,7 @@ @pytest.mark.parametrize("node_class", ["urllib3", "requests"]) -def test_simple_request(node_class, httpbin_node_config): +def test_simple_request(node_class, httpbin_node_config, httpbin): t = Transport([httpbin_node_config], node_class=node_class) resp, data = t.perform_request( @@ -37,7 +37,7 @@ def test_simple_request(node_class, httpbin_node_config): ) assert resp.status == 200 assert data["method"] == "GET" - assert data["url"] == "http://localhost:8080/anything?key[]=1&key[]=2&q1&q2=" + assert data["url"] == f"{httpbin.url}/anything?key[]=1&key[]=2&q1&q2=" # httpbin makes no-value query params into '' assert data["args"] == { @@ -53,13 +53,13 @@ def test_simple_request(node_class, httpbin_node_config): "Content-Length": "15", "Custom": "headeR", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", } assert all(v == data["headers"][k] for k, v in request_headers.items()) @pytest.mark.parametrize("node_class", ["urllib3", "requests"]) -def test_node(node_class, httpbin_node_config): +def test_node(node_class, httpbin_node_config, httpbin): def new_node(**kwargs): return NODE_CLASS_NAMES[node_class]( dataclasses.replace(httpbin_node_config, **kwargs) @@ -73,11 +73,11 @@ def new_node(**kwargs): "headers": { "Accept-Encoding": "identity", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } node = new_node(http_compress=True) @@ -88,11 +88,11 @@ def new_node(**kwargs): "headers": { "Accept-Encoding": "gzip", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } resp, data = node.perform_request("GET", "/anything", body=b"hello, world!") @@ -104,11 +104,11 @@ def new_node(**kwargs): "Content-Encoding": "gzip", "Content-Length": "33", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "GET", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } resp, data = node.perform_request( @@ -126,11 +126,11 @@ def new_node(**kwargs): "Content-Length": "36", "Content-Type": "application/json", "Connection": "keep-alive", - "Host": "localhost:8080", + "Host": f"{httpbin.host}:{httpbin.port}", "User-Agent": DEFAULT_USER_AGENT, }, "method": "POST", - "url": "http://localhost:8080/anything", + "url": f"{httpbin.url}/anything", } diff --git a/tests/test_logging.py b/tests/test_logging.py index 86ee9532..98e084c6 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -38,7 +38,7 @@ @node_class @pytest.mark.asyncio -async def test_debug_logging(node_class, httpbin_node_config): +async def test_debug_logging(node_class, httpbin_node_config, httpbin): debug_logging() stream = io.StringIO() @@ -59,8 +59,8 @@ async def test_debug_logging(node_class, httpbin_node_config): print(node_class) print(stream.getvalue()) - lines = stream.getvalue().split("\n") - print(lines) + response = stream.getvalue() + print(response) for line in [ "> GET /anything HTTP/1.1", "> Connection: keep-alive", @@ -72,23 +72,23 @@ async def test_debug_logging(node_class, httpbin_node_config): "< Access-Control-Allow-Origin: *", "< Content-Type: application/json", "< {", - ' "args": {}, ', - ' "data": "{\\"key\\":\\"value\\"}", ', - ' "files": {}, ', - ' "form": {}, ', - ' "headers": {', - ' "Content-Type": "application/json", ', - ' "Host": "localhost:8080", ', - f' "User-Agent": "{DEFAULT_USER_AGENT}"', - " }, ", - ' "json": {', - ' "key": "value"', - " }, ", - ' "method": "GET", ', - ' "url": "http://localhost:8080/anything"', + '"args":{},', + '"data":"{\\"key\\":\\"value\\"}",', + '"files":{},', + '"form":{},', + '"headers":{', + '"Content-Type":"application/json",', + f'"Host":"{httpbin.host}:{httpbin.port}",', + f'"User-Agent":"{DEFAULT_USER_AGENT}"', + "},", + '"json":{', + '"key":"value"', + "},", + '"method":"GET",', + f'"url":"{httpbin.url}/anything"', "}", ]: - assert line in lines + assert line in response @node_class From 4bf82b6e94c461498a68590769519030b65be21b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 9 Jul 2025 16:45:53 +0100 Subject: [PATCH 4/5] use pytest-httpbin also for SSL tests --- .github/workflows/ci.yml | 7 ------- tests/conftest.py | 16 +++++++++----- tests/node/test_http_aiohttp.py | 8 +++---- tests/node/test_http_httpx.py | 6 +++--- tests/node/test_urllib3_chain_certs.py | 29 ++++++++++---------------- 5 files changed, 29 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68025701..e4e679e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,13 +56,6 @@ jobs: runs-on: ${{ matrix.os }} name: test-${{ matrix.python-version }} ${{ matrix.nox-session }} continue-on-error: ${{ matrix.experimental }} - services: - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:9.0.2 - env: - discovery.type: single-node - ports: - - 9200:9200 steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/tests/conftest.py b/tests/conftest.py index 0a2d8fcd..405df663 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,10 +66,12 @@ async def perform_request(self, *args, **kwargs): return NodeApiResponse(meta, self.body) -@pytest.fixture(scope="session", params=[True, False]) -def cert_fingerprint(request) -> str: - """Gets the SHA256 fingerprint of the certificate for localhost:9200""" - sock = socket.create_connection(("localhost", 9200)) +@pytest.fixture( + scope="session", params=["short-lower", "short-upper", "long-lower", "long-upper"] +) +def cert_fingerprint(request, httpbin_secure) -> str: + """Gets the SHA256 fingerprint of the certificate for the secure httpbin""" + sock = socket.create_connection((httpbin_secure.host, httpbin_secure.port)) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE @@ -77,7 +79,11 @@ def cert_fingerprint(request) -> str: digest = hashlib.sha256(sock.getpeercert(binary_form=True)).hexdigest() assert len(digest) == 64 sock.close() - if request.param: + if "upper" in request.param: + digest = digest.upper() + else: + digest = digest.lower() + if "short" in request.param: return digest else: return ":".join([digest[i : i + 2] for i in range(0, len(digest), 2)]) diff --git a/tests/node/test_http_aiohttp.py b/tests/node/test_http_aiohttp.py index e3df4ea1..c9cddf7d 100644 --- a/tests/node/test_http_aiohttp.py +++ b/tests/node/test_http_aiohttp.py @@ -290,19 +290,19 @@ async def test_head_workaround(self, aiohttp_fixed_head_bug): @pytest.mark.asyncio -async def test_ssl_assert_fingerprint(cert_fingerprint): +async def test_ssl_assert_fingerprint(cert_fingerprint, httpbin_secure): with warnings.catch_warnings(record=True) as w: node = AiohttpHttpNode( NodeConfig( scheme="https", - host="localhost", - port=9200, + host=httpbin_secure.host, + port=httpbin_secure.port, ssl_assert_fingerprint=cert_fingerprint, ) ) resp, _ = await node.perform_request("GET", "/") - assert resp.status == 401 + assert resp.status == 200 assert [str(x.message) for x in w if x.category != DeprecationWarning] == [] diff --git a/tests/node/test_http_httpx.py b/tests/node/test_http_httpx.py index 9896e3c8..ce6e7f4a 100644 --- a/tests/node/test_http_httpx.py +++ b/tests/node/test_http_httpx.py @@ -145,13 +145,13 @@ async def test_merge_headers(self): assert request.headers["h3"] == "v3" -def test_ssl_assert_fingerprint(cert_fingerprint): +def test_ssl_assert_fingerprint(cert_fingerprint, httpbin_secure): with pytest.raises(ValueError, match="httpx does not support certificate pinning"): HttpxAsyncHttpNode( NodeConfig( scheme="https", - host="localhost", - port=9200, + host=httpbin_secure.host, + port=httpbin_secure.port, ssl_assert_fingerprint=cert_fingerprint, ) ) diff --git a/tests/node/test_urllib3_chain_certs.py b/tests/node/test_urllib3_chain_certs.py index bd4422d9..6dc52571 100644 --- a/tests/node/test_urllib3_chain_certs.py +++ b/tests/node/test_urllib3_chain_certs.py @@ -30,13 +30,13 @@ @requires_ssl_assert_fingerprint_in_chain @pytest.mark.parametrize("node_cls", [Urllib3HttpNode, RequestsHttpNode]) -def test_ssl_assert_fingerprint_invalid_length(node_cls): +def test_ssl_assert_fingerprint_invalid_length(node_cls, httpbin_secure): with pytest.raises(ValueError) as e: node_cls( NodeConfig( "https", - "localhost", - 9200, + httpbin_secure.host, + httpbin_secure.port, ssl_assert_fingerprint="0000", ) ) @@ -49,22 +49,14 @@ def test_ssl_assert_fingerprint_invalid_length(node_cls): @requires_ssl_assert_fingerprint_in_chain @pytest.mark.parametrize("node_cls", [Urllib3HttpNode, RequestsHttpNode]) -@pytest.mark.parametrize( - "ssl_assert_fingerprint", - [ - "18efbd94dda87e3598a1251f9440cd2f4fd1dbf08be007c1012e992e830ca262", - "18:EF:BD:94:DD:A8:7E:35:98:A1:25:1F:94:40:CD:2F:4F:D1:DB:F0:8B:E0:07:C1:01:2E:99:2E:83:0C:A2:62", - "18EFBD94DDA87E3598A1251F9440CD2F4FD1DBF08BE007C1012E992E830CA262", - ], -) -def test_assert_fingerprint_in_cert_chain(node_cls, ssl_assert_fingerprint): +def test_assert_fingerprint_in_cert_chain(node_cls, cert_fingerprint, httpbin_secure): with warnings.catch_warnings(record=True) as w: node = node_cls( NodeConfig( "https", - "www.elastic.co", - 443, - ssl_assert_fingerprint=ssl_assert_fingerprint, + httpbin_secure.host, + httpbin_secure.port, + ssl_assert_fingerprint=cert_fingerprint, ) ) meta, _ = node.perform_request("GET", "/") @@ -75,7 +67,9 @@ def test_assert_fingerprint_in_cert_chain(node_cls, ssl_assert_fingerprint): @requires_ssl_assert_fingerprint_in_chain @pytest.mark.parametrize("node_cls", [Urllib3HttpNode, RequestsHttpNode]) -def test_assert_fingerprint_in_cert_chain_failure(node_cls): +def test_assert_fingerprint_in_cert_chain_failure( + node_cls, httpbin_secure, cert_fingerprint +): node = node_cls( NodeConfig( "https", @@ -89,7 +83,6 @@ def test_assert_fingerprint_in_cert_chain_failure(node_cls): node.perform_request("GET", "/") err = str(e.value) - print(err) assert "Fingerprints did not match." in err # This is the bad value we "expected" assert ( @@ -97,4 +90,4 @@ def test_assert_fingerprint_in_cert_chain_failure(node_cls): in err ) # This is the root CA for httpbin.org with a leading comma to denote more than one cert was listed. - assert '"18efbd94dda87e3598a1251f9440cd2f4fd1dbf08be007c1012e992e830ca262"' in err + assert ', "cbb522d7b7f127ad6a0113865bdf1cd4102e7d0759af635a7cf4720dc963c53b"' in err From 64a445071d47b22c646b7c5f8ec4927bdd3a56b6 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 10 Jul 2025 14:14:46 +0100 Subject: [PATCH 5/5] fix inaccurate comment --- tests/node/test_urllib3_chain_certs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/node/test_urllib3_chain_certs.py b/tests/node/test_urllib3_chain_certs.py index 6dc52571..8834010e 100644 --- a/tests/node/test_urllib3_chain_certs.py +++ b/tests/node/test_urllib3_chain_certs.py @@ -89,5 +89,5 @@ def test_assert_fingerprint_in_cert_chain_failure( 'Expected "0000000000000000000000000000000000000000000000000000000000000000",' in err ) - # This is the root CA for httpbin.org with a leading comma to denote more than one cert was listed. + # This is the root CA for www.elastic.co with a leading comma to denote more than one cert was listed. assert ', "cbb522d7b7f127ad6a0113865bdf1cd4102e7d0759af635a7cf4720dc963c53b"' in err