11"""Tests for httpx utility functions."""
22
33import httpx
4+ import pytest
45
5- from mcp .shared ._httpx_utils import create_mcp_http_client
6+ from mcp .shared ._httpx_utils import RedirectPolicy , _check_redirect , create_mcp_http_client
7+
8+ pytestmark = pytest .mark .anyio
69
710
811def test_default_settings ():
@@ -22,3 +25,142 @@ def test_custom_parameters():
2225
2326 assert client .headers ["Authorization" ] == "Bearer token"
2427 assert client .timeout .connect == 60.0
28+
29+
30+ def test_default_redirect_policy ():
31+ """Test that the default redirect policy is BLOCK_SCHEME_DOWNGRADE."""
32+ client = create_mcp_http_client ()
33+ # Event hooks should be installed for the default policy
34+ assert len (client .event_hooks ["response" ]) == 1
35+
36+
37+ def test_allow_all_policy_no_hooks ():
38+ """Test that ALLOW_ALL does not install event hooks."""
39+ client = create_mcp_http_client (redirect_policy = RedirectPolicy .ALLOW_ALL )
40+ assert len (client .event_hooks ["response" ]) == 0
41+
42+
43+ # --- _check_redirect unit tests ---
44+
45+
46+ async def test_check_redirect_ignores_non_redirect ():
47+ """Test that non-redirect responses are ignored."""
48+ response = httpx .Response (200 , request = httpx .Request ("GET" , "https://example.com" ))
49+ # Should not raise
50+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
51+ await _check_redirect (response , RedirectPolicy .ENFORCE_HTTPS )
52+
53+
54+ async def test_check_redirect_ignores_redirect_without_next_request ():
55+ """Test that redirect responses without next_request are ignored."""
56+ response = httpx .Response (
57+ 302 ,
58+ headers = {"Location" : "http://evil.com" },
59+ request = httpx .Request ("GET" , "https://example.com" ),
60+ )
61+ # next_request is None on a manually constructed response
62+ assert response .next_request is None
63+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
64+
65+
66+ # --- BLOCK_SCHEME_DOWNGRADE tests ---
67+
68+
69+ async def test_block_scheme_downgrade_blocks_https_to_http ():
70+ """Test BLOCK_SCHEME_DOWNGRADE blocks HTTPS->HTTP redirect."""
71+ response = httpx .Response (
72+ 302 ,
73+ headers = {"Location" : "http://evil.com" },
74+ request = httpx .Request ("GET" , "https://example.com" ),
75+ )
76+ response .next_request = httpx .Request ("GET" , "http://evil.com" )
77+
78+ with pytest .raises (httpx .HTTPStatusError , match = "HTTPS-to-HTTP redirect blocked" ):
79+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
80+
81+
82+ async def test_block_scheme_downgrade_allows_https_to_https ():
83+ """Test BLOCK_SCHEME_DOWNGRADE allows HTTPS->HTTPS redirect."""
84+ response = httpx .Response (
85+ 302 ,
86+ headers = {"Location" : "https://other.com" },
87+ request = httpx .Request ("GET" , "https://example.com" ),
88+ )
89+ response .next_request = httpx .Request ("GET" , "https://other.com" )
90+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
91+
92+
93+ async def test_block_scheme_downgrade_allows_http_to_http ():
94+ """Test BLOCK_SCHEME_DOWNGRADE allows HTTP->HTTP redirect."""
95+ response = httpx .Response (
96+ 302 ,
97+ headers = {"Location" : "http://other.com" },
98+ request = httpx .Request ("GET" , "http://example.com" ),
99+ )
100+ response .next_request = httpx .Request ("GET" , "http://other.com" )
101+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
102+
103+
104+ async def test_block_scheme_downgrade_allows_http_to_https ():
105+ """Test BLOCK_SCHEME_DOWNGRADE allows HTTP->HTTPS upgrade."""
106+ response = httpx .Response (
107+ 302 ,
108+ headers = {"Location" : "https://other.com" },
109+ request = httpx .Request ("GET" , "http://example.com" ),
110+ )
111+ response .next_request = httpx .Request ("GET" , "https://other.com" )
112+ await _check_redirect (response , RedirectPolicy .BLOCK_SCHEME_DOWNGRADE )
113+
114+
115+ # --- ENFORCE_HTTPS tests ---
116+
117+
118+ async def test_enforce_https_blocks_http_target ():
119+ """Test ENFORCE_HTTPS blocks any HTTP redirect target."""
120+ response = httpx .Response (
121+ 302 ,
122+ headers = {"Location" : "http://evil.com" },
123+ request = httpx .Request ("GET" , "https://example.com" ),
124+ )
125+ response .next_request = httpx .Request ("GET" , "http://evil.com" )
126+
127+ with pytest .raises (httpx .HTTPStatusError , match = "Non-HTTPS redirect blocked" ):
128+ await _check_redirect (response , RedirectPolicy .ENFORCE_HTTPS )
129+
130+
131+ async def test_enforce_https_blocks_http_to_http ():
132+ """Test ENFORCE_HTTPS blocks HTTP->HTTP redirect."""
133+ response = httpx .Response (
134+ 302 ,
135+ headers = {"Location" : "http://other.com" },
136+ request = httpx .Request ("GET" , "http://example.com" ),
137+ )
138+ response .next_request = httpx .Request ("GET" , "http://other.com" )
139+
140+ with pytest .raises (httpx .HTTPStatusError , match = "Non-HTTPS redirect blocked" ):
141+ await _check_redirect (response , RedirectPolicy .ENFORCE_HTTPS )
142+
143+
144+ async def test_enforce_https_allows_https_target ():
145+ """Test ENFORCE_HTTPS allows HTTPS redirect target."""
146+ response = httpx .Response (
147+ 302 ,
148+ headers = {"Location" : "https://other.com" },
149+ request = httpx .Request ("GET" , "https://example.com" ),
150+ )
151+ response .next_request = httpx .Request ("GET" , "https://other.com" )
152+ await _check_redirect (response , RedirectPolicy .ENFORCE_HTTPS )
153+
154+
155+ # --- ALLOW_ALL tests ---
156+
157+
158+ async def test_allow_all_permits_https_to_http ():
159+ """Test ALLOW_ALL permits HTTPS->HTTP redirect."""
160+ response = httpx .Response (
161+ 302 ,
162+ headers = {"Location" : "http://evil.com" },
163+ request = httpx .Request ("GET" , "https://example.com" ),
164+ )
165+ response .next_request = httpx .Request ("GET" , "http://evil.com" )
166+ await _check_redirect (response , RedirectPolicy .ALLOW_ALL )
0 commit comments