Skip to content

Commit 7e66e38

Browse files
authored
[Test Proxy] Add bodiless matcher, sanitize test SAS tokens (Azure#21823)
1 parent e81c614 commit 7e66e38

File tree

5 files changed

+91
-32
lines changed

5 files changed

+91
-32
lines changed

tools/azure-sdk-tools/devtools_testutils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
add_remove_header_sanitizer,
2929
add_request_subscription_id_sanitizer,
3030
add_uri_regex_sanitizer,
31+
set_bodiless_matcher,
3132
)
3233
from .helpers import ResponseCallback, RetryCounter
3334
from .fake_credential import FakeTokenCredential, ACCOUNT_FAKE_KEY
@@ -61,6 +62,7 @@
6162
"PowerShellPreparer",
6263
"recorded_by_proxy",
6364
"test_proxy",
65+
"set_bodiless_matcher",
6466
"start_test_proxy",
6567
"stop_test_proxy",
6668
"ResponseCallback",

tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
import functools
7-
import logging
87
import os
98
import os.path
109
import six
@@ -14,11 +13,12 @@
1413

1514
from dotenv import load_dotenv, find_dotenv
1615

17-
from azure_devtools.scenario_tests.config import TestConfig
1816
from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function
1917

2018
from . import mgmt_settings_fake as fake_settings
2119
from .azure_testcase import _is_autorest_v3, get_resource_name, get_qualified_method_name
20+
from .helpers import is_live
21+
from .sanitizers import add_general_regex_sanitizer
2222

2323
try:
2424
# Try to import the AsyncFakeCredential, if we cannot assume it is Python 2
@@ -33,11 +33,12 @@
3333
load_dotenv(find_dotenv())
3434

3535

36-
def is_live():
37-
"""A module version of is_live, that could be used in pytest marker."""
38-
if not hasattr(is_live, "_cache"):
39-
is_live._cache = TestConfig().record_mode
40-
return is_live._cache
36+
def _sanitize_token(token, fake_token):
37+
add_general_regex_sanitizer(value=fake_token, regex=token)
38+
url_safe_token = token.replace("/", u"%2F")
39+
add_general_regex_sanitizer(value=fake_token, regex=url_safe_token)
40+
async_token = token.replace(u"%3A", ":")
41+
add_general_regex_sanitizer(value=fake_token, regex=async_token)
4142

4243

4344
class AzureRecordedTestCase(object):
@@ -204,6 +205,7 @@ def generate_sas(self, *args, **kwargs):
204205
token = sas_func(*sas_func_pos_args, **kwargs)
205206

206207
fake_token = self._create_fake_token(token, fake_value)
208+
_sanitize_token(token, fake_token)
207209

208210
if self.is_live:
209211
return token

tools/azure-sdk-tools/devtools_testutils/helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# --------------------------------------------------------------------------
6+
from azure_devtools.scenario_tests.config import TestConfig
7+
8+
9+
def is_live():
10+
"""A module version of is_live, that could be used in pytest marker."""
11+
if not hasattr(is_live, "_cache"):
12+
is_live._cache = TestConfig().record_mode
13+
return is_live._cache
14+
15+
616
class RetryCounter(object):
717
def __init__(self):
818
self.count = 0

tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import logging
88
import requests
99
import six
10+
import sys
1011
from typing import TYPE_CHECKING
12+
import pdb
1113

1214
try:
1315
# py3
@@ -23,13 +25,12 @@
2325

2426
# the trimming function to clean up incoming arguments to the test function we are wrapping
2527
from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function
26-
from .azure_recorded_testcase import is_live
28+
from .helpers import is_live
2729
from .config import PROXY_URL
2830

2931
if TYPE_CHECKING:
3032
from typing import Tuple
3133

32-
3334
# To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at
3435
# https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md
3536

@@ -40,9 +41,15 @@
4041
PLAYBACK_START_URL = "{}/playback/start".format(PROXY_URL)
4142
PLAYBACK_STOP_URL = "{}/playback/stop".format(PROXY_URL)
4243

