|  | 
| 3 | 3 | 
 | 
| 4 | 4 | import logging | 
| 5 | 5 | import unittest.mock | 
|  | 6 | +from collections import deque | 
| 6 | 7 | 
 | 
| 7 | 8 | import pytest | 
| 8 | 9 | 
 | 
| @@ -160,3 +161,178 @@ def test_basic_query_through_proxy( | 
| 160 | 161 |         "/queries/v1/query-request" in r["request"]["url"] | 
| 161 | 162 |         for r in target_reqs["requests"] | 
| 162 | 163 |     ) | 
|  | 164 | + | 
|  | 165 | + | 
|  | 166 | +""" | 
|  | 167 | +Note: The single-target no_proxy test was removed in favor of | 
|  | 168 | +test_no_proxy_multiple_hosts_and_ports, which validates both backend and storage | 
|  | 169 | +paths and multiple no_proxy entries using shared session manager logic. | 
|  | 170 | +""" | 
|  | 171 | + | 
|  | 172 | + | 
|  | 173 | +@pytest.mark.skipolddriver | 
|  | 174 | +@pytest.mark.parametrize( | 
|  | 175 | +    "no_proxy_factory,expect_backend_proxy,expect_storage_proxy", | 
|  | 176 | +    [ | 
|  | 177 | +        (lambda target, storage: [f"localhost:{target}"], False, True), | 
|  | 178 | +        (lambda target, storage: [f"localhost:{storage}"], True, False), | 
|  | 179 | +        ( | 
|  | 180 | +            lambda target, storage: [f"localhost:{target}", f"localhost:{storage}"], | 
|  | 181 | +            False, | 
|  | 182 | +            False, | 
|  | 183 | +        ), | 
|  | 184 | +        ( | 
|  | 185 | +            lambda target, storage: ["localhost"], | 
|  | 186 | +            False, | 
|  | 187 | +            False, | 
|  | 188 | +        ),  # host-only bypasses both | 
|  | 189 | +        # Tuple and set variants | 
|  | 190 | +        (lambda target, storage: (f"localhost:{target}",), False, True), | 
|  | 191 | +        (lambda target, storage: {f"localhost:{storage}"}, True, False), | 
|  | 192 | +        ( | 
|  | 193 | +            lambda target, storage: frozenset( | 
|  | 194 | +                {f"localhost:{target}", f"localhost:{storage}"} | 
|  | 195 | +            ), | 
|  | 196 | +            False, | 
|  | 197 | +            False, | 
|  | 198 | +        ), | 
|  | 199 | +        # Deque (generic iterable) variant | 
|  | 200 | +        ( | 
|  | 201 | +            lambda target, storage: deque( | 
|  | 202 | +                [f"localhost:{target}", f"localhost:{storage}"] | 
|  | 203 | +            ), | 
|  | 204 | +            False, | 
|  | 205 | +            False, | 
|  | 206 | +        ), | 
|  | 207 | +        # One long CSV string with many irrelevant entries around both target and storage | 
|  | 208 | +        ( | 
|  | 209 | +            lambda target, storage: ( | 
|  | 210 | +                "foo.invalid:1,bar.invalid:2," | 
|  | 211 | +                f"localhost:{target}," | 
|  | 212 | +                "baz.invalid:3,qux.invalid:4," | 
|  | 213 | +                f"localhost:{storage}," | 
|  | 214 | +                "zoo.invalid:5" | 
|  | 215 | +            ), | 
|  | 216 | +            False, | 
|  | 217 | +            False, | 
|  | 218 | +        ), | 
|  | 219 | +    ], | 
|  | 220 | +) | 
|  | 221 | +@pytest.mark.parametrize("proxy_method", ["explicit_args", "env_vars"]) | 
|  | 222 | +def test_no_proxy_multiple_hosts_and_ports( | 
|  | 223 | +    wiremock_backend_storage_proxy, | 
|  | 224 | +    wiremock_generic_mappings_dir, | 
|  | 225 | +    wiremock_mapping_dir, | 
|  | 226 | +    proxy_env_vars, | 
|  | 227 | +    proxy_method, | 
|  | 228 | +    no_proxy_factory, | 
|  | 229 | +    expect_backend_proxy, | 
|  | 230 | +    expect_storage_proxy, | 
|  | 231 | +): | 
|  | 232 | +    target_wm, storage_wm, proxy_wm = wiremock_backend_storage_proxy | 
|  | 233 | + | 
|  | 234 | +    # Configure DB and storage mappings (no Via header assertion; we check journals) | 
|  | 235 | +    password_mapping = wiremock_mapping_dir / "auth/password/successful_flow.json" | 
|  | 236 | +    multi_chunk_request_mapping = ( | 
|  | 237 | +        wiremock_mapping_dir / "queries/select_large_request_successful.json" | 
|  | 238 | +    ) | 
|  | 239 | +    chunk_1_mapping = wiremock_mapping_dir / "queries/chunk_1.json" | 
|  | 240 | +    chunk_2_mapping = wiremock_mapping_dir / "queries/chunk_2.json" | 
|  | 241 | +    disconnect_mapping = ( | 
|  | 242 | +        wiremock_generic_mappings_dir / "snowflake_disconnect_successful.json" | 
|  | 243 | +    ) | 
|  | 244 | +    telemetry_mapping = wiremock_generic_mappings_dir / "telemetry.json" | 
|  | 245 | + | 
|  | 246 | +    # Import login, disconnect, telemetry on backend | 
|  | 247 | +    target_wm.import_mapping_with_default_placeholders(password_mapping) | 
|  | 248 | +    target_wm.add_mapping(disconnect_mapping) | 
|  | 249 | +    target_wm.add_mapping(telemetry_mapping) | 
|  | 250 | + | 
|  | 251 | +    # Add multi-chunk query response on backend, but point chunk URLs to storage host | 
|  | 252 | +    target_wm.add_mapping( | 
|  | 253 | +        multi_chunk_request_mapping, | 
|  | 254 | +        placeholders={ | 
|  | 255 | +            "{{WIREMOCK_HTTP_HOST_WITH_PORT}}": storage_wm.http_host_with_port | 
|  | 256 | +        }, | 
|  | 257 | +    ) | 
|  | 258 | + | 
|  | 259 | +    # Add chunk GET mappings to storage | 
|  | 260 | +    storage_wm.add_mapping_with_default_placeholders(chunk_1_mapping) | 
|  | 261 | +    storage_wm.add_mapping_with_default_placeholders(chunk_2_mapping) | 
|  | 262 | + | 
|  | 263 | +    # Configure proxy env/args | 
|  | 264 | +    set_proxy_env_vars, clear_proxy_env_vars = proxy_env_vars | 
|  | 265 | +    connect_kwargs = { | 
|  | 266 | +        "user": "testUser", | 
|  | 267 | +        "password": "testPassword", | 
|  | 268 | +        "account": "testAccount", | 
|  | 269 | +        "host": target_wm.wiremock_host, | 
|  | 270 | +        "port": target_wm.wiremock_http_port, | 
|  | 271 | +        "protocol": "http", | 
|  | 272 | +        "warehouse": "TEST_WH", | 
|  | 273 | +    } | 
|  | 274 | + | 
|  | 275 | +    if proxy_method == "explicit_args": | 
|  | 276 | +        connect_kwargs.update( | 
|  | 277 | +            { | 
|  | 278 | +                "proxy_host": proxy_wm.wiremock_host, | 
|  | 279 | +                "proxy_port": str(proxy_wm.wiremock_http_port), | 
|  | 280 | +            } | 
|  | 281 | +        ) | 
|  | 282 | +        clear_proxy_env_vars() | 
|  | 283 | +    else: | 
|  | 284 | +        proxy_url = f"http://{proxy_wm.wiremock_host}:{proxy_wm.wiremock_http_port}" | 
|  | 285 | +        set_proxy_env_vars(proxy_url) | 
|  | 286 | + | 
|  | 287 | +    # Build no_proxy (factory may return CSV string or any iterable) | 
|  | 288 | +    connect_kwargs["no_proxy"] = no_proxy_factory( | 
|  | 289 | +        target_wm.wiremock_http_port, storage_wm.wiremock_http_port | 
|  | 290 | +    ) | 
|  | 291 | + | 
|  | 292 | +    # Connect and perform DB query (will return chunk URLs to storage host) | 
|  | 293 | +    cnx = snowflake.connector.connect(**connect_kwargs) | 
|  | 294 | +    cur = cnx.cursor() | 
|  | 295 | +    cur.execute("SELECT 1") | 
|  | 296 | +    # Consume results to force chunk downloads | 
|  | 297 | +    _ = list(cur) | 
|  | 298 | + | 
|  | 299 | +    # Simulate a storage(S3) GET using the same session manager (to honor connection's proxy settings) | 
|  | 300 | +    cnx._session_manager.get(f"{storage_wm.http_host_with_port}/__admin/health") | 
|  | 301 | + | 
|  | 302 | +    cur.close() | 
|  | 303 | +    cnx.close() | 
|  | 304 | + | 
|  | 305 | +    # Check proxy vs target/storage | 
|  | 306 | +    proxy_reqs = requests.get(f"{proxy_wm.http_host_with_port}/__admin/requests").json() | 
|  | 307 | +    target_reqs = requests.get( | 
|  | 308 | +        f"{target_wm.http_host_with_port}/__admin/requests" | 
|  | 309 | +    ).json() | 
|  | 310 | +    storage_reqs = requests.get( | 
|  | 311 | +        f"{storage_wm.http_host_with_port}/__admin/requests" | 
|  | 312 | +    ).json() | 
|  | 313 | + | 
|  | 314 | +    # DB query expectation | 
|  | 315 | +    proxy_saw_db = any( | 
|  | 316 | +        "/queries/v1/query-request" in r["request"]["url"] | 
|  | 317 | +        for r in proxy_reqs["requests"] | 
|  | 318 | +    ) | 
|  | 319 | +    target_saw_db = any( | 
|  | 320 | +        "/queries/v1/query-request" in r["request"]["url"] | 
|  | 321 | +        for r in target_reqs["requests"] | 
|  | 322 | +    ) | 
|  | 323 | +    assert target_saw_db | 
|  | 324 | +    assert proxy_saw_db == expect_backend_proxy | 
|  | 325 | + | 
|  | 326 | +    # Storage chunk GET expectation | 
|  | 327 | +    proxy_saw_storage = any( | 
|  | 328 | +        "/amazonaws/test/s3testaccount/stage/results/" in r["request"]["url"] | 
|  | 329 | +        for r in proxy_reqs["requests"] | 
|  | 330 | +    ) | 
|  | 331 | +    storage_saw_storage = any( | 
|  | 332 | +        "/amazonaws/test/s3testaccount/stage/results/" in r["request"]["url"] | 
|  | 333 | +        for r in storage_reqs["requests"] | 
|  | 334 | +    ) | 
|  | 335 | +    assert storage_saw_storage | 
|  | 336 | +    assert proxy_saw_storage == expect_storage_proxy | 
|  | 337 | + | 
|  | 338 | +    # No extra CSV branch: connection code normalizes strings/iterables equivalently | 
0 commit comments