Skip to content

Commit f7be268

Browse files
authored
[Test Proxy] Add recording option setting methods (#25460)
1 parent 79a15d7 commit f7be268

File tree

2 files changed

+133
-3
lines changed

2 files changed

+133
-3
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@
3939
add_uri_regex_sanitizer,
4040
add_uri_string_sanitizer,
4141
add_uri_subscription_id_sanitizer,
42+
PemCertificate,
4243
set_bodiless_matcher,
4344
set_custom_default_matcher,
4445
set_default_function_settings,
4546
set_default_session_settings,
47+
set_function_recording_options,
4648
set_headerless_matcher,
49+
set_session_recording_options,
4750
)
4851
from .helpers import ResponseCallback, RetryCounter
4952
from .fake_credentials import FakeTokenCredential
@@ -82,6 +85,7 @@
8285
"KeyVaultPreparer",
8386
"RandomNameResourceGroupPreparer",
8487
"CachedResourceGroupPreparer",
88+
"PemCertificate",
8589
"PowerShellPreparer",
8690
"EnvironmentVariableLoader",
8791
"recorded_by_proxy",
@@ -91,7 +95,9 @@
9195
"set_custom_default_matcher",
9296
"set_default_function_settings",
9397
"set_default_session_settings",
98+
"set_function_recording_options",
9499
"set_headerless_matcher",
100+
"set_session_recording_options",
95101
"start_test_proxy",
96102
"stop_test_proxy",
97103
"variable_recorder",

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

+127-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,24 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
from typing import TYPE_CHECKING
7+
from urllib.error import HTTPError
78
import requests
89

910
from .config import PROXY_URL
1011
from .helpers import get_recording_id, is_live, is_live_and_not_recording
1112

1213
if TYPE_CHECKING:
13-
from typing import Any, Optional
14+
from typing import Any, Iterable, Optional
15+
16+
17+
# This file contains methods for adjusting many aspects of test proxy behavior:
18+
#
19+
# - Sanitizers: record stand-in values to hide secrets and/or enable playback when behavior is inconsistent
20+
# - Transforms: extend test proxy functionality by changing how recordings are processed in playback mode
21+
# - Matchers: modify the conditions that are used to match request and response content with recorded values
22+
# - Recording options: further customization for advanced scenarios, such as providing certificates to the transport
23+
#
24+
# Methods for a given category are grouped together under a header containing more details.
1425

1526

1627
def set_default_function_settings() -> None:
@@ -34,7 +45,7 @@ def set_default_session_settings() -> None:
3445
"""Resets sanitizers, matchers, and transforms for the test proxy to their default settings, for all tests.
3546
3647
This will reset any setting customizations for an entire test session. To reset setting customizations for a single
37-
test -- which is recommended -- use `set_default_session_settings` instead.
48+
test -- which is recommended -- use `set_default_function_settings` instead.
3849
"""
3950

4051
_send_reset_request({})
@@ -393,21 +404,110 @@ def add_storage_request_id_transform() -> None:
393404
_send_transform_request("StorageRequestIdTransform", {})
394405

395406

407+
# ----------RECORDING OPTIONS----------
408+
#
409+
# Recording options enable customization beyond what is offered by sanitizers, matchers, and transforms. These are
410+
# intended for advanced scenarios and are generally not applicable.
411+
#
412+
# -------------------------------------
413+
414+
415+
def set_function_recording_options(**kwargs: "Any") -> None:
416+
"""Sets custom recording options for the current test only.
417+
418+
This must be called during test case execution, rather than at a session, module, or class level. To set recording
419+
options for all tests, use `set_session_recording_options` instead.
420+
421+
:keyword bool handle_redirects: The test proxy does not perform transparent follow directs by default. That means
422+
that if the initial request sent through the test proxy results in a 3XX redirect status, the test proxy will
423+
not follow. It will return that redirect response to the client and allow it to handle the redirect. Setting
424+
`handle_redirects` to True will set the proxy to instead handle redirects itself.
425+
:keyword str context_directory: This changes the "root" path that the test proxy uses when loading a recording.
426+
:keyword certificates: A list of `PemCertificate`s. Any number of certificates is allowed.
427+
:type certificates: Iterable[PemCertificate]
428+
:keyword str tls_certificate: The public key portion of a TLS certificate, as a string. This is used specifically so
429+
that an SSL connection presenting a non-standard certificate can still be validated.
430+
"""
431+
432+
x_recording_id = get_recording_id()
433+
request_args = _get_recording_option_args(**kwargs)
434+
_send_recording_options_request(request_args, {"x-recording-id": x_recording_id})
435+
436+
437+
def set_session_recording_options(**kwargs: "Any") -> None:
438+
"""Sets custom recording options for all tests.
439+
440+
This will set the specified recording options for an entire test session. To set recording options for a single test
441+
-- which is recommended -- use `set_function_recording_options` instead.
442+
443+
:keyword bool handle_redirects: The test proxy does not perform transparent follow directs by default. That means
444+
that if the initial request sent through the test proxy results in a 3XX redirect status, the test proxy will
445+
not follow. It will return that redirect response to the client and allow it to handle the redirect. Setting
446+
`handle_redirects` to True will set the proxy to instead handle redirects itself.
447+
:keyword str context_directory: This changes the "root" path that the test proxy uses when loading a recording.
448+
:keyword certificates: A list of `PemCertificate`s. Any number of certificates is allowed.
449+
:type certificates: Iterable[PemCertificate]
450+
:keyword str tls_certificate: The public key portion of a TLS certificate, as a string. This is used specifically so
451+
that an SSL connection presenting a non-standard certificate can still be validated.
452+
"""
453+
454+
request_args = _get_recording_option_args(**kwargs)
455+
_send_recording_options_request(request_args)
456+
457+
458+
class PemCertificate:
459+
"""Represents a PEM certificate that can be sent to and used by the test proxy.
460+
461+
:param str data: The content of the certificate, as a string.
462+
:param str key: The certificate key, as a string.
463+
"""
464+
465+
def __init__(self, data: str, key: str) -> None:
466+
self.data = data
467+
self.key = key
468+
469+
396470
# ----------HELPERS----------
397471

