Skip to content

Commit 2aea6b1

Browse files
SNOW-762783: cleaned up
1 parent 5ee8aec commit 2aea6b1

File tree

7 files changed

+364
-279
lines changed

7 files changed

+364
-279
lines changed

test/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from logging import getLogger
77
from pathlib import Path
88
from test.test_utils.cross_module_fixtures.http_fixtures import * # NOQA
9+
from test.test_utils.cross_module_fixtures.mitm_fixtures import * # NOQA
910
from test.test_utils.cross_module_fixtures.wiremock_fixtures import * # NOQA
1011
from typing import Generator
1112

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
#!/usr/bin/env python
2-
"""Integration test for SNOW-2865839 regression with real Snowflake connection.
3-
4-
These tests verify that proxy settings (including NO_PROXY) work correctly.
2+
"""Integration test for proxy with real Snowflake connection.
53
64
Requirements:
75
mitmproxy is installed automatically via the [development] extras in setup.cfg
86
9-
The mitm_proxy fixture (session-scoped):
10-
- Starts a transparent HTTPS proxy once for all tests
11-
- Returns proxy information (does NOT auto-configure)
12-
- Tests control proxy usage via:
13-
1. mitm_proxy.set_env_vars(monkeypatch) - for environment variables
14-
2. conn_cnx(proxy_host=..., proxy_port=...) - for connection params
15-
167
Important:
178
When connecting through mitmproxy, you MUST set disable_ocsp_checks=True
189
because mitmproxy performs MITM with self-signed certificates that cannot
@@ -27,16 +18,9 @@
2718
except ImportError:
2819
from ..randomize import random_string
2920

30-
pytestmark = pytest.mark.aws
31-
3221

3322
@pytest.mark.skipolddriver
34-
def test_put_with_https_proxy_baseline(conn_cnx, tmp_path, mitm_proxy, monkeypatch):
35-
"""Step 1: PUT through mitmproxy (no NO_PROXY yet).
36-
37-
Establishes that transparent HTTPS proxy works with real Snowflake.
38-
This proves traffic flows through the proxy for all endpoints.
39-
"""
23+
def test_put_with_https_proxy(conn_cnx, tmp_path, mitm_proxy, monkeypatch):
4024
test_file = tmp_path / "test_data.csv"
4125
test_file.write_text("col1,col2\n1,2\n3,4\n")
4226