43-
# TODO, create a pytest scope="session" implementation that can be added to a fixture such that unit tests can
44-
# startup/shutdown the local test proxy
45-
# this should also fire the admin mapping updates, and start/end the session for commiting recording updates
44+
# we store recording IDs in a module-level variable so that sanitizers can access them
45+
# we map test IDs to recording IDs, rather than storing only the current test's recording ID, for parallelization
46+
this = sys.modules[__name__]
47+
this.recording_ids = {}
48+
49+
50+
def get_recording_id():
51+
test_id = get_test_id()
52+
return this.recording_ids.get(test_id)
4653

4754

4855
def get_test_id():
@@ -92,6 +99,8 @@ def start_record_or_playback(test_id):
9299
ValueError("The response body returned from starting playback did not contain valid JSON"), ex
93100
)
94101

102+
# set recording ID in a module-level variable so that sanitizers can access it
103+
this.recording_ids[test_id] = recording_id
95104
return (recording_id, variables)
96105

97106

tools/azure-sdk-tools/devtools_testutils/sanitizers.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,23 @@
77
from typing import TYPE_CHECKING
88

99
from .config import PROXY_URL
10+
from .proxy_testcase import get_recording_id
1011

1112
if TYPE_CHECKING:
1213
from typing import Any, Dict
1314

1415

16+
def set_bodiless_matcher():
17+
# type: () -> None
18+
"""Adjusts the "match" operation to EXCLUDE the body when matching a request to a recording's entries.
19+
20+
This method must be called during test case execution, rather than at a session, module, or class level.
21+
"""
22+
23+
x_recording_id = get_recording_id()
24+
_send_matcher_request("BodilessMatcher", {"x-recording-id": x_recording_id})
25+
26+
1527
def add_body_key_sanitizer(**kwargs):
1628
# type: (**Any) -> None
1729
"""Registers a sanitizer that offers regex update of a specific JTokenPath within a returned body.
@@ -28,7 +40,7 @@ def add_body_key_sanitizer(**kwargs):
2840
"""
2941

3042
request_args = _get_request_args(**kwargs)
31-
_send_request("BodyKeySanitizer", request_args)
43+
_send_sanitizer_request("BodyKeySanitizer", request_args)
3244

3345

3446
def add_body_regex_sanitizer(**kwargs):
@@ -46,7 +58,7 @@ def add_body_regex_sanitizer(**kwargs):
4658
"""
4759

4860
request_args = _get_request_args(**kwargs)
49-
_send_request("BodyRegexSanitizer", request_args)
61+
_send_sanitizer_request("BodyRegexSanitizer", request_args)
5062

5163

5264
def add_continuation_sanitizer(**kwargs):
@@ -65,7 +77,7 @@ def add_continuation_sanitizer(**kwargs):
6577
"""
6678

6779
request_args = _get_request_args(**kwargs)
68-
_send_request("ContinuationSanitizer", request_args)
80+
_send_sanitizer_request("ContinuationSanitizer", request_args)
6981

7082

7183
def add_general_regex_sanitizer(**kwargs):
@@ -82,7 +94,7 @@ def add_general_regex_sanitizer(**kwargs):
8294
"""
8395

8496
request_args = _get_request_args(**kwargs)
85-
_send_request("GeneralRegexSanitizer", request_args)
97+
_send_sanitizer_request("GeneralRegexSanitizer", request_args)
8698

8799

88100
def add_header_regex_sanitizer(**kwargs):
@@ -102,14 +114,14 @@ def add_header_regex_sanitizer(**kwargs):
102114
"""
103115

104116
request_args = _get_request_args(**kwargs)
105-
_send_request("HeaderRegexSanitizer", request_args)
117+
_send_sanitizer_request("HeaderRegexSanitizer", request_args)
106118

107119

108120
def add_oauth_response_sanitizer():
109121
# type: () -> None
110122
"""Registers a sanitizer that cleans out all request/response pairs that match an oauth regex in their URI."""
111123

112-
_send_request("OAuthResponseSanitizer", {})
124+
_send_sanitizer_request("OAuthResponseSanitizer", {})
113125

114126

