From 7d6b3a9e45e507dd4a0329e4b7fa4406bfaafbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Fri, 6 Aug 2021 16:58:48 -0700 Subject: [PATCH 01/12] Add RecordedByProxy, AzureRecordedTestCase --- .../azure_recorded_testcase.py | 260 ++++++++++++++++++ .../devtools_testutils/proxy_testcase.py | 144 ++++++++++ 2 files changed, 404 insertions(+) create mode 100644 tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py create mode 100644 tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py new file mode 100644 index 000000000000..e15321dadefe --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -0,0 +1,260 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import functools +import logging +import os +import os.path +import sys +import time + +from dotenv import load_dotenv, find_dotenv + +from azure_devtools.scenario_tests import AzureTestError +from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function + +from .config import TEST_SETTING_FILENAME +from . import mgmt_settings_fake as fake_settings +from .azure_testcase import is_live, _is_autorest_v3, get_resource_name, get_qualified_method_name + +try: + # Try to import the AsyncFakeCredential, if we cannot assume it is Python 2 + from .fake_async_credential import AsyncFakeCredential +except SyntaxError: + pass + + +load_dotenv(find_dotenv()) + +class AzureRecordedTestCase(object): + + @property + def settings(self): + if self.is_live: + if self._real_settings: + return self._real_settings + else: + raise AzureTestError( + "Need a mgmt_settings_real.py file to run tests live." + ) + else: + return self._fake_settings + + def _load_settings(self): + try: + from . import mgmt_settings_real as real_settings + + return fake_settings, real_settings + except ImportError: + return fake_settings, None + + @property + def is_live(self): + return is_live() + + @property + def qualified_test_name(self): + return get_qualified_method_name(self, "method_name") + + @property + def in_recording(self): + return os.getenv("AZURE_RECORD_MODE") == "record" + + # TODO: This needs to be removed, recording processors are handled on the proxy side, but + # this is needed for the preparers + @property + def recording_processors(self): + return [] + + def is_playback(self): + return not self.is_live + + def get_settings_value(self, key): + key_value = os.environ.get("AZURE_" + key, None) + + if ( + key_value + and self._real_settings + and getattr(self._real_settings, key) != key_value + ): + raise ValueError( + "You have both AZURE_{key} env variable and mgmt_settings_real.py for {key} to different values".format( + key=key + ) + ) + + if not key_value: + try: + key_value = getattr(self.settings, key) + except Exception: + print("Could not get {}".format(key)) + raise + return key_value + + def get_credential(self, client_class, **kwargs): + + tenant_id = os.environ.get( + "AZURE_TENANT_ID", getattr(self._real_settings, "TENANT_ID", None) + ) + client_id = os.environ.get( + "AZURE_CLIENT_ID", getattr(self._real_settings, "CLIENT_ID", None) + ) + secret = os.environ.get( + "AZURE_CLIENT_SECRET", getattr(self._real_settings, "CLIENT_SECRET", None) + ) + is_async = kwargs.pop("is_async", False) + + if tenant_id and client_id and secret and self.is_live: + if _is_autorest_v3(client_class): + # Create azure-identity class + from azure.identity import ClientSecretCredential + + if is_async: + from azure.identity.aio import ClientSecretCredential + return ClientSecretCredential( + tenant_id=tenant_id, client_id=client_id, client_secret=secret + ) + else: + # Create msrestazure class + from msrestazure.azure_active_directory import ( + ServicePrincipalCredentials, + ) + + return ServicePrincipalCredentials( + tenant=tenant_id, client_id=client_id, secret=secret + ) + else: + if _is_autorest_v3(client_class): + if is_async: + if self.is_live: + raise ValueError( + "Async live doesn't support mgmt_setting_real, please set AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET" + ) + return AsyncFakeCredential() + else: + return self.settings.get_azure_core_credentials() + else: + return self.settings.get_credentials() + + def create_client_from_credential(self, client_class, credential, **kwargs): + + # Real client creation + # TODO decide what is the final argument for that + # if self.is_playback(): + # kwargs.setdefault("polling_interval", 0) + if _is_autorest_v3(client_class): + kwargs.setdefault("logging_enable", True) + client = client_class(credential=credential, **kwargs) + else: + client = client_class(credentials=credential, **kwargs) + + if self.is_playback(): + try: + client._config.polling_interval = ( + 0 # FIXME in azure-mgmt-core, make this a kwargs + ) + except AttributeError: + pass + + if hasattr(client, "config"): # Autorest v2 + if self.is_playback(): + client.config.long_running_operation_timeout = 0 + client.config.enable_http_logger = True + return client + + def create_basic_client(self, client_class, **kwargs): + """ DO NOT USE ME ANYMORE.""" + logger = logging.getLogger() + logger.warning( + "'create_basic_client' will be deprecated in the future. It is recommended that you use \ + 'get_credential' and 'create_client_from_credential' to create your client." + ) + + credentials = self.get_credential(client_class) + return self.create_client_from_credential(client_class, credentials, **kwargs) + + def create_random_name(self, name): + unique_test_name = os.getenv("PYTEST_CURRENT_TEST").encode("utf-8") + print(unique_test_name) + return get_resource_name(name, unique_test_name) + + def get_resource_name(self, name): + """Alias to create_random_name for back compatibility.""" + return self.create_random_name(name) + + def get_replayable_random_resource_name(self, name): + """In a replay scenario, (is not live) gives the static moniker. In the random scenario, gives generated name.""" + if self.is_live: + created_name = self.create_random_name(name) + self.scrubber.register_name_pair(created_name, name) + return name + + def get_preparer_resource_name(self, prefix): + """Random name generation for use by preparers. + + If prefix is a blank string, use the fully qualified test name instead. + This is what legacy tests do for resource groups.""" + return self.get_resource_name( + prefix #or self.qualified_test_name.replace(".", "_") + ) + + @staticmethod + def await_prepared_test(test_fn): + """Synchronous wrapper for async test methods. Used to avoid making changes + upstream to AbstractPreparer, which only awaits async tests that use preparers. + (Add @AzureTestCase.await_prepared_test decorator to async tests without preparers) + + # Note: this will only be needed so long as we maintain unittest.TestCase in our + test-class inheritance chain. + """ + + if sys.version_info < (3, 5): + raise ImportError("Async wrapper is not needed for Python 2.7 code.") + + import asyncio + + @functools.wraps(test_fn) + def run(test_class_instance, *args, **kwargs): + trim_kwargs_from_test_function(test_fn, kwargs) + loop = asyncio.get_event_loop() + return loop.run_until_complete(test_fn(test_class_instance, **kwargs)) + + return run + + def sleep(self, seconds): + if self.is_live: + time.sleep(seconds) + + def generate_sas(self, *args, **kwargs): + sas_func = args[0] + sas_func_pos_args = args[1:] + + fake_value = kwargs.pop("fake_value", "fake_token_value") + token = sas_func(*sas_func_pos_args, **kwargs) + + fake_token = self._create_fake_token(token, fake_value) + + if self.is_live: + return token + return fake_token + + def _create_fake_token(self, token, fake_value): + parts = token.split("&") + + for idx, part in enumerate(parts): + if part.startswith("sig"): + key = part.split("=") + key[1] = fake_value + parts[idx] = "=".join(key) + elif part.startswith("st"): + key = part.split("=") + key[1] = "start" + parts[idx] = "=".join(key) + elif part.startswith("se"): + key = part.split("=") + key[1] = "end" + parts[idx] = "=".join(key) + + return "&".join(parts) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py new file mode 100644 index 000000000000..c01048995bc4 --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -0,0 +1,144 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import os +import requests + +try: + # py3 + import urllib.parse as url_parse +except: + # py2 + import urlparse as url_parse + +import subprocess + +# the functions we patch +from azure.core.pipeline.transport import RequestsTransport + +# the trimming function to clean up incoming arguments to the test function we are wrapping +from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function + +# defaults +PROXY_URL = "http://localhost:5000" +RECORDING_START_URL = "{}/record/start".format(PROXY_URL) +RECORDING_STOP_URL = "{}/record/stop".format(PROXY_URL) +PLAYBACK_START_URL = "{}/playback/start".format(PROXY_URL) +PLAYBACK_STOP_URL = "{}/playback/stop".format(PROXY_URL) + +# TODO, create a pytest scope="session" implementation that can be added to a fixture such that unit tests can +# startup/shutdown the local test proxy +# this should also fire the admin mapping updates, and start/end the session for commiting recording updates + + +def get_test_id(): + # pytest sets the current running test in an environment variable + return os.getenv("PYTEST_CURRENT_TEST").split(" ")[0].replace("::", ".") + + +def get_current_sha(): + result = subprocess.check_output(["git", "rev-parse", "HEAD"]) + + # TODO: is this compatible with py27? + return result.decode("utf-8").strip() + + +def start_record_or_playback(test_id): + if os.getenv("AZURE_RECORD_MODE") == "record": + result = requests.post( + RECORDING_START_URL, + headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, + verify=False, + ) + recording_id = result.headers["x-recording-id"] + elif os.getenv("AZURE_RECORD_MODE") == "playback": + result = requests.post( + PLAYBACK_START_URL, + # headers={"x-recording-file": test_id, "x-recording-id": recording_id}, + headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, + verify=False, + ) + recording_id = result.headers["x-recording-id"] + return recording_id + + +def stop_record_or_playback(test_id, recording_id): + if os.getenv("AZURE_RECORD_MODE") == "record": + result = requests.post( + RECORDING_STOP_URL, + headers={"x-recording-file": test_id, "x-recording-id": recording_id, "x-recording-save": "true"}, + verify=False, + ) + elif os.getenv("AZURE_RECORD_MODE") == "playback": + result = requests.post( + PLAYBACK_STOP_URL, + headers={"x-recording-file": test_id, "x-recording-id": recording_id}, + verify=False, + ) + + +def get_proxy_netloc(): + parsed_result = url_parse.urlparse(PROXY_URL) + return {"scheme": parsed_result.scheme, "netloc": parsed_result.netloc} + + +def transform_request(request, recording_id): + """Redirect the request to the test proxy, and store the original request URI in a header""" + upstream_url = request.url.replace("//text", "/text") + headers = request.headers + + # quiet passthrough if neither are set + if os.getenv("AZURE_RECORD_MODE") == "record" or os.getenv("AZURE_RECORD_MODE") == "playback": + parsed_result = url_parse.urlparse(request.url) + updated_target = parsed_result._replace(**get_proxy_netloc()).geturl() + if headers.get("x-recording-upstream-base-uri", None) is None: + headers["x-recording-upstream-base-uri"] = "{}://{}".format(parsed_result.scheme, parsed_result.netloc) + headers["x-recording-id"] = recording_id + headers["x-recording-mode"] = os.getenv("AZURE_RECORD_MODE") + request.url = updated_target + + +def RecordedByProxy(func): + def record_wrap(*args, **kwargs): + test_id = get_test_id() + recording_id = start_record_or_playback(test_id) + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + request = copied_positional_args[1] + + # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are + # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get + # a recommendation for how to get a valid SSL Cert for localhost + kwargs["connection_verify"] = False + + transform_request(request, recording_id) + + return tuple(copied_positional_args), kwargs + + trimmed_kwargs = {k: v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + original_transport_func = RequestsTransport.send + + def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + req = adjusted_args[1] + return original_transport_func(*adjusted_args, **adjusted_kwargs) + + RequestsTransport.send = combined_call + + # call the modified function. + try: + value = func(*args, **trimmed_kwargs) + finally: + RequestsTransport.send = original_transport_func + # print("Exiting patch context. RequestsTransport.send is at {}".format(id(RequestsTransport.send))) + stop_record_or_playback(test_id, recording_id) + + return value + + return record_wrap From 98fdfa93d20b792a948dc4a9ea6296ee89054634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Mon, 9 Aug 2021 17:01:34 -0700 Subject: [PATCH 02/12] Use AZURE_TEST_RUN_LIVE as recording flag --- .../azure_recorded_testcase.py | 14 ++++++++++--- .../devtools_testutils/proxy_testcase.py | 21 +++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index e15321dadefe..4428ccbe7b08 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -13,11 +13,11 @@ from dotenv import load_dotenv, find_dotenv from azure_devtools.scenario_tests import AzureTestError +from azure_devtools.scenario_tests.config import TestConfig from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function -from .config import TEST_SETTING_FILENAME from . import mgmt_settings_fake as fake_settings -from .azure_testcase import is_live, _is_autorest_v3, get_resource_name, get_qualified_method_name +from .azure_testcase import _is_autorest_v3, get_resource_name, get_qualified_method_name try: # Try to import the AsyncFakeCredential, if we cannot assume it is Python 2 @@ -28,6 +28,14 @@ load_dotenv(find_dotenv()) + +def is_live(): + """A module version of is_live, that could be used in pytest marker.""" + if not hasattr(is_live, "_cache"): + is_live._cache = TestConfig().record_mode + return is_live._cache + + class AzureRecordedTestCase(object): @property @@ -60,7 +68,7 @@ def qualified_test_name(self): @property def in_recording(self): - return os.getenv("AZURE_RECORD_MODE") == "record" + return self.is_live # TODO: This needs to be removed, recording processors are handled on the proxy side, but # this is needed for the preparers diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index c01048995bc4..29b0968bda4f 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -34,6 +34,10 @@ # this should also fire the admin mapping updates, and start/end the session for commiting recording updates +IS_LIVE = os.getenv("AZURE_TEST_RUN_LIVE") == "true" or os.getenv("AZURE_TEST_RUN_LIVE") == "yes" +IS_PLAYBACK = os.getenv("AZURE_TEST_RUN_LIVE") == "false" or os.getenv("AZURE_TEST_RUN_LIVE") == "no" + + def get_test_id(): # pytest sets the current running test in an environment variable return os.getenv("PYTEST_CURRENT_TEST").split(" ")[0].replace("::", ".") @@ -47,14 +51,14 @@ def get_current_sha(): def start_record_or_playback(test_id): - if os.getenv("AZURE_RECORD_MODE") == "record": + if IS_LIVE: result = requests.post( RECORDING_START_URL, headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, verify=False, ) recording_id = result.headers["x-recording-id"] - elif os.getenv("AZURE_RECORD_MODE") == "playback": + elif IS_PLAYBACK: result = requests.post( PLAYBACK_START_URL, # headers={"x-recording-file": test_id, "x-recording-id": recording_id}, @@ -66,14 +70,14 @@ def start_record_or_playback(test_id): def stop_record_or_playback(test_id, recording_id): - if os.getenv("AZURE_RECORD_MODE") == "record": - result = requests.post( + if IS_LIVE: + requests.post( RECORDING_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id, "x-recording-save": "true"}, verify=False, ) - elif os.getenv("AZURE_RECORD_MODE") == "playback": - result = requests.post( + elif IS_PLAYBACK: + requests.post( PLAYBACK_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id}, verify=False, @@ -87,17 +91,16 @@ def get_proxy_netloc(): def transform_request(request, recording_id): """Redirect the request to the test proxy, and store the original request URI in a header""" - upstream_url = request.url.replace("//text", "/text") headers = request.headers # quiet passthrough if neither are set - if os.getenv("AZURE_RECORD_MODE") == "record" or os.getenv("AZURE_RECORD_MODE") == "playback": + if IS_LIVE or IS_PLAYBACK: parsed_result = url_parse.urlparse(request.url) updated_target = parsed_result._replace(**get_proxy_netloc()).geturl() if headers.get("x-recording-upstream-base-uri", None) is None: headers["x-recording-upstream-base-uri"] = "{}://{}".format(parsed_result.scheme, parsed_result.netloc) headers["x-recording-id"] = recording_id - headers["x-recording-mode"] = os.getenv("AZURE_RECORD_MODE") + headers["x-recording-mode"] = "record" if IS_LIVE else "playback" request.url = updated_target From da070f4c01a6c4924b4c3408690ea4aa71509b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Mon, 9 Aug 2021 17:35:00 -0700 Subject: [PATCH 03/12] RecordedByProxyAsync and preparer compat. --- .../scenario_tests/preparers.py | 4 +- .../devtools_testutils/__init__.py | 4 ++ .../devtools_testutils/aio/__init__.py | 5 ++ .../aio/proxy_testcase_async.py | 54 +++++++++++++++++++ .../devtools_testutils/powershell_preparer.py | 8 +-- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tools/azure-sdk-tools/devtools_testutils/aio/__init__.py create mode 100644 tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py diff --git a/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py b/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py index 1bc992962882..73f1150db866 100644 --- a/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py +++ b/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py @@ -135,7 +135,9 @@ def _preparer_wrapper(test_class_instance, **kwargs): ) if test_class_instance.is_live: - test_class_instance.scrubber.register_name_pair(resource_name, self.moniker) + # Adding this for new proxy testcase + if hasattr(test_class_instance, "scrubber"): + test_class_instance.scrubber.register_name_pair(resource_name, self.moniker) # We shouldn't trim the same kwargs that we use for deletion, # we may remove some of the variables we needed to do the delete. diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 640da9b62531..4d142e6fae2e 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -1,4 +1,5 @@ from .mgmt_testcase import AzureMgmtTestCase, AzureMgmtPreparer +from .azure_recorded_testcase import AzureRecordedTestCase from .azure_testcase import AzureTestCase, is_live, get_region_override from .resource_testcase import ( FakeResource, @@ -14,12 +15,14 @@ ) from .keyvault_preparer import KeyVaultPreparer from .powershell_preparer import PowerShellPreparer +from .proxy_testcase import RecordedByProxy from .helpers import ResponseCallback, RetryCounter from .fake_credential import FakeTokenCredential __all__ = [ "AzureMgmtTestCase", "AzureMgmtPreparer", + "AzureRecordedTestCase", "FakeResource", "ResourceGroupPreparer", "StorageAccountPreparer", @@ -33,6 +36,7 @@ "RandomNameResourceGroupPreparer", "CachedResourceGroupPreparer", "PowerShellPreparer", + "RecordedByProxy", "ResponseCallback", "RetryCounter", "FakeTokenCredential", diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py b/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py new file mode 100644 index 000000000000..5d6674dc4843 --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py @@ -0,0 +1,5 @@ +from .proxy_testcase_async import RecordedByProxyAsync + +__all__ = [ + "RecordedByProxyAsync" +] diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py new file mode 100644 index 000000000000..e217ef4f56c4 --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py @@ -0,0 +1,54 @@ +from ..proxy_testcase import ( + get_test_id, + start_record_or_playback, + transform_request, + stop_record_or_playback, +) + +from azure.core.pipeline.transport import AioHttpTransport + +from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function + +def RecordedByProxyAsync(func): + async def record_wrap(*args, **kwargs): + test_id = get_test_id() + recording_id = start_record_or_playback(test_id) + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + request = copied_positional_args[1] + + # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are + # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get + # a recommendation for how to get a valid SSL Cert for localhost + kwargs["connection_verify"] = False + + transform_request(request, recording_id) + + return tuple(copied_positional_args), kwargs + + trimmed_kwargs = {k: v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + original_func = AioHttpTransport.send + + async def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + req = adjusted_args[1] + print("HEADERS: ", req.headers) + print("BODY: ", req.body) + print("METHOD: ", req.method) + return await original_func(*adjusted_args, **adjusted_kwargs) + + AioHttpTransport.send = combined_call + + # call the modified function. + try: + value = await func(*args, **trimmed_kwargs) + finally: + AioHttpTransport.send = original_func + stop_record_or_playback(test_id, recording_id) + + return value + + return record_wrap diff --git a/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py b/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py index 562bb825bec6..d7df0c5422cc 100644 --- a/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py +++ b/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py @@ -65,9 +65,11 @@ def create_resource(self, name, **kwargs): scrubbed_value = self.fake_values[key] if scrubbed_value: self.real_values[key.lower()] = os.environ[key.upper()] - self.test_class_instance.scrubber.register_name_pair( - self.real_values[key.lower()], scrubbed_value - ) + # Adding this for new proxy testcase + if hasattr(self.test_class_instance, "scrubber"): + self.test_class_instance.scrubber.register_name_pair( + self.real_values[key.lower()], scrubbed_value + ) else: template = 'To pass a live ID you must provide the scrubbed value for recordings to \ prevent secrets from being written to files. {} was not given. For example: \ From ad60ef11d29bcd29fe5b3553e3ea9ddb1767f5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 10 Aug 2021 12:06:06 -0700 Subject: [PATCH 04/12] Default to playback mode --- .../devtools_testutils/proxy_testcase.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 29b0968bda4f..88ff55f40f97 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -21,6 +21,7 @@ # the trimming function to clean up incoming arguments to the test function we are wrapping from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function +from devtools_testutils.azure_recorded_testcase import is_live # defaults PROXY_URL = "http://localhost:5000" @@ -34,10 +35,6 @@ # this should also fire the admin mapping updates, and start/end the session for commiting recording updates -IS_LIVE = os.getenv("AZURE_TEST_RUN_LIVE") == "true" or os.getenv("AZURE_TEST_RUN_LIVE") == "yes" -IS_PLAYBACK = os.getenv("AZURE_TEST_RUN_LIVE") == "false" or os.getenv("AZURE_TEST_RUN_LIVE") == "no" - - def get_test_id(): # pytest sets the current running test in an environment variable return os.getenv("PYTEST_CURRENT_TEST").split(" ")[0].replace("::", ".") @@ -51,14 +48,14 @@ def get_current_sha(): def start_record_or_playback(test_id): - if IS_LIVE: + if is_live: result = requests.post( RECORDING_START_URL, headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, verify=False, ) recording_id = result.headers["x-recording-id"] - elif IS_PLAYBACK: + else: result = requests.post( PLAYBACK_START_URL, # headers={"x-recording-file": test_id, "x-recording-id": recording_id}, @@ -70,13 +67,13 @@ def start_record_or_playback(test_id): def stop_record_or_playback(test_id, recording_id): - if IS_LIVE: + if is_live: requests.post( RECORDING_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id, "x-recording-save": "true"}, verify=False, ) - elif IS_PLAYBACK: + else: requests.post( PLAYBACK_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id}, @@ -93,15 +90,13 @@ def transform_request(request, recording_id): """Redirect the request to the test proxy, and store the original request URI in a header""" headers = request.headers - # quiet passthrough if neither are set - if IS_LIVE or IS_PLAYBACK: - parsed_result = url_parse.urlparse(request.url) - updated_target = parsed_result._replace(**get_proxy_netloc()).geturl() - if headers.get("x-recording-upstream-base-uri", None) is None: - headers["x-recording-upstream-base-uri"] = "{}://{}".format(parsed_result.scheme, parsed_result.netloc) - headers["x-recording-id"] = recording_id - headers["x-recording-mode"] = "record" if IS_LIVE else "playback" - request.url = updated_target + parsed_result = url_parse.urlparse(request.url) + updated_target = parsed_result._replace(**get_proxy_netloc()).geturl() + if headers.get("x-recording-upstream-base-uri", None) is None: + headers["x-recording-upstream-base-uri"] = "{}://{}".format(parsed_result.scheme, parsed_result.netloc) + headers["x-recording-id"] = recording_id + headers["x-recording-mode"] = "record" if is_live else "playback" + request.url = updated_target def RecordedByProxy(func): From cd2a545593fb0448efaf2b313d4172343e05b6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 10 Aug 2021 12:15:57 -0700 Subject: [PATCH 05/12] Run black --- .../devtools_testutils/aio/__init__.py | 4 +- .../aio/proxy_testcase_async.py | 11 +++-- .../azure_recorded_testcase.py | 45 +++++-------------- .../devtools_testutils/proxy_testcase.py | 2 +- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py b/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py index 5d6674dc4843..5265d80fe58e 100644 --- a/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/aio/__init__.py @@ -1,5 +1,3 @@ from .proxy_testcase_async import RecordedByProxyAsync -__all__ = [ - "RecordedByProxyAsync" -] +__all__ = ["RecordedByProxyAsync"] diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py index e217ef4f56c4..aac663f62912 100644 --- a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py +++ b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py @@ -1,3 +1,11 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.core.pipeline.transport import AioHttpTransport + +from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from ..proxy_testcase import ( get_test_id, start_record_or_playback, @@ -5,9 +13,6 @@ stop_record_or_playback, ) -from azure.core.pipeline.transport import AioHttpTransport - -from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function def RecordedByProxyAsync(func): async def record_wrap(*args, **kwargs): diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index 4428ccbe7b08..274f86a8a319 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -37,16 +37,13 @@ def is_live(): class AzureRecordedTestCase(object): - @property def settings(self): if self.is_live: if self._real_settings: return self._real_settings else: - raise AzureTestError( - "Need a mgmt_settings_real.py file to run tests live." - ) + raise AzureTestError("Need a mgmt_settings_real.py file to run tests live.") else: return self._fake_settings @@ -82,11 +79,7 @@ def is_playback(self): def get_settings_value(self, key): key_value = os.environ.get("AZURE_" + key, None) - if ( - key_value - and self._real_settings - and getattr(self._real_settings, key) != key_value - ): + if key_value and self._real_settings and getattr(self._real_settings, key) != key_value: raise ValueError( "You have both AZURE_{key} env variable and mgmt_settings_real.py for {key} to different values".format( key=key @@ -102,16 +95,9 @@ def get_settings_value(self, key): return key_value def get_credential(self, client_class, **kwargs): - - tenant_id = os.environ.get( - "AZURE_TENANT_ID", getattr(self._real_settings, "TENANT_ID", None) - ) - client_id = os.environ.get( - "AZURE_CLIENT_ID", getattr(self._real_settings, "CLIENT_ID", None) - ) - secret = os.environ.get( - "AZURE_CLIENT_SECRET", getattr(self._real_settings, "CLIENT_SECRET", None) - ) + tenant_id = os.environ.get("AZURE_TENANT_ID", getattr(self._real_settings, "TENANT_ID", None)) + client_id = os.environ.get("AZURE_CLIENT_ID", getattr(self._real_settings, "CLIENT_ID", None)) + secret = os.environ.get("AZURE_CLIENT_SECRET", getattr(self._real_settings, "CLIENT_SECRET", None)) is_async = kwargs.pop("is_async", False) if tenant_id and client_id and secret and self.is_live: @@ -121,24 +107,21 @@ def get_credential(self, client_class, **kwargs): if is_async: from azure.identity.aio import ClientSecretCredential - return ClientSecretCredential( - tenant_id=tenant_id, client_id=client_id, client_secret=secret - ) + return ClientSecretCredential(tenant_id=tenant_id, client_id=client_id, client_secret=secret) else: # Create msrestazure class from msrestazure.azure_active_directory import ( ServicePrincipalCredentials, ) - return ServicePrincipalCredentials( - tenant=tenant_id, client_id=client_id, secret=secret - ) + return ServicePrincipalCredentials(tenant=tenant_id, client_id=client_id, secret=secret) else: if _is_autorest_v3(client_class): if is_async: if self.is_live: raise ValueError( - "Async live doesn't support mgmt_setting_real, please set AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET" + "Async live doesn't support mgmt_setting_real, please set AZURE_TENANT_ID," + "AZURE_CLIENT_ID, AZURE_CLIENT_SECRET" ) return AsyncFakeCredential() else: @@ -160,9 +143,7 @@ def create_client_from_credential(self, client_class, credential, **kwargs): if self.is_playback(): try: - client._config.polling_interval = ( - 0 # FIXME in azure-mgmt-core, make this a kwargs - ) + client._config.polling_interval = 0 # FIXME in azure-mgmt-core, make this a kwargs except AttributeError: pass @@ -193,7 +174,7 @@ def get_resource_name(self, name): return self.create_random_name(name) def get_replayable_random_resource_name(self, name): - """In a replay scenario, (is not live) gives the static moniker. In the random scenario, gives generated name.""" + """In a replay scenario (not live), gives the static moniker. In the random scenario, gives generated name.""" if self.is_live: created_name = self.create_random_name(name) self.scrubber.register_name_pair(created_name, name) @@ -204,9 +185,7 @@ def get_preparer_resource_name(self, prefix): If prefix is a blank string, use the fully qualified test name instead. This is what legacy tests do for resource groups.""" - return self.get_resource_name( - prefix #or self.qualified_test_name.replace(".", "_") - ) + return self.get_resource_name(prefix) # or self.qualified_test_name.replace(".", "_") @staticmethod def await_prepared_test(test_fn): diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 88ff55f40f97..8c0f4664422f 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -3,7 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- - import os import requests @@ -23,6 +22,7 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from devtools_testutils.azure_recorded_testcase import is_live + # defaults PROXY_URL = "http://localhost:5000" RECORDING_START_URL = "{}/record/start".format(PROXY_URL) From 2f7a34ca41b398651b43590c212c1fabf923d298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 10 Aug 2021 12:26:15 -0700 Subject: [PATCH 06/12] Remove debugging print statements --- .../devtools_testutils/aio/proxy_testcase_async.py | 3 --- .../devtools_testutils/azure_recorded_testcase.py | 7 +++---- tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py index aac663f62912..1477899c49b6 100644 --- a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py +++ b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py @@ -40,9 +40,6 @@ def transform_args(*args, **kwargs): async def combined_call(*args, **kwargs): adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) req = adjusted_args[1] - print("HEADERS: ", req.headers) - print("BODY: ", req.body) - print("METHOD: ", req.method) return await original_func(*adjusted_args, **adjusted_kwargs) AioHttpTransport.send = combined_call diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index 274f86a8a319..e82b2e80e707 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -7,6 +7,7 @@ import logging import os import os.path +import six import sys import time @@ -89,9 +90,8 @@ def get_settings_value(self, key): if not key_value: try: key_value = getattr(self.settings, key) - except Exception: - print("Could not get {}".format(key)) - raise + except Exception as ex: + six.raise_from(ValueError("Could not get {}".format(key)), ex) return key_value def get_credential(self, client_class, **kwargs): @@ -166,7 +166,6 @@ def create_basic_client(self, client_class, **kwargs): def create_random_name(self, name): unique_test_name = os.getenv("PYTEST_CURRENT_TEST").encode("utf-8") - print(unique_test_name) return get_resource_name(name, unique_test_name) def get_resource_name(self, name): diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 8c0f4664422f..fe04266ae854 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -134,7 +134,6 @@ def combined_call(*args, **kwargs): value = func(*args, **trimmed_kwargs) finally: RequestsTransport.send = original_transport_func - # print("Exiting patch context. RequestsTransport.send is at {}".format(id(RequestsTransport.send))) stop_record_or_playback(test_id, recording_id) return value From 4d7f3eba043e09082bad108d69df1bd3ff56f458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 10 Aug 2021 19:35:32 -0700 Subject: [PATCH 07/12] add_sanitizer (still need HTTPS default) --- .../aio/proxy_testcase_async.py | 2 +- .../azure_recorded_testcase.py | 22 ++++++++++++++++++- .../devtools_testutils/config.py | 1 + .../devtools_testutils/enums.py | 11 ++++++++++ .../devtools_testutils/proxy_testcase.py | 12 +++++----- 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 tools/azure-sdk-tools/devtools_testutils/enums.py diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py index 1477899c49b6..6848e1d751c2 100644 --- a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py +++ b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py @@ -26,7 +26,7 @@ def transform_args(*args, **kwargs): # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get # a recommendation for how to get a valid SSL Cert for localhost - kwargs["connection_verify"] = False + # kwargs["connection_verify"] = False transform_request(request, recording_id) diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index e82b2e80e707..f6e465aee66f 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -7,9 +7,11 @@ import logging import os import os.path +import requests import six import sys import time +from typing import TYPE_CHECKING from dotenv import load_dotenv, find_dotenv @@ -19,6 +21,8 @@ from . import mgmt_settings_fake as fake_settings from .azure_testcase import _is_autorest_v3, get_resource_name, get_qualified_method_name +from .config import PROXY_URL +from .enums import ProxyRecordingSanitizer try: # Try to import the AsyncFakeCredential, if we cannot assume it is Python 2 @@ -26,6 +30,9 @@ except SyntaxError: pass +if TYPE_CHECKING: + from typing import Optional + load_dotenv(find_dotenv()) @@ -74,6 +81,19 @@ def in_recording(self): def recording_processors(self): return [] + def add_sanitizer(self, sanitizer, regex=None, value=None): + # type: (ProxyRecordingSanitizer, Optional[str], Optional[str]) -> None + if sanitizer == ProxyRecordingSanitizer.URI: + requests.post( + "{}/Admin/AddSanitizer".format(PROXY_URL), + headers={"x-abstraction-identifier": ProxyRecordingSanitizer.URI.value}, + json={ + "regex": regex or "[a-z]+(?=(?:-secondary)\\.(?:table|blob|queue)\\.core\\.windows\\.net)", + "value": value or "fakevalue" + }, + verify=False + ) + def is_playback(self): return not self.is_live @@ -184,7 +204,7 @@ def get_preparer_resource_name(self, prefix): If prefix is a blank string, use the fully qualified test name instead. This is what legacy tests do for resource groups.""" - return self.get_resource_name(prefix) # or self.qualified_test_name.replace(".", "_") + return self.get_resource_name(prefix) @staticmethod def await_prepared_test(test_fn): diff --git a/tools/azure-sdk-tools/devtools_testutils/config.py b/tools/azure-sdk-tools/devtools_testutils/config.py index 117c3b38ca08..27b4efd90d6a 100644 --- a/tools/azure-sdk-tools/devtools_testutils/config.py +++ b/tools/azure-sdk-tools/devtools_testutils/config.py @@ -1 +1,2 @@ +PROXY_URL = "http://localhost:5000" TEST_SETTING_FILENAME = "testsettings_local.cfg" diff --git a/tools/azure-sdk-tools/devtools_testutils/enums.py b/tools/azure-sdk-tools/devtools_testutils/enums.py new file mode 100644 index 000000000000..f1456dbc8a1f --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/enums.py @@ -0,0 +1,11 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from enum import Enum + +class ProxyRecordingSanitizer(str, Enum): + """General-purpose sanitizers for sanitizing test proxy recordings""" + + URI = "UriRegexSanitizer" diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index fe04266ae854..67d6d5906c45 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -21,10 +21,10 @@ # the trimming function to clean up incoming arguments to the test function we are wrapping from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from devtools_testutils.azure_recorded_testcase import is_live +from .config import PROXY_URL # defaults -PROXY_URL = "http://localhost:5000" RECORDING_START_URL = "{}/record/start".format(PROXY_URL) RECORDING_STOP_URL = "{}/record/stop".format(PROXY_URL) PLAYBACK_START_URL = "{}/playback/start".format(PROXY_URL) @@ -108,10 +108,10 @@ def transform_args(*args, **kwargs): copied_positional_args = list(args) request = copied_positional_args[1] - # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are - # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get - # a recommendation for how to get a valid SSL Cert for localhost - kwargs["connection_verify"] = False + # # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are + # # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get + # # a recommendation for how to get a valid SSL Cert for localhost + # kwargs["connection_verify"] = False transform_request(request, recording_id) @@ -138,4 +138,4 @@ def combined_call(*args, **kwargs): return value - return record_wrap + return record_wrap \ No newline at end of file From 444f41eea7dbb6cb6fc227cb218a307b661b6ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 11 Aug 2021 21:23:43 -0700 Subject: [PATCH 08/12] Default to HTTPS --- .gitignore | 3 +++ .../aio/proxy_testcase_async.py | 6 ------ .../azure_recorded_testcase.py | 1 - .../devtools_testutils/config.py | 9 +++++++- .../devtools_testutils/proxy_testcase.py | 21 +++++-------------- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 87b0050a20cc..fa4e4fc71035 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,6 @@ sdk/cosmos/azure-cosmos/test/test_config.py # env vars .env + +# local SSL certificate folder +.certificate diff --git a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py index 6848e1d751c2..11c8f9e44395 100644 --- a/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py +++ b/tools/azure-sdk-tools/devtools_testutils/aio/proxy_testcase_async.py @@ -23,11 +23,6 @@ def transform_args(*args, **kwargs): copied_positional_args = list(args) request = copied_positional_args[1] - # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are - # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get - # a recommendation for how to get a valid SSL Cert for localhost - # kwargs["connection_verify"] = False - transform_request(request, recording_id) return tuple(copied_positional_args), kwargs @@ -39,7 +34,6 @@ def transform_args(*args, **kwargs): async def combined_call(*args, **kwargs): adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - req = adjusted_args[1] return await original_func(*adjusted_args, **adjusted_kwargs) AioHttpTransport.send = combined_call diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index f6e465aee66f..e045da62966a 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -91,7 +91,6 @@ def add_sanitizer(self, sanitizer, regex=None, value=None): "regex": regex or "[a-z]+(?=(?:-secondary)\\.(?:table|blob|queue)\\.core\\.windows\\.net)", "value": value or "fakevalue" }, - verify=False ) def is_playback(self): diff --git a/tools/azure-sdk-tools/devtools_testutils/config.py b/tools/azure-sdk-tools/devtools_testutils/config.py index 27b4efd90d6a..7b9bcfbb06aa 100644 --- a/tools/azure-sdk-tools/devtools_testutils/config.py +++ b/tools/azure-sdk-tools/devtools_testutils/config.py @@ -1,2 +1,9 @@ -PROXY_URL = "http://localhost:5000" +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + + +PROXY_URL = "https://localhost:5001" TEST_SETTING_FILENAME = "testsettings_local.cfg" diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 67d6d5906c45..6ab7d75e64d4 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -20,7 +20,7 @@ # the trimming function to clean up incoming arguments to the test function we are wrapping from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function -from devtools_testutils.azure_recorded_testcase import is_live +from .azure_recorded_testcase import is_live from .config import PROXY_URL @@ -48,36 +48,31 @@ def get_current_sha(): def start_record_or_playback(test_id): - if is_live: + if is_live(): result = requests.post( RECORDING_START_URL, headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, - verify=False, ) recording_id = result.headers["x-recording-id"] else: result = requests.post( PLAYBACK_START_URL, - # headers={"x-recording-file": test_id, "x-recording-id": recording_id}, headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, - verify=False, ) recording_id = result.headers["x-recording-id"] return recording_id def stop_record_or_playback(test_id, recording_id): - if is_live: + if is_live(): requests.post( RECORDING_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id, "x-recording-save": "true"}, - verify=False, ) else: requests.post( PLAYBACK_STOP_URL, headers={"x-recording-file": test_id, "x-recording-id": recording_id}, - verify=False, ) @@ -95,7 +90,7 @@ def transform_request(request, recording_id): if headers.get("x-recording-upstream-base-uri", None) is None: headers["x-recording-upstream-base-uri"] = "{}://{}".format(parsed_result.scheme, parsed_result.netloc) headers["x-recording-id"] = recording_id - headers["x-recording-mode"] = "record" if is_live else "playback" + headers["x-recording-mode"] = "record" if is_live() else "playback" request.url = updated_target @@ -108,11 +103,6 @@ def transform_args(*args, **kwargs): copied_positional_args = list(args) request = copied_positional_args[1] - # # TODO, get the test-proxy server a real SSL certificate. The issue here is that SSL Certificates are - # # normally associated with a domain name. Need to talk to the //SSLAdmin folks (or someone else) and get - # # a recommendation for how to get a valid SSL Cert for localhost - # kwargs["connection_verify"] = False - transform_request(request, recording_id) return tuple(copied_positional_args), kwargs @@ -124,7 +114,6 @@ def transform_args(*args, **kwargs): def combined_call(*args, **kwargs): adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - req = adjusted_args[1] return original_transport_func(*adjusted_args, **adjusted_kwargs) RequestsTransport.send = combined_call @@ -138,4 +127,4 @@ def combined_call(*args, **kwargs): return value - return record_wrap \ No newline at end of file + return record_wrap From d2cd0b067120b99f852c5ef5280b5831917e0720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 11 Aug 2021 21:55:41 -0700 Subject: [PATCH 09/12] Add ProxyRecordingSanitizer to __init__ --- sdk/tables/azure-data-tables/tests/test_table_service_stats.py | 2 +- .../azure-data-tables/tests/test_table_service_stats_async.py | 2 +- tools/azure-sdk-tools/devtools_testutils/__init__.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/tables/azure-data-tables/tests/test_table_service_stats.py b/sdk/tables/azure-data-tables/tests/test_table_service_stats.py index 6658e702d677..3f623f1b6551 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_service_stats.py +++ b/sdk/tables/azure-data-tables/tests/test_table_service_stats.py @@ -10,7 +10,7 @@ from preparers import tables_decorator # --Test Class ----------------------------------------------------------------- -class TableServiceStatsTest(AzureTestCase, TableTestCase): +class TestTableServiceStats(AzureTestCase, TableTestCase): # --Test cases per service --------------------------------------- @tables_decorator diff --git a/sdk/tables/azure-data-tables/tests/test_table_service_stats_async.py b/sdk/tables/azure-data-tables/tests/test_table_service_stats_async.py index 2aeffef1e965..e22d406ef8e4 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_service_stats_async.py +++ b/sdk/tables/azure-data-tables/tests/test_table_service_stats_async.py @@ -19,7 +19,7 @@ '> ' -class TableServiceStatsTest(AzureTestCase, AsyncTableTestCase): +class TestTableServiceStats(AzureTestCase, AsyncTableTestCase): @staticmethod def override_response_body_with_unavailable_status(response): diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 4d142e6fae2e..69db6f41dfb3 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -16,6 +16,7 @@ from .keyvault_preparer import KeyVaultPreparer from .powershell_preparer import PowerShellPreparer from .proxy_testcase import RecordedByProxy +from .enums import ProxyRecordingSanitizer from .helpers import ResponseCallback, RetryCounter from .fake_credential import FakeTokenCredential @@ -36,6 +37,7 @@ "RandomNameResourceGroupPreparer", "CachedResourceGroupPreparer", "PowerShellPreparer", + "ProxyRecordingSanitizer", "RecordedByProxy", "ResponseCallback", "RetryCounter", From f9f7ed3d407da07a10bc48e46c01a5eb6cfdab3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Thu, 12 Aug 2021 10:06:14 -0700 Subject: [PATCH 10/12] Thanks, Sean! --- .../src/azure_devtools/scenario_tests/preparers.py | 6 +++++- .../devtools_testutils/azure_recorded_testcase.py | 2 +- .../devtools_testutils/powershell_preparer.py | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py b/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py index 73f1150db866..733ad04a689a 100644 --- a/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py +++ b/tools/azure-devtools/src/azure_devtools/scenario_tests/preparers.py @@ -6,7 +6,6 @@ import contextlib import functools import logging -import sys from collections import namedtuple from threading import Lock @@ -138,6 +137,11 @@ def _preparer_wrapper(test_class_instance, **kwargs): # Adding this for new proxy testcase if hasattr(test_class_instance, "scrubber"): test_class_instance.scrubber.register_name_pair(resource_name, self.moniker) + else: + _logger.info( + "This test class instance has no scrubber, so the AbstractPreparer will not scrub any values " + "in recordings." + ) # We shouldn't trim the same kwargs that we use for deletion, # we may remove some of the variables we needed to do the delete. diff --git a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py index e045da62966a..d39cfe79960c 100644 --- a/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py @@ -139,7 +139,7 @@ def get_credential(self, client_class, **kwargs): if is_async: if self.is_live: raise ValueError( - "Async live doesn't support mgmt_setting_real, please set AZURE_TENANT_ID," + "Async live doesn't support mgmt_setting_real, please set AZURE_TENANT_ID, " "AZURE_CLIENT_ID, AZURE_CLIENT_SECRET" ) return AsyncFakeCredential() diff --git a/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py b/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py index d7df0c5422cc..0ce216f416c1 100644 --- a/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py +++ b/tools/azure-sdk-tools/devtools_testutils/powershell_preparer.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +import logging import os from . import AzureMgmtPreparer @@ -70,6 +71,12 @@ def create_resource(self, name, **kwargs): self.test_class_instance.scrubber.register_name_pair( self.real_values[key.lower()], scrubbed_value ) + else: + logger = logging.getLogger() + logger.info( + "This test class instance has no scrubber, so the PowerShellPreparer will not scrub " + "the value of {} in recordings.".format(key) + ) else: template = 'To pass a live ID you must provide the scrubbed value for recordings to \ prevent secrets from being written to files. {} was not given. For example: \ From e388418a102b21c0d2b6b12cbd15b98f487b437c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 17 Aug 2021 09:54:36 -0700 Subject: [PATCH 11/12] Correct recording storage (thanks Scott!) --- .../devtools_testutils/proxy_testcase.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 6ab7d75e64d4..ff0c151a9258 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -37,7 +37,17 @@ def get_test_id(): # pytest sets the current running test in an environment variable - return os.getenv("PYTEST_CURRENT_TEST").split(" ")[0].replace("::", ".") + setting_value = os.getenv("PYTEST_CURRENT_TEST") + + path_to_test = os.path.normpath(setting_value.split(" ")[0]) + path_components = path_to_test.split(os.sep) + + for idx, val in enumerate(path_components): + if val.startswith("test"): + path_components.insert(idx + 1, "recordings") + break + + return os.sep.join(path_components).replace("::", "").replace("\\", "/") def get_current_sha(): From 2d1811689dab365b936186621fd8660e21b3c543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 17 Aug 2021 13:48:29 -0700 Subject: [PATCH 12/12] Factor out get_current_sha --- .../devtools_testutils/proxy_testcase.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index ff0c151a9258..53cc41bf9064 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -50,24 +50,20 @@ def get_test_id(): return os.sep.join(path_components).replace("::", "").replace("\\", "/") -def get_current_sha(): +def start_record_or_playback(test_id): result = subprocess.check_output(["git", "rev-parse", "HEAD"]) + current_sha = result.decode("utf-8").strip() - # TODO: is this compatible with py27? - return result.decode("utf-8").strip() - - -def start_record_or_playback(test_id): if is_live(): result = requests.post( RECORDING_START_URL, - headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, + headers={"x-recording-file": test_id, "x-recording-sha": current_sha}, ) recording_id = result.headers["x-recording-id"] else: result = requests.post( PLAYBACK_START_URL, - headers={"x-recording-file": test_id, "x-recording-sha": get_current_sha()}, + headers={"x-recording-file": test_id, "x-recording-sha": current_sha}, ) recording_id = result.headers["x-recording-id"] return recording_id