Skip to content

Commit 8a2b8ba

Browse files
SNOW-921045 Add wiremock tests support (#2170)
Co-authored-by: Richard Ebeling <[email protected]>
1 parent a254964 commit 8a2b8ba

File tree

9 files changed

+253
-0
lines changed

9 files changed

+253
-0
lines changed

.github/workflows/build_test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ jobs:
138138
python-version: ${{ matrix.python-version }}
139139
- name: Display Python version
140140
run: python -c "import sys; print(sys.version)"
141+
- name: Set up Java
142+
uses: actions/setup-java@v4 # for wiremock
143+
with:
144+
java-version: 11
145+
distribution: 'temurin'
146+
java-package: 'jre'
147+
- name: Fetch Wiremock
148+
shell: bash
149+
run: curl https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wiremock-standalone-3.11.0.jar --output .wiremock/wiremock-standalone.jar
141150
- name: Setup parameters file
142151
shell: bash
143152
env:

.wiremock/ca-cert.jks

2.29 KB
Binary file not shown.

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1515
- Fixed a bug where privatelink OCSP Cache url could not be determined if privatelink account name was specified in uppercase.
1616
- Added support for iceberg tables to `write_pandas`.
1717
- Fixed base64 encoded private key tests.
18+
- Added Wiremock tests.
1819

1920
- v3.13.2(January 29, 2025)
2021
- Changed not to use scoped temporary objects.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ exclude license_header.txt
1919
exclude tox.ini
2020
exclude mypy.ini
2121
exclude .clang-format
22+
exclude .wiremock/*
2223

2324
prune ci
2425
prune benchmark

ci/docker/connector_test_fips/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo && \
1919
RUN yum clean all && \
2020
yum install -y redhat-rpm-config gcc libffi-devel openssl openssl-devel && \
2121
yum install -y python38 python38-devel && \
22+
yum install -y java-11-openjdk && \
2223
yum clean all && \
2324
rm -rf /var/cache/yum
2425
RUN python3 -m pip install --user --upgrade pip setuptools wheel

ci/test_fips.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
88
CONNECTOR_DIR="$( dirname "${THIS_DIR}")"
99
CONNECTOR_WHL="$(ls $CONNECTOR_DIR/dist/*cp38*manylinux2014*.whl | sort -r | head -n 1)"
1010

11+
# fetch wiremock
12+
curl https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.11.0/wiremock-standalone-3.11.0.jar --output "${CONNECTOR_DIR}/.wiremock/wiremock-standalone.jar"
13+
1114
python3.8 -m venv fips_env
1215
source fips_env/bin/activate
1316
pip install -U setuptools pip

test/unit/test_wiremock_client.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
from typing import Any, Generator
6+
7+
import pytest
8+
9+
# old driver support
10+
try:
11+
from snowflake.connector.vendored import requests
12+
from src.snowflake.connector.test_util import RUNNING_ON_JENKINS
13+
except ImportError:
14+
import os
15+
16+
import requests
17+
18+
RUNNING_ON_JENKINS = os.getenv("JENKINS_HOME") is not None
19+
20+
21+
from ..wiremock.wiremock_utils import WiremockClient
22+
23+
24+
@pytest.fixture(scope="session")
25+
def wiremock_client() -> Generator[WiremockClient, Any, None]:
26+
with WiremockClient() as client:
27+
yield client
28+
29+
30+
@pytest.mark.skipif(RUNNING_ON_JENKINS, reason="jenkins doesn't support wiremock tests")
31+
def test_wiremock(wiremock_client):
32+
connection_reset_by_peer_mapping = {
33+
"mappings": [
34+
{
35+
"scenarioName": "Basic example",
36+
"requiredScenarioState": "Started",
37+
"request": {"method": "GET", "url": "/endpoint"},
38+
"response": {"status": 200},
39+
}
40+
],
41+
"importOptions": {"duplicatePolicy": "IGNORE", "deleteAllNotInImport": True},
42+
}
43+
wiremock_client.import_mapping(connection_reset_by_peer_mapping)
44+
45+
response = requests.get(
46+
f"http://{wiremock_client.wiremock_host}:{wiremock_client.wiremock_http_port}/endpoint"
47+
)
48+
49+
assert response is not None, "response is None"
50+
assert (
51+
response.status_code == requests.codes.ok
52+
), f"response status is not 200, received status {response.status_code}"

test/wiremock/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#

test/wiremock/wiremock_utils.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#
2+
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3+
#
4+
5+
import json
6+
import logging
7+
import pathlib
8+
import socket
9+
import subprocess
10+
from time import sleep
11+
from typing import List, Optional, Union
12+
13+
try:
14+
from snowflake.connector.vendored import requests
15+
except ImportError:
16+
import requests
17+
18+
WIREMOCK_START_MAX_RETRY_COUNT = 12
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def _get_mapping_str(mapping: Union[str, dict, pathlib.Path]) -> str:
23+
if isinstance(mapping, str):
24+
return mapping
25+
if isinstance(mapping, dict):
26+
return json.dumps(mapping)
27+
if isinstance(mapping, pathlib.Path):
28+
if mapping.is_file():
29+
with open(mapping) as f:
30+
return f.read()
31+
else:
32+
raise RuntimeError(f"File with mapping: {mapping} does not exist")
33+
34+
raise RuntimeError(f"Mapping {mapping} is of an invalid type")
35+
36+
37+
class WiremockClient:
38+
def __init__(self):
39+
self.wiremock_filename = "wiremock-standalone.jar"
40+
self.wiremock_host = "localhost"
41+
self.wiremock_http_port = None
42+
self.wiremock_https_port = None
43+
44+
self.wiremock_dir = pathlib.Path(__file__).parent.parent.parent / ".wiremock"
45+
assert self.wiremock_dir.exists(), f"{self.wiremock_dir} does not exist"
46+
47+
self.wiremock_jar_path = self.wiremock_dir / self.wiremock_filename
48+
assert (
49+
self.wiremock_jar_path.exists()
50+
), f"{self.wiremock_jar_path} does not exist"
51+
52+
def _start_wiremock(self):
53+
self.wiremock_http_port = self._find_free_port()
54+
self.wiremock_https_port = self._find_free_port(
55+
forbidden_ports=[self.wiremock_http_port]
56+
)
57+
self.wiremock_process = subprocess.Popen(
58+
[
59+
"java",
60+
"-jar",
61+
self.wiremock_jar_path,
62+
"--root-dir",
63+
self.wiremock_dir,
64+
"--enable-browser-proxying", # work as forward proxy
65+
"--proxy-pass-through",
66+
"false", # pass through only matched requests
67+
"--port",
68+
str(self.wiremock_http_port),
69+
"--https-port",
70+
str(self.wiremock_https_port),
71+
"--https-keystore",
72+
self.wiremock_dir / "ca-cert.jks",
73+
"--ca-keystore",
74+
self.wiremock_dir / "ca-cert.jks",
75+
]
76+
)
77+
self._wait_for_wiremock()
78+
79+
def _stop_wiremock(self):
80+
response = self._wiremock_post(
81+
f"http://{self.wiremock_host}:{self.wiremock_http_port}/__admin/shutdown"
82+
)
83+
if response.status_code != 200:
84+
logger.info("Wiremock shutdown failed, the process will be killed")
85+
self.wiremock_process.kill()
86+
else:
87+
logger.debug("Wiremock shutdown gracefully")
88+
89+
def _wait_for_wiremock(self):
90+
retry_count = 0
91+
while retry_count < WIREMOCK_START_MAX_RETRY_COUNT:
92+
if self._health_check():
93+
return
94+
retry_count += 1
95+
sleep(1)
96+
97+
raise TimeoutError(
98+
f"WiremockClient did not respond within {WIREMOCK_START_MAX_RETRY_COUNT} seconds"
99+
)
100+
101+
def _health_check(self):
102+
mappings_endpoint = (
103+
f"http://{self.wiremock_host}:{self.wiremock_http_port}/__admin/health"
104+
)
105+
try:
106+
response = requests.get(mappings_endpoint)
107+
except requests.exceptions.RequestException as e:
108+
logger.warning(f"Wiremock healthcheck failed with exception: {e}")
109+
return False
110+
111+
if (
112+
response.status_code == requests.codes.ok
113+
and response.json()["status"] != "healthy"
114+
):
115+
logger.warning(f"Wiremock healthcheck failed with response: {response}")
116+
return False
117+
elif response.status_code != requests.codes.ok:
118+
logger.warning(
119+
f"Wiremock healthcheck failed with status code: {response.status_code}"
120+
)
121+
return False
122+
123+
return True
124+
125+
def _reset_wiremock(self):
126+
reset_endpoint = (
127+
f"http://{self.wiremock_host}:{self.wiremock_http_port}/__admin/reset"
128+
)
129+
response = self._wiremock_post(reset_endpoint)
130+
if response.status_code != requests.codes.ok:
131+
raise RuntimeError("Failed to reset WiremockClient")
132+
133+
def _wiremock_post(
134+
self, endpoint: str, body: Optional[str] = None
135+
) -> requests.Response:
136+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
137+
return requests.post(endpoint, data=body, headers=headers)
138+
139+
def import_mapping(self, mapping: Union[str, dict, pathlib.Path]):
140+
self._reset_wiremock()
141+
import_mapping_endpoint = f"http://{self.wiremock_host}:{self.wiremock_http_port}/__admin/mappings/import"
142+
mapping_str = _get_mapping_str(mapping)
143+
response = self._wiremock_post(import_mapping_endpoint, mapping_str)
144+
if response.status_code != requests.codes.ok:
145+
raise RuntimeError("Failed to import mapping")
146+
147+
def add_mapping(self, mapping: Union[str, dict, pathlib.Path]):
148+
add_mapping_endpoint = (
149+
f"http://{self.wiremock_host}:{self.wiremock_http_port}/__admin/mappings"
150+
)
151+
mapping_str = _get_mapping_str(mapping)
152+
response = self._wiremock_post(add_mapping_endpoint, mapping_str)
153+
if response.status_code != requests.codes.created:
154+
raise RuntimeError("Failed to add mapping")
155+
156+
def _find_free_port(self, forbidden_ports: Union[List[int], None] = None) -> int:
157+
max_retries = 1 if forbidden_ports is None else 3
158+
if forbidden_ports is None:
159+
forbidden_ports = []
160+
161+
retry_count = 0
162+
while retry_count < max_retries:
163+
retry_count += 1
164+
with socket.socket() as sock:
165+
sock.bind((self.wiremock_host, 0))
166+
port = sock.getsockname()[1]
167+
if port not in forbidden_ports:
168+
return port
169+
170+
raise RuntimeError(
171+
f"Unable to find a free port for wiremock in {max_retries} attempts"
172+
)
173+
174+
def __enter__(self):
175+
self._start_wiremock()
176+
logger.debug(
177+
f"Starting wiremock process, listening on {self.wiremock_host}:{self.wiremock_http_port}"
178+
)
179+
return self
180+
181+
def __exit__(self, exc_type, exc_val, exc_tb):
182+
logger.debug("Stopping wiremock process")
183+
self._stop_wiremock()

0 commit comments

Comments
 (0)