398472

473+
def _get_recording_option_args(**kwargs: "Any") -> dict:
474+
"""Returns a dictionary of recording option request arguments, formatted for test proxy consumption."""
475+
476+
certificates = kwargs.pop("certificates", None)
477+
tls_certificate = kwargs.pop("tls_certificate", None)
478+
request_args = _get_request_args(**kwargs)
479+
480+
if certificates or tls_certificate:
481+
transport = {}
482+
483+
if certificates:
484+
cert_pairs = [{"PemValue": cert.data, "PemKey": cert.key} for cert in certificates]
485+
transport["Certificates"] = cert_pairs
486+
487+
if tls_certificate:
488+
transport["TLSValidationCert"] = tls_certificate
489+
490+
request_args["Transport"] = transport
491+
492+
return request_args
493+
494+
399495
def _get_request_args(**kwargs: "Any") -> dict:
400-
"""Returns a dictionary of sanitizer constructor headers"""
496+
"""Returns a dictionary of request arguments, formatted for test proxy consumption."""
401497

402498
request_args = {}
403499
if "compare_bodies" in kwargs:
404500
request_args["compareBodies"] = kwargs.get("compare_bodies")
405501
if "condition" in kwargs:
406502
request_args["condition"] = kwargs.get("condition")
503+
if "context_directory" in kwargs:
504+
request_args["ContextDirectory"] = kwargs.get("context_directory")
407505
if "excluded_headers" in kwargs:
408506
request_args["excludedHeaders"] = kwargs.get("excluded_headers")
409507
if "group_for_replace" in kwargs:
410508
request_args["groupForReplace"] = kwargs.get("group_for_replace")
509+
if "handle_redirects" in kwargs:
510+
request_args["HandleRedirects"] = kwargs.get("handle_redirects")
411511
if "headers" in kwargs:
412512
request_args["headersForRemoval"] = kwargs.get("headers")
413513
if "ignored_headers" in kwargs:
@@ -440,6 +540,8 @@ def _send_matcher_request(matcher: str, headers: dict, parameters: "Optional[dic
440540
441541
:param str matcher: The name of the matcher to set.
442542
:param dict headers: Any matcher headers, as a dictionary.
543+
:param parameters: Any matcher constructor parameters, as a dictionary. Defaults to None.
544+
:type parameters: Optional[dict]
443545
"""
444546

445547
if is_live():
@@ -455,6 +557,28 @@ def _send_matcher_request(matcher: str, headers: dict, parameters: "Optional[dic
455557
response.raise_for_status()
456558

457559

560+
def _send_recording_options_request(parameters: dict, headers: "Optional[dict]" = None) -> None:
561+
"""Sends a POST request to the test proxy endpoint to set the specified recording options.
562+
563+
If live tests are being run with recording turned off via the AZURE_SKIP_LIVE_RECORDING environment variable, no
564+
request will be sent.
565+
566+
:param dict parameters: The recording options, as a dictionary.
567+
:param headers: Any recording option request headers, as a dictionary. Defaults to None.
568+
:type headers: Optional[dict]
569+
"""
570+
571+
if is_live_and_not_recording():
572+
return
573+
574+
response = requests.post(
575+
f"{PROXY_URL}/Admin/SetRecordingOptions",
576+
headers=headers,
577+
json=parameters
578+
)
579+
response.raise_for_status()
580+
581+
458582
def _send_reset_request(headers: dict) -> None:
459583
"""Sends a POST request to the test proxy endpoint to reset setting customizations.
460584

0 commit comments

Comments
 (0)