From 68692fe3ac5dbe0717e7a83607425cb1b0614f58 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 12 May 2026 08:29:53 -0700 Subject: [PATCH 1/2] Skip urllib3 SSRF redirect tests when urllib3 not installed (#1681) TestRedirectRevalidation mocks urllib3.PoolManager and exercises read_range, which internally calls _urllib3_timeout() and tries to import urllib3 lazily. urllib3 is an optional runtime dependency (_HTTPSource falls back to stdlib urllib.request when it's missing), but the test class did not gate on its presence, so CI without urllib3 saw four ModuleNotFoundError failures. Add an autouse pytest.importorskip("urllib3") fixture on the class so the urllib3-specific tests skip cleanly when the package is absent. The stdlib redirect-handler tests in the same class remain untouched. --- xrspatial/geotiff/tests/test_ssrf_hardening_1664.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py b/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py index 9c45fefd..111bbd60 100644 --- a/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py +++ b/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py @@ -267,6 +267,16 @@ def request(self, method, url, **kwargs): class TestRedirectRevalidation: + # The tests in this class exercise the urllib3 transport path: they mock + # urllib3.PoolManager and call read_range(), which internally builds a + # urllib3.Timeout via _urllib3_timeout(). urllib3 is an optional runtime + # dependency (_HTTPSource falls back to stdlib urllib.request when it's + # missing -- see _reader.py:615-617). Skip rather than fail when the + # package isn't installed; the stdlib fallback path is covered elsewhere. + @pytest.fixture(autouse=True) + def _require_urllib3(self): + pytest.importorskip("urllib3") + def test_urllib3_redirect_to_private_rejected(self, monkeypatch): """Public host that 302-redirects to loopback must be rejected.""" # Initial validator pass: example.com resolves to a public IP. From 4842bc335eae09ed6f15a68bc09fc1135c8ffe05 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 12 May 2026 08:33:38 -0700 Subject: [PATCH 2/2] Move urllib3 importorskip from autouse fixture to per-method (#1681) The autouse fixture on TestRedirectRevalidation also skipped the three test_stdlib_* tests in the same class, which exercise the stdlib redirect handler directly and must run regardless of whether urllib3 is installed. Move the gate into each of the four urllib3 test methods individually so the stdlib tests stay live. --- .../geotiff/tests/test_ssrf_hardening_1664.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py b/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py index 111bbd60..f1a12f02 100644 --- a/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py +++ b/xrspatial/geotiff/tests/test_ssrf_hardening_1664.py @@ -267,18 +267,17 @@ def request(self, method, url, **kwargs): class TestRedirectRevalidation: - # The tests in this class exercise the urllib3 transport path: they mock + # The test_urllib3_* tests exercise the urllib3 transport path: they mock # urllib3.PoolManager and call read_range(), which internally builds a # urllib3.Timeout via _urllib3_timeout(). urllib3 is an optional runtime # dependency (_HTTPSource falls back to stdlib urllib.request when it's - # missing -- see _reader.py:615-617). Skip rather than fail when the - # package isn't installed; the stdlib fallback path is covered elsewhere. - @pytest.fixture(autouse=True) - def _require_urllib3(self): - pytest.importorskip("urllib3") + # missing -- see _reader.py:615-617), so each urllib3-using test starts + # with pytest.importorskip("urllib3"). The test_stdlib_* tests below + # exercise the stdlib redirect handler directly and run regardless. def test_urllib3_redirect_to_private_rejected(self, monkeypatch): """Public host that 302-redirects to loopback must be rejected.""" + pytest.importorskip("urllib3") # Initial validator pass: example.com resolves to a public IP. monkeypatch.setattr( socket, 'getaddrinfo', _fake_getaddrinfo('93.184.216.34')) @@ -300,6 +299,7 @@ def test_urllib3_redirect_to_private_rejected(self, monkeypatch): def test_urllib3_redirect_to_public_followed(self, monkeypatch): """Public -> public redirect is followed; validator passes each hop.""" + pytest.importorskip("urllib3") monkeypatch.setattr( socket, 'getaddrinfo', _fake_getaddrinfo('93.184.216.34')) src = _reader_mod._HTTPSource('https://example.com/cog.tif') @@ -315,6 +315,7 @@ def test_urllib3_redirect_to_public_followed(self, monkeypatch): def test_urllib3_redirect_chain_capped(self, monkeypatch): """More than _HTTP_MAX_REDIRECTS hops raises rather than looping.""" + pytest.importorskip("urllib3") monkeypatch.setattr( socket, 'getaddrinfo', _fake_getaddrinfo('93.184.216.34')) src = _reader_mod._HTTPSource('https://example.com/cog.tif') @@ -332,6 +333,7 @@ def test_urllib3_redirect_chain_capped(self, monkeypatch): def test_urllib3_relative_location_resolved(self, monkeypatch): """Relative Location like ``/other.tif`` resolves against the source.""" + pytest.importorskip("urllib3") monkeypatch.setattr( socket, 'getaddrinfo', _fake_getaddrinfo('93.184.216.34')) src = _reader_mod._HTTPSource('https://example.com/dir/cog.tif')