115127
def add_remove_header_sanitizer(**kwargs):
@@ -121,7 +133,7 @@ def add_remove_header_sanitizer(**kwargs):
121133
"""
122134

123135
request_args = _get_request_args(**kwargs)
124-
_send_request("RemoveHeaderSanitizer", request_args)
136+
_send_sanitizer_request("RemoveHeaderSanitizer", request_args)
125137

126138

127139
def add_request_subscription_id_sanitizer(**kwargs):
@@ -134,7 +146,7 @@ def add_request_subscription_id_sanitizer(**kwargs):
134146
"""
135147

136148
request_args = _get_request_args(**kwargs)
137-
_send_request("ReplaceRequestSubscriptionId", request_args)
149+
_send_sanitizer_request("ReplaceRequestSubscriptionId", request_args)
138150

139151

140152
def add_uri_regex_sanitizer(**kwargs):
@@ -149,35 +161,59 @@ def add_uri_regex_sanitizer(**kwargs):
149161
"""
150162

151163
request_args = _get_request_args(**kwargs)
152-
_send_request("UriRegexSanitizer", request_args)
164+
_send_sanitizer_request("UriRegexSanitizer", request_args)
153165

154166

155167
def _get_request_args(**kwargs):
156168
# type: (**Any) -> Dict
157169
"""Returns a dictionary of sanitizer constructor headers"""
158170

159171
request_args = {}
160-
request_args["groupForReplace"] = kwargs.get("group_for_replace")
161-
request_args["headersForRemoval"] = kwargs.get("headers")
162-
request_args["jsonPath"] = kwargs.get("json_path")
163-
request_args["key"] = kwargs.get("key")
164-
request_args["method"] = kwargs.get("method")
165-
request_args["regex"] = kwargs.get("regex")
166-
request_args["resetAfterFirst"] = kwargs.get("reset_after_first")
167-
request_args["value"] = kwargs.get("value")
172+
if "group_for_replace" in kwargs:
173+
request_args["groupForReplace"] = kwargs.get("group_for_replace")
174+
if "headers" in kwargs:
175+
request_args["headersForRemoval"] = kwargs.get("headers")
176+
if "json_path" in kwargs:
177+
request_args["jsonPath"] = kwargs.get("json_path")
178+
if "key" in kwargs:
179+
request_args["key"] = kwargs.get("key")
180+
if "method" in kwargs:
181+
request_args["method"] = kwargs.get("method")
182+
if "regex" in kwargs:
183+
request_args["regex"] = kwargs.get("regex")
184+
if "reset_after_first" in kwargs:
185+
request_args["resetAfterFirst"] = kwargs.get("reset_after_first")
186+
if "value" in kwargs:
187+
request_args["value"] = kwargs.get("value")
168188
return request_args
169189

170190

171-
def _send_request(sanitizer, parameters):
191+
def _send_matcher_request(matcher, headers):
192+
# type: (str, Dict) -> None
193+
"""Sends a POST request to the test proxy endpoint to register the specified matcher.
194+
195+
:param str matcher: The name of the matcher to set.
196+
:param dict headers: Any matcher headers, as a dictionary.
197+
"""
198+
199+
headers_to_send = {"x-abstraction-identifier": matcher}
200+
headers_to_send.update(headers)
201+
requests.post(
202+
"{}/Admin/SetMatcher".format(PROXY_URL),
203+
headers=headers_to_send,
204+
)
205+
206+
207+
def _send_sanitizer_request(sanitizer, parameters):
172208
# type: (str, Dict) -> None
173-
"""Send a POST request to the test proxy endpoint to register the specified sanitizer.
209+
"""Sends a POST request to the test proxy endpoint to register the specified sanitizer.
174210
175211
:param str sanitizer: The name of the sanitizer to add.
176212
:param dict parameters: The sanitizer constructor parameters, as a dictionary.
177213
"""
178214

179215
requests.post(
180216
"{}/Admin/AddSanitizer".format(PROXY_URL),
181-
headers={"x-abstraction-identifier": sanitizer},
217+
headers={"x-abstraction-identifier": sanitizer, "Content-Type": "application/json"},
182218
json=parameters
183219
)

0 commit comments

Comments
 (0)