@@ -47,7 +31,7 @@ def test_put_with_https_proxy_baseline(conn_cnx, tmp_path, mitm_proxy, monkeypat
4731
# Must disable OCSP checks since mitmproxy presents self-signed certs for MITM
4832
with conn_cnx(disable_ocsp_checks=True) as conn:
4933
with conn.cursor() as cur:
50-
stage_name = random_string(5, "test_proxy_baseline_")
34+
stage_name = random_string(5, "test_proxy_")
5135
cur.execute(f"CREATE TEMPORARY STAGE {stage_name}")
5236

5337
put_result = cur.execute(
Lines changed: 0 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
import os
2-
import socket
3-
import subprocess
4-
import tempfile
5-
import time
6-
from pathlib import Path
7-
from typing import NamedTuple
82

93
import pytest
104

@@ -48,257 +42,3 @@ def clear_proxy_env_vars():
4842
os.environ["NO_PROXY"] = original_no_proxy
4943
elif "NO_PROXY" in os.environ:
5044
del os.environ["NO_PROXY"]
51-
52-
53-
class MitmProxyInfo(NamedTuple):
54-
"""Information about a running mitmproxy instance.
55-
56-
Use this to configure your connection or environment variables as needed.
57-
"""
58-
59-
host: str
60-
port: int
61-
ca_cert_path: Path
62-
proxy_url: str
63-
64-
def set_env_vars(self, monkeypatch):
65-
"""Helper to set proxy environment variables.
66-
67-
Args:
68-
monkeypatch: pytest monkeypatch fixture
69-
"""
70-
monkeypatch.setenv("HTTP_PROXY", self.proxy_url)
71-
monkeypatch.setenv("HTTPS_PROXY", self.proxy_url)
72-
monkeypatch.setenv("REQUESTS_CA_BUNDLE", str(self.ca_cert_path))
73-
monkeypatch.setenv("CURL_CA_BUNDLE", str(self.ca_cert_path))
74-
monkeypatch.setenv("SSL_CERT_FILE", str(self.ca_cert_path))
75-
76-
77-
@pytest.fixture(scope="session")
78-
def mitm_proxy():
79-
"""Start mitmproxy for transparent HTTPS proxying in tests.
80-
81-
This fixture (session-scoped):
82-
- Starts mitmdump once for all tests
83-
- Waits for CA certificate generation
84-
- Returns proxy information (does NOT set env vars automatically)
85-
- Cleans up after all tests complete
86-
87-
The fixture does NOT automatically configure proxy settings.
88-
Tests should explicitly use the proxy via:
89-
1. Environment variables: mitm_proxy.set_env_vars(monkeypatch)
90-
2. Connection parameters: conn_cnx(proxy_host=..., proxy_port=...)
91-
92-
Yields:
93-
MitmProxyInfo: Information about the running proxy instance
94-
"""
95-
print("\n[MITM] Starting mitmproxy fixture setup...")
96-
97-
# Check if mitmproxy is available
98-
print("[MITM] Checking if mitmdump is installed...")
99-
try:
100-
subprocess.run(
101-
["mitmdump", "--version"],
102-
capture_output=True,
103-
check=True,
104-
timeout=5,
105-
)
106-
print("[MITM] mitmdump is installed and available")
107-
except (
108-
subprocess.CalledProcessError,
109-
FileNotFoundError,
110-
subprocess.TimeoutExpired,
111-
) as e:
112-
print(f"[MITM] mitmdump check failed: {e}")
113-
pytest.fail(
114-
"mitmproxy (mitmdump) is not installed. Install with: pip install mitmproxy"
115-
)
116-
117-
proxy_host = "127.0.0.1"
118-
119-
# Create a temporary addon script to capture the assigned port
120-
# This is the recommended approach per mitmproxy maintainers:
121-
# https://github.com/mitmproxy/mitmproxy/discussions/6011
122-
port_file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt")
123-
port_file_path = port_file.name
124-
port_file.close()
125-
126-
addon_script = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".py")
127-
addon_script.write(
128-
f'''
129-
from mitmproxy import ctx
130-
131-
def running():
132-
"""Called when mitmproxy is fully started and ready."""
133-
# Get the actual port that was bound (when using --listen-port 0)
134-
# ctx.master.addons.get("proxyserver").listen_addrs() returns:
135-
# [('::', port, 0, 0), ('0.0.0.0', port)]
136-
addrs = ctx.master.addons.get("proxyserver").listen_addrs()
137-
if addrs:
138-
port = addrs[0][1]
139-
with open(r"{port_file_path}", "w") as f:
140-
f.write(str(port))
141-
ctx.log.info(f"Proxy listening on port {{port}}")
142-
'''
143-
)
144-
addon_script.close()
145-
addon_script_path = addon_script.name
146-
print(f"[MITM] Created port detection addon: {addon_script_path}")
147-
print(f"[MITM] Port will be written to: {port_file_path}")
148-
149-
# Start mitmdump with port 0 (let OS assign a free port) - thread-safe!
150-
print(f"[MITM] Starting mitmdump process on {proxy_host}:0 (auto-assign port)...")
151-
mitm_process = subprocess.Popen(
152-
[
153-
"mitmdump",
154-
"--listen-host",
155-
proxy_host,
156-
"--listen-port",
157-
"0", # OS will assign a free port
158-
"--set",
159-
"connection_strategy=lazy", # Don't connect to upstream unless needed
160-
"--set",
161-
"stream_large_bodies=1m", # Stream large bodies
162-
"-s",
163-
addon_script_path, # Load our addon to capture the port
164-
],
165-
stdout=subprocess.PIPE,
166-
stderr=subprocess.PIPE,
167-
text=True,
168-
)
169-
print(f"[MITM] mitmdump process started with PID {mitm_process.pid}")
170-
171-
# Wait for mitmproxy to generate CA certificate
172-
ca_cert_path = Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.pem"
173-
print(f"[MITM] Waiting for CA certificate at: {ca_cert_path}")
174-
max_wait_seconds = 30
175-
start_time = time.time()
176-
177-
cert_wait_count = 0
178-
while not ca_cert_path.exists():
179-
cert_wait_count += 1
180-
elapsed = time.time() - start_time
181-
182-
if cert_wait_count % 4 == 0: # Print every 2 seconds
183-
print(f"[MITM] Still waiting for CA cert... ({elapsed:.1f}s elapsed)")
184-
185-
if elapsed > max_wait_seconds:
186-
print(
187-
f"[MITM] Timeout waiting for CA certificate after {max_wait_seconds}s"
188-
)
189-
mitm_process.kill()
190-
pytest.fail(
191-
f"mitmproxy CA certificate not generated after {max_wait_seconds}s"
192-
)
193-
194-
if mitm_process.poll() is not None:
195-
# Process died
196-
print("[MITM] ERROR: mitmdump process died during CA cert generation")
197-
stdout, stderr = mitm_process.communicate()
198-
pytest.fail(
199-
f"mitmproxy process died during startup.\nStdout: {stdout}\nStderr: {stderr}"
200-
)
201-
202-
time.sleep(0.5)
203-
204-
print(f"[MITM] CA certificate found at {ca_cert_path}")
205-
206-
# Wait for the addon to write the port to the file
207-
print("[MITM] Waiting for addon to write port to file...")
208-
proxy_port = None
209-
max_port_wait = 10
210-
port_start_time = time.time()
211-
212-
while proxy_port is None:
213-
elapsed = time.time() - port_start_time
214-
215-
if elapsed > max_port_wait:
216-
print(f"[MITM] Timeout waiting for port file after {max_port_wait}s")
217-
print(f"[MITM] Port file path: {port_file_path}")
218-
print(f"[MITM] Port file exists: {Path(port_file_path).exists()}")
219-
mitm_process.kill()
220-
# Cleanup temp files
221-
try:
222-
os.unlink(port_file_path)
223-
os.unlink(addon_script_path)
224-
except OSError:
225-
pass
226-
pytest.fail("Could not determine mitmproxy port from addon")
227-
228-
if mitm_process.poll() is not None:
229-
print("[MITM] ERROR: mitmdump process died during port detection")
230-
stdout, stderr = mitm_process.communicate()
231-
# Cleanup temp files
232-
try:
233-
os.unlink(port_file_path)
234-
os.unlink(addon_script_path)
235-
except OSError:
236-
pass
237-
pytest.fail(
238-
f"mitmproxy died before port detection.\nStdout: {stdout}\nStderr: {stderr}"
239-
)
240-
241-
# Check if port file has been written
242-
if Path(port_file_path).exists():
243-
try:
244-
with open(port_file_path) as f:
245-
port_str = f.read().strip()
246-
if port_str:
247-
proxy_port = int(port_str)
248-
print(f"[MITM] Port detected from file: {proxy_port}")
249-
break
250-
except (ValueError, OSError) as e:
251-
print(f"[MITM] Error reading port file: {e}")
252-
253-
time.sleep(0.1)
254-
255-
print(f"[MITM] Successfully detected port {proxy_port}")
256-
257-
# Cleanup temp files
258-
try:
259-
os.unlink(port_file_path)
260-
os.unlink(addon_script_path)
261-
print("[MITM] Cleaned up temporary addon files")
262-
except Exception as e:
263-
print(f"[MITM] Warning: Could not cleanup temp files: {e}")
264-
265-
proxy_url = f"http://{proxy_host}:{proxy_port}"
266-
print(f"[MITM] Proxy URL: {proxy_url}")
267-
268-
# Verify proxy is listening
269-
print(
270-
f"[MITM] Verifying proxy is accepting connections on {proxy_host}:{proxy_port}..."
271-
)
272-
try:
273-
with socket.create_connection((proxy_host, proxy_port), timeout=5):
274-
pass
275-
print("[MITM] Proxy is accepting connections!")
276-
except (socket.timeout, ConnectionRefusedError) as e:
277-
print(f"[MITM] ERROR: Proxy not accepting connections: {e}")
278-
mitm_process.kill()
279-
pytest.fail(f"mitmproxy not accepting connections: {e}")
280-
281-
proxy_info = MitmProxyInfo(
282-
host=proxy_host,
283-
port=proxy_port,
284-
ca_cert_path=ca_cert_path,
285-
proxy_url=proxy_url,
286-
)
287-
288-
print(f"[MITM] Setup complete! Proxy ready at {proxy_url}")
289-
print(f"[MITM] CA cert: {ca_cert_path}")
290-
291-
try:
292-
yield proxy_info
293-
finally:
294-
# Cleanup: stop mitmproxy
295-
print("[MITM] Cleaning up: stopping mitmproxy...")
296-
mitm_process.terminate()
297-
try:
298-
mitm_process.wait(timeout=5)
299-
print("[MITM] mitmproxy stopped gracefully")
300-
except subprocess.TimeoutExpired:
301-
print("[MITM] mitmproxy didn't stop gracefully, killing...")
302-
mitm_process.kill()
303-
mitm_process.wait()
304-
print("[MITM] mitmproxy killed")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Mitmproxy fixtures for testing."""
2+
3+
from typing import Any, Generator, Union
4+
5+
import pytest
6+
7+
from ..mitm import MitmClient
8+
9+
10+
@pytest.fixture(scope="session")
11+
def mitm_proxy() -> Generator[Union[MitmClient, Any], Any, None]:
12+
"""Start mitmproxy for transparent HTTPS proxying in tests.
13+
14+
This fixture (session-scoped):
15+
- Starts mitmdump once for all tests
16+
- Waits for CA certificate generation
17+
- Returns MitmClient instance
18+
- Cleans up after all tests complete
19+
20+
The fixture does NOT automatically configure proxy settings.
21+
Tests should explicitly use the proxy via:
22+
1. Environment variables: mitm_proxy.set_env_vars(monkeypatch)
23+
2. Connection parameters: conn_cnx(proxy_host=mitm_proxy.host, ...)
24+
25+
Yields:
26+
MitmClient: Running mitmproxy client instance
27+
28+
Fails:
29+
When RuntimeError: If mitmproxy is not installed or fails to start
30+
"""
31+
try:
32+
with MitmClient() as client:
33+
yield client
34+
except (RuntimeError, TimeoutError) as e:
35+
pytest.fail(f"Failed to start mitmproxy: {e}")

test/test_utils/mitm/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""mitmproxy test utilities."""
2+
3+
from .mitm_client import MitmClient
4+
5+
__all__ = ["MitmClient"]

0 commit comments

Comments
 (0)