pax_global_header00006660000000000000000000000064131456616610014523gustar00rootroot0000000000000052 comment=9c3ceae5fc673d5d96f0529bac531eba9527a762 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/000077500000000000000000000000001314566166100232015ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.coveragerc000066400000000000000000000001451314566166100253220ustar00rootroot00000000000000[run] branch = True include = src/* omit = */tests/* env/* [paths] source = src/ [report] azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.gitignore000066400000000000000000000022161314566166100251720ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Editor caches .vscode .vs *.pyproj *.pyproj.user *.sln # PyCharm / IntelliJ IDEA .idea/workspace.xml .idea/tasks.xml azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/000077500000000000000000000000001314566166100241615ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/azure-python-devtools.iml000066400000000000000000000010421314566166100311630ustar00rootroot00000000000000 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/inspectionProfiles/000077500000000000000000000000001314566166100300405ustar00rootroot00000000000000profiles_settings.xml000066400000000000000000000003441314566166100342470ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/inspectionProfiles azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/misc.xml000066400000000000000000000003521314566166100256360ustar00rootroot00000000000000 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/modules.xml000066400000000000000000000004461314566166100263570ustar00rootroot00000000000000 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.idea/vcs.xml000066400000000000000000000002641314566166100255000ustar00rootroot00000000000000 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/.travis.yml000066400000000000000000000006711314566166100253160ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "3.6" - "3.6-dev" install: - pip install -r requirements.txt script: - pylint src/azure_devtools - coverage run -m unittest discover -s src -v after_success: - codecov deploy: provider: pypi user: Laurent.Mazuel skip_upload_docs: true # password: use $PYPI_PASSWORD distributions: "sdist bdist_wheel" on: tags: true python: '3.6' azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/LICENSE000066400000000000000000000022371314566166100242120ustar00rootroot00000000000000 MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/README.rst000066400000000000000000000014771314566166100247010ustar00rootroot00000000000000.. image:: https://travis-ci.org/Azure/azure-python-devtools.svg?branch=master :target: https://travis-ci.org/Azure/azure-python-devtools Development tools for Python-based Azure tools ============================================== This package contains tools to aid in developing Python-based Azure code. Currently it includes ``scenario_tests``, a testing framework to handle much of the busywork associated with testing code that interacts with Azure. Contributing ============ This project has adopted the `Microsoft Open Source Code of Conduct `__. For more information see the `Code of Conduct FAQ `__ or contact `opencode@microsoft.com `__ with any additional questions or comments. azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/doc/000077500000000000000000000000001314566166100237465ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/doc/scenario_base_tests.md000066400000000000000000000111331314566166100303060ustar00rootroot00000000000000# How to write ReplayableTest based VCR tests The `scenario_tests` package uses the [VCR.py](https://pypi.python.org/pypi/vcrpy) library to record the HTTP messages exchanged during a program run and play them back at a later time, making it useful for creating "scenario tests" that interact with Azure (or other) services. These tests can be replayed at a later time without any network activity, allowing us to detect changes in the Python layers between the code being tested and the underlying REST API. ## Overview Tests all derive from the `ReplayableTest` class found in `azure_devtools.scenario_tests.base`. This class exposes the VCR tests using the standard Python `unittest` framework and allows the tests to be discovered by and debugged in Visual Studio. When you run a test, the test driver will automatically detect the test is unrecorded and record the HTTP requests and responses in a .yaml file (referred to by VCR.py as a "cassette"). If the test succeeds, the cassette will be preserved and future playthroughs of the test will come from the cassette rather than using actual network communication. If the tests are run on TravisCI, any tests which cannot be replayed will automatically fail. `ReplayableTest` itself derives from `IntegrationTestBase`, which provides some helpful methods for use in more general unit tests but no functionality pertaining to network communication. ## Configuring ReplayableTest The only configuration of `ReplayableTest` that is "exposed" (in the sense of being accessible other than through subclassing) is whether tests should be run in "live" or "playback" mode. This can be set in the following two ways, of which the first takes precedence: * Set the environment variable `AZURE_TEST_RUN_LIVE`. Any value will cause the tests to run in live mode; if the variable is unset the default of playback mode will be used. * Specify a boolean value for `live-mode` in a configuration file, the path to which must be specified by a `ReplayableTest` subclass as described below (i.e. by default no config file will be read). True values mean "live" mode; false ones mean "playback." "Live" and "playback" mode are actually just shorthand for recording modes in the underlying VCR.py package; they correspond to "all" and "once" as described in the [VCR.py documentation](http://vcrpy.readthedocs.io/en/latest/usage.html#record-modes). ### Subclassing ReplayableTest and features Most customization of `ReplayableTest` is accessible only through subclassing. The two main users of `ReplayableTest` are [azure-cli](https://github.com/Azure/azure-cli) and [azure-sdk-for-python](https://github.com/Azure/azure-sdk-for-python). Each uses a subclass of `ReplayableTest` to add context-specific functionality and preserve backward compatibility with test code prior to the existence of `azure-devtools`. For example, azure-cli's [compatibility layer](https://github.com/Azure/azure-cli/tree/master/src/azure-cli-testsdk) adds methods for running CLI commands and evaluating their output. Subclasses of `ReplayableTest` can configure its behavior by passing the following keyword arguments when they call its `__init__` method (probably using `super`): * `config_file`: Path to a configuration file. It should be in the format described in Python's [ConfigParser](https://docs.python.org/3/library/configparser.html) docs and currently allows only the boolean option `live-mode`. * `recording_dir` and `recording_name`: Directory path and file name, respectively, for the recording that should be used for a given test case. By default, the directory will be a `recordings` directory in the same location as the file containing the test case, and the file name will be the same as the test method name. A `.yaml` extension will be appended to whatever is used for `recording_name`. * `recording_processors` and `replay_processors`: Lists of `RecordingProcessor` instances for making changes to requests and responses during test recording and test playback, respectively. See [recording_processors.py](src/azure_devtools/scenario_tests/recording_processors.py) for some examples and how to implement them. * `recording_patches` and `replay_patches`: Lists of patches to apply to functions, methods, etc. during test recording and playback, respectively. See [patches.py](src/azure_devtools/scenario_tests/patches.py) for some examples. Note the `mock_in_unit_test` function which abstracts out some boilerplate for applying a patch. azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/pylintrc000066400000000000000000000014711314566166100247730ustar00rootroot00000000000000[MESSAGES CONTROL] # For all codes, run 'pylint --list-msgs' or go to 'http://pylint-messages.wikidot.com/all-codes' # C0111 Missing docstring # C0103 Invalid %s name "%s" # I0011 Warning locally suppressed using disable-msg # W0511 fixme # R0401 Cyclic import (because of https://github.com/PyCQA/pylint/issues/850) # R0913 Too many arguments - Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods. disable=C0111, C0103, W0511 [FORMAT] max-line-length=120 [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=yes [DESIGN] # Maximum number of locals for function / method body # max-locals=8 # Maximum number of branch for function / method body # max-branches=16 [SIMILARITIES] min-similarity-lines=8 azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/requirements.txt000066400000000000000000000001201314566166100264560ustar00rootroot00000000000000codecov==2.0.9 mock==2.0.0;python_version<="2.7" nose==1.3.7 pylint==1.7.1 -e .azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/setup.cfg000066400000000000000000000001241314566166100250170ustar00rootroot00000000000000[bdist_wheel] universal=1 [nosetests] tests=src/azure_devtools/scenario_tests/testsazure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/setup.py000066400000000000000000000030171314566166100247140ustar00rootroot00000000000000#!/usr/bin/env python # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import io from setuptools import setup VERSION = "0.5.0" CLASSIFIERS = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'License :: OSI Approved :: MIT License', ] DEPENDENCIES = [ 'ConfigArgParse>=0.12.0', 'six>=1.10.0', 'vcrpy>=1.11.0', ] with io.open('README.rst', 'r', encoding='utf-8') as f: README = f.read() setup( name='azure-devtools', version=VERSION, description='Microsoft Azure Development Tools for SDK', long_description=README, license='MIT', author='Microsoft Corporation', author_email='ptvshelp@microsoft.com', url='https://github.com/Azure/azure-python-devtools', zip_safe=False, classifiers=CLASSIFIERS, packages=[ 'azure_devtools', 'azure_devtools.scenario_tests' ], package_dir={'': 'src'}, install_requires=DEPENDENCIES, ) azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/000077500000000000000000000000001314566166100237705ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/000077500000000000000000000000001314566166100270355ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/__init__.py000066400000000000000000000000001314566166100311340ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/000077500000000000000000000000001314566166100320625ustar00rootroot00000000000000__init__.py000066400000000000000000000031321314566166100341130ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- from .base import IntegrationTestBase, ReplayableTest, LiveTest from .exceptions import AzureTestError from .decorators import live_only, record_only from .patches import mock_in_unit_test, patch_time_sleep_api, patch_long_run_operation_delay from .preparers import AbstractPreparer, SingleValueReplacer from .recording_processors import ( RecordingProcessor, SubscriptionRecordingProcessor, LargeRequestBodyProcessor, LargeResponseBodyProcessor, LargeResponseBodyReplacer, OAuthRequestResponsesFilter, DeploymentNameReplacer, GeneralNameReplacer, AccessTokenReplacer, ) from .utilities import create_random_name, get_sha1_hash __all__ = ['IntegrationTestBase', 'ReplayableTest', 'LiveTest', 'AzureTestError', 'mock_in_unit_test', 'patch_time_sleep_api', 'patch_long_run_operation_delay', 'AbstractPreparer', 'SingleValueReplacer', 'RecordingProcessor', 'SubscriptionRecordingProcessor', 'LargeRequestBodyProcessor', 'LargeResponseBodyProcessor', 'LargeResponseBodyReplacer', 'OAuthRequestResponsesFilter', 'DeploymentNameReplacer', 'GeneralNameReplacer', 'AccessTokenReplacer', 'live_only', 'record_only', 'create_random_name', 'get_sha1_hash'] __version__ = '0.5.0' base.py000066400000000000000000000156421314566166100332770ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- from __future__ import print_function import unittest import os import inspect import tempfile import shutil import logging import six import vcr from .config import TestConfig from .const import ENV_TEST_DIAGNOSE from .utilities import create_random_name from .decorators import live_only class IntegrationTestBase(unittest.TestCase): def __init__(self, method_name): super(IntegrationTestBase, self).__init__(method_name) self.diagnose = os.environ.get(ENV_TEST_DIAGNOSE, None) == 'True' self.logger = logging.getLogger('azure_devtools.scenario_tests') def create_random_name(self, prefix, length): # pylint: disable=no-self-use return create_random_name(prefix=prefix, length=length) def create_temp_file(self, size_kb, full_random=False): """ Create a temporary file for testing. The test harness will delete the file during tearing down. """ fd, path = tempfile.mkstemp() os.close(fd) self.addCleanup(lambda: os.remove(path)) with open(path, mode='r+b') as f: if full_random: chunk = os.urandom(1024) else: chunk = bytearray([0] * 1024) for _ in range(size_kb): f.write(chunk) return path def create_temp_dir(self): """ Create a temporary directory for testing. The test harness will delete the directory during tearing down. """ temp_dir = tempfile.mkdtemp() self.addCleanup(lambda: shutil.rmtree(temp_dir, ignore_errors=True)) return temp_dir @classmethod def set_env(cls, key, val): os.environ[key] = val @classmethod def pop_env(cls, key): return os.environ.pop(key, None) @live_only() class LiveTest(IntegrationTestBase): pass class ReplayableTest(IntegrationTestBase): # pylint: disable=too-many-instance-attributes FILTER_HEADERS = [ 'authorization', 'client-request-id', 'retry-after', 'x-ms-client-request-id', 'x-ms-correlation-request-id', 'x-ms-ratelimit-remaining-subscription-reads', 'x-ms-request-id', 'x-ms-routing-request-id', 'x-ms-gateway-service-instanceid', 'x-ms-ratelimit-remaining-tenant-reads', 'x-ms-served-by', ] def __init__(self, # pylint: disable=too-many-arguments method_name, config_file=None, recording_dir=None, recording_name=None, recording_processors=None, replay_processors=None, recording_patches=None, replay_patches=None): super(ReplayableTest, self).__init__(method_name) self.recording_processors = recording_processors or [] self.replay_processors = replay_processors or [] self.recording_patches = recording_patches or [] self.replay_patches = replay_patches or [] self.config = TestConfig(config_file=config_file) self.disable_recording = False test_file_path = inspect.getfile(self.__class__) recording_dir = recording_dir or os.path.join(os.path.dirname(test_file_path), 'recordings') self.is_live = self.config.record_mode self.vcr = vcr.VCR( cassette_library_dir=recording_dir, before_record_request=self._process_request_recording, before_record_response=self._process_response_recording, decode_compressed_response=True, record_mode='once' if not self.is_live else 'all', filter_headers=self.FILTER_HEADERS ) self.vcr.register_matcher('query', self._custom_request_query_matcher) self.recording_file = os.path.join( recording_dir, '{}.yaml'.format(recording_name or method_name) ) if self.is_live and os.path.exists(self.recording_file): os.remove(self.recording_file) self.in_recording = self.is_live or not os.path.exists(self.recording_file) self.test_resources_count = 0 self.original_env = os.environ.copy() def setUp(self): super(ReplayableTest, self).setUp() # set up cassette cm = self.vcr.use_cassette(self.recording_file) self.cassette = cm.__enter__() self.addCleanup(cm.__exit__) # set up mock patches if self.in_recording: for patch in self.recording_patches: patch(self) else: for patch in self.replay_patches: patch(self) def tearDown(self): os.environ = self.original_env def _process_request_recording(self, request): if self.disable_recording: return None if self.in_recording: for processor in self.recording_processors: request = processor.process_request(request) if not request: break else: for processor in self.replay_processors: request = processor.process_request(request) if not request: break return request def _process_response_recording(self, response): if self.in_recording: # make header name lower case and filter unwanted headers headers = {} for key in response['headers']: if key.lower() not in self.FILTER_HEADERS: headers[key.lower()] = response['headers'][key] response['headers'] = headers body = response['body']['string'] if body and not isinstance(body, six.string_types): response['body']['string'] = body.decode('utf-8') for processor in self.recording_processors: response = processor.process_response(response) if not response: break else: for processor in self.replay_processors: response = processor.process_response(response) if not response: break return response @classmethod def _custom_request_query_matcher(cls, r1, r2): """ Ensure method, path, and query parameters match. """ from six.moves.urllib_parse import urlparse, parse_qs # pylint: disable=import-error url1 = urlparse(r1.uri) url2 = urlparse(r2.uri) q1 = parse_qs(url1.query) q2 = parse_qs(url2.query) shared_keys = set(q1.keys()).intersection(set(q2.keys())) if len(shared_keys) != len(q1) or len(shared_keys) != len(q2): return False for key in shared_keys: if q1[key][0].lower() != q2[key][0].lower(): return False return True config.py000066400000000000000000000022251314566166100336230ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import configargparse from .const import ENV_LIVE_TEST class TestConfig(object): # pylint: disable=too-few-public-methods def __init__(self, parent_parsers=None, config_file=None): parent_parsers = parent_parsers or [] self.parser = configargparse.ArgumentParser(parents=parent_parsers) self.parser.add_argument( '-c', '--config', is_config_file=True, default=config_file, help='Path to a configuration file in YAML format.' ) self.parser.add_argument( '-l', '--live-mode', action='store_true', dest='live_mode', env_var=ENV_LIVE_TEST, help='Activate "live" recording mode for tests.' ) self.args = self.parser.parse_args([]) @property def record_mode(self): return self.args.live_mode const.py000066400000000000000000000013011314566166100334760ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- # Replaced mock values MOCKED_SUBSCRIPTION_ID = '00000000-0000-0000-0000-000000000000' MOCKED_TENANT_ID = '00000000-0000-0000-0000-000000000000' # Configuration environment variable ENV_COMMAND_COVERAGE = 'AZURE_TEST_COMMAND_COVERAGE' ENV_LIVE_TEST = 'AZURE_TEST_RUN_LIVE' ENV_SKIP_ASSERT = 'AZURE_TEST_SKIP_ASSERT' ENV_TEST_DIAGNOSE = 'AZURE_TEST_DIAGNOSE' decorators.py000066400000000000000000000014361314566166100345260ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # 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 unittest from .const import ENV_LIVE_TEST def live_only(): return unittest.skipUnless( os.environ.get(ENV_LIVE_TEST, False), 'This is a live only test. A live test will bypass all vcrpy components.') def record_only(): return unittest.skipUnless( not os.environ.get(ENV_LIVE_TEST, False), 'This test is excluded from being run live. To force a recording, please remove the recording file.') exceptions.py000066400000000000000000000011011314566166100345270ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- class AzureTestError(Exception): def __init__(self, error_message): message = 'An error caused by the Azure test harness failed the test: {}' super(AzureTestError, self).__init__(message.format(error_message)) patches.py000066400000000000000000000022321314566166100340030ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- from .exceptions import AzureTestError def patch_time_sleep_api(unit_test): def _time_sleep_skip(*_): return mock_in_unit_test(unit_test, 'time.sleep', _time_sleep_skip) def patch_long_run_operation_delay(unit_test): def _shortcut_long_run_operation(*args, **kwargs): # pylint: disable=unused-argument return mock_in_unit_test(unit_test, 'msrestazure.azure_operation.AzureOperationPoller._delay', _shortcut_long_run_operation) def mock_in_unit_test(unit_test, target, replacement): import mock import unittest if not isinstance(unit_test, unittest.TestCase): raise AzureTestError('Patches can be only called from a unit test') mp = mock.patch(target, replacement) mp.__enter__() unit_test.addCleanup(mp.__exit__) preparers.py000066400000000000000000000123021314566166100343560ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import contextlib import inspect import functools from .base import ReplayableTest from .utilities import create_random_name from .recording_processors import RecordingProcessor # Core Utility class AbstractPreparer(object): def __init__(self, name_prefix, name_len, disable_recording=False): self.name_prefix = name_prefix self.name_len = name_len self.resource_moniker = None self.resource_random_name = None self.test_class_instance = None self.live_test = False self.disable_recording = disable_recording def __call__(self, fn): def _preparer_wrapper(test_class_instance, **kwargs): self.live_test = not isinstance(test_class_instance, ReplayableTest) self.test_class_instance = test_class_instance if self.live_test or test_class_instance.in_recording: resource_name = self.random_name if not self.live_test and isinstance(self, RecordingProcessor): test_class_instance.recording_processors.append(self) else: resource_name = self.moniker with self.override_disable_recording(): parameter_update = self.create_resource( resource_name, **kwargs ) test_class_instance.addCleanup( lambda: self.remove_resource_with_record_override(resource_name, **kwargs) ) if parameter_update: kwargs.update(parameter_update) if not is_preparer_func(fn): # the next function is the actual test function. the kwargs need to be trimmed so # that parameters which are not required will not be passed to it. args, _, kw, _ = inspect.getargspec(fn) # pylint: disable=deprecated-method if kw is None: args = set(args) for key in [k for k in kwargs if k not in args]: del kwargs[key] fn(test_class_instance, **kwargs) setattr(_preparer_wrapper, '__is_preparer', True) functools.update_wrapper(_preparer_wrapper, fn) return _preparer_wrapper @contextlib.contextmanager def override_disable_recording(self): if hasattr(self.test_class_instance, 'disable_recording'): orig_enabled = self.test_class_instance.disable_recording self.test_class_instance.disable_recording = self.disable_recording yield self.test_class_instance.disable_recording = orig_enabled else: yield @property def moniker(self): if not self.resource_moniker: self.test_class_instance.test_resources_count += 1 self.resource_moniker = '{}{:06}'.format(self.name_prefix, self.test_class_instance.test_resources_count) return self.resource_moniker def create_random_name(self): return create_random_name(self.name_prefix, self.name_len) @property def random_name(self): if not self.resource_random_name: self.resource_random_name = self.create_random_name() return self.resource_random_name def create_resource(self, name, **kwargs): # pylint: disable=unused-argument,no-self-use return {} def remove_resource(self, name, **kwargs): # pylint: disable=unused-argument pass def remove_resource_with_record_override(self, name, **kwargs): with self.override_disable_recording(): self.remove_resource(name, **kwargs) class SingleValueReplacer(RecordingProcessor): # pylint: disable=no-member def process_request(self, request): from six.moves.urllib_parse import quote_plus # pylint: disable=import-error if self.random_name in request.uri: request.uri = request.uri.replace(self.random_name, self.moniker) elif quote_plus(self.random_name) in request.uri: request.uri = request.uri.replace(quote_plus(self.random_name), quote_plus(self.moniker)) if request.body: body = str(request.body) if self.random_name in body: request.body = body.replace(self.random_name, self.moniker) return request def process_response(self, response): if response['body']['string']: response['body']['string'] = response['body']['string'].replace(self.random_name, self.moniker) self.replace_header(response, 'location', self.random_name, self.moniker) self.replace_header(response, 'azure-asyncoperation', self.random_name, self.moniker) return response # Utility def is_preparer_func(fn): return getattr(fn, '__is_preparer', False) recording_processors.py000066400000000000000000000152451314566166100366220ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- class RecordingProcessor(object): def process_request(self, request): # pylint: disable=no-self-use return request def process_response(self, response): # pylint: disable=no-self-use return response @classmethod def replace_header(cls, entity, header, old, new): cls.replace_header_fn(entity, header, lambda v: v.replace(old, new)) @classmethod def replace_header_fn(cls, entity, header, replace_fn): # Loop over the headers to find the one we want case insensitively, # but we don't want to modify the case of original header key. for key, values in entity['headers'].items(): if key.lower() == header.lower(): entity['headers'][key] = [replace_fn(v) for v in values] class SubscriptionRecordingProcessor(RecordingProcessor): def __init__(self, replacement): self._replacement = replacement def process_request(self, request): request.uri = self._replace_subscription_id(request.uri) if request.body: request.body = self._replace_subscription_id(request.body.decode()).encode() return request def process_response(self, response): if response['body']['string']: response['body']['string'] = self._replace_subscription_id(response['body']['string']) self.replace_header_fn(response, 'location', self._replace_subscription_id) self.replace_header_fn(response, 'azure-asyncoperation', self._replace_subscription_id) return response def _replace_subscription_id(self, val): import re # subscription presents in all api call retval = re.sub('/(subscriptions)/([^/]+)/', r'/\1/{}/'.format(self._replacement), val, flags=re.IGNORECASE) # subscription is also used in graph call retval = re.sub('https://(graph.windows.net)/([^/]+)/', r'https://\1/{}/'.format(self._replacement), retval, flags=re.IGNORECASE) return retval class LargeRequestBodyProcessor(RecordingProcessor): def __init__(self, max_request_body=128): self._max_request_body = max_request_body def process_request(self, request): if request.body and len(request.body) > self._max_request_body * 1024: request.body = '!!! The request body has been omitted from the recording because its ' \ 'size {} is larger than {}KB. !!!'.format(len(request.body), self._max_request_body) return request class LargeResponseBodyProcessor(RecordingProcessor): control_flag = '' def __init__(self, max_response_body=128): self._max_response_body = max_response_body def process_response(self, response): length = len(response['body']['string'] or '') if length > self._max_response_body * 1024: response['body']['string'] = \ "!!! The response body has been omitted from the recording because it is larger " \ "than {} KB. It will be replaced with blank content of {} bytes while replay. " \ "{}{}".format(self._max_response_body, length, self.control_flag, length) return response class LargeResponseBodyReplacer(RecordingProcessor): def process_response(self, response): import six body = response['body']['string'] # backward compatibility. under 2.7 response body is unicode, under 3.5 response body is # bytes. when set the value back, the same type must be used. body_is_string = isinstance(body, six.string_types) content_in_string = (response['body']['string'] or b'').decode('utf-8') index = content_in_string.find(LargeResponseBodyProcessor.control_flag) if index > -1: length = int(content_in_string[index + len(LargeResponseBodyProcessor.control_flag):]) if body_is_string: response['body']['string'] = '0' * length else: response['body']['string'] = bytes([0] * length) return response class OAuthRequestResponsesFilter(RecordingProcessor): """Remove oauth authentication requests and responses from recording.""" def process_request(self, request): # filter request like: # GET https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/token import re if not re.match('https://login.microsoftonline.com/([^/]+)/oauth2/token', request.uri): return request class DeploymentNameReplacer(RecordingProcessor): """Replace the random deployment name with a fixed mock name.""" def process_request(self, request): import re request.uri = re.sub('/deployments/([^/?]+)', '/deployments/mock-deployment', request.uri) return request class AccessTokenReplacer(RecordingProcessor): """Replace the access token for service principal authentication in a response body.""" def __init__(self, replacement='fake_token'): self._replacement = replacement def process_response(self, response): import json try: body = json.loads(response['body']['string']) body['access_token'] = self._replacement except (KeyError, ValueError): return response response['body']['string'] = json.dumps(body) return response class GeneralNameReplacer(RecordingProcessor): def __init__(self): self.names_name = [] def register_name_pair(self, old, new): self.names_name.append((old, new)) def process_request(self, request): for old, new in self.names_name: request.uri = request.uri.replace(old, new) if request.body: body = str(request.body) if old in body: request.body = body.replace(old, new) return request def process_response(self, response): for old, new in self.names_name: if response['body']['string']: response['body']['string'] = response['body']['string'].replace(old, new) self.replace_header(response, 'location', old, new) self.replace_header(response, 'azure-asyncoperation', old, new) return response 000077500000000000000000000000001314566166100331455ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests__init__.py000066400000000000000000000005311314566166100352550ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- test_config.py000066400000000000000000000022401314566166100360210ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests# -------------------------------------------------------------------------------------------- # 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 tempfile import unittest try: import mock except ImportError: from unittest import mock from azure_devtools.scenario_tests.const import ENV_LIVE_TEST from azure_devtools.scenario_tests.config import TestConfig class TestScenarioConfig(unittest.TestCase): def setUp(self): with tempfile.NamedTemporaryFile(mode='w', delete=False) as cfgfile: cfgfile.write('live-mode: yes') self.cfgfile = cfgfile.name def tearDown(self): os.remove(self.cfgfile) def test_env_var(self): with mock.patch.dict('os.environ', {ENV_LIVE_TEST: 'yes'}): config = TestConfig() self.assertIs(config.record_mode, True) def test_config_file(self): config = TestConfig(config_file=self.cfgfile) self.assertIs(config.record_mode, True) test_integration_test_base.py000066400000000000000000000046701314566166100411410ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import os.path import unittest from azure_devtools.scenario_tests.base import IntegrationTestBase, LiveTest class TestIntegrationTestBase(unittest.TestCase): def test_integration_test_default_constructor(self): class MockTest(IntegrationTestBase): def __init__(self): super(MockTest, self).__init__('sample_test') def sample_test(self): pass tb = MockTest() random_name = tb.create_random_name('example', 90) self.assertEqual(len(random_name), 90) self.assertTrue(random_name.startswith('example')) random_file = tb.create_temp_file(size_kb=16, full_random=False) self.addCleanup(lambda: os.remove(random_file)) self.assertTrue(os.path.isfile(random_file)) self.assertEqual(os.path.getsize(random_file), 16 * 1024) self.assertEqual(len(tb._cleanups), 1) # pylint: disable=protected-access with open(random_file, 'rb') as fq: # the file is blank self.assertFalse(any(b for b in fq.read(16 * 1024) if b != '\x00')) random_file_2 = tb.create_temp_file(size_kb=8, full_random=True) self.addCleanup(lambda: os.remove(random_file_2)) self.assertTrue(os.path.isfile(random_file_2)) self.assertEqual(os.path.getsize(random_file_2), 8 * 1024) self.assertEqual(len(tb._cleanups), 2) # pylint: disable=protected-access with open(random_file_2, 'rb') as fq: # the file is blank self.assertTrue(any(b for b in fq.read(8 * 1024) if b != '\x00')) random_dir = tb.create_temp_dir() self.addCleanup(lambda: os.rmdir(random_dir)) self.assertTrue(os.path.isdir(random_dir)) self.assertEqual(len(tb._cleanups), 3) # pylint: disable=protected-access def test_live_test_default_constructor(self): class MockTest(LiveTest): def __init__(self): super(MockTest, self).__init__('sample_test') def sample_test(self): pass self.assertIsNone(MockTest().run(), 'The live test is not skipped as expected') test_recording_processor.py000066400000000000000000000136671314566166100406460ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import json try: import unittest.mock as mock except ImportError: import mock import unittest import uuid from azure_devtools.scenario_tests.recording_processors import ( RecordingProcessor, SubscriptionRecordingProcessor, AccessTokenReplacer ) class TestRecordingProcessors(unittest.TestCase): def test_recording_processor_base_class(self): rp = RecordingProcessor() request_sample = {'url': 'https://www.bing,com', 'headers': {'beta': ['value_1', 'value_2']}} response_sample = {'body': 'something', 'headers': {'charlie': ['value_3']}} self.assertIs(request_sample, rp.process_request(request_sample)) # reference equality self.assertIs(response_sample, rp.process_response(response_sample)) rp.replace_header(request_sample, 'beta', 'value_1', 'replaced_1') self.assertSequenceEqual(request_sample['headers']['beta'], ['replaced_1', 'value_2']) rp.replace_header(request_sample, 'Beta', 'replaced_1', 'replaced_2') # case insensitive self.assertSequenceEqual(request_sample['headers']['beta'], ['replaced_2', 'value_2']) rp.replace_header(request_sample, 'alpha', 'replaced_1', 'replaced_2') # ignore KeyError self.assertSequenceEqual(request_sample['headers']['beta'], ['replaced_2', 'value_2']) rp.replace_header_fn(request_sample, 'beta', lambda v: 'customized') self.assertSequenceEqual(request_sample['headers']['beta'], ['customized', 'customized']) def test_access_token_processor(self): replaced_subscription_id = 'test_fake_token' rp = AccessTokenReplacer(replaced_subscription_id) TOKEN_STR = '{"token_type": "Bearer", "resource": "url", "access_token": "real_token"}' token_response_sample = {'body': {'string': TOKEN_STR}} self.assertEqual(json.loads(rp.process_response(token_response_sample)['body']['string'])['access_token'], replaced_subscription_id) no_token_response_sample = {'body': {'string': '{"location": "westus"}'}} self.assertDictEqual(rp.process_response(no_token_response_sample), no_token_response_sample) @staticmethod def _mock_subscription_request_body(mock_sub_id): return json.dumps({ "location": "westus", "properties": { "ipConfigurations": [ { "properties": { "subnet": {"id": "/Subscriptions/{}/resourceGroups/etc".format(mock_sub_id)}, "name": "azure-sample-ip-config" } }, ] } }).encode() def test_subscription_recording_processor_for_request(self): replaced_subscription_id = str(uuid.uuid4()) rp = SubscriptionRecordingProcessor(replaced_subscription_id) uri_templates = ['https://management.azure.com/subscriptions/{}/providers/Microsoft.ContainerRegistry/' 'checkNameAvailability?api-version=2017-03-01', 'https://graph.windows.net/{}/applications?api-version=1.6'] for template in uri_templates: mock_sub_id = str(uuid.uuid4()) mock_request = mock.Mock() mock_request.uri = template.format(mock_sub_id) mock_request.body = self._mock_subscription_request_body(mock_sub_id) rp.process_request(mock_request) self.assertEqual(mock_request.uri, template.format(replaced_subscription_id)) self.assertEqual(mock_request.body, self._mock_subscription_request_body(replaced_subscription_id)) def test_subscription_recording_processor_for_response(self): replaced_subscription_id = str(uuid.uuid4()) rp = SubscriptionRecordingProcessor(replaced_subscription_id) uri_templates = ['https://management.azure.com/subscriptions/{}/providers/Microsoft.ContainerRegistry/' 'checkNameAvailability?api-version=2017-03-01', 'https://graph.Windows.net/{}/applications?api-version=1.6'] location_header_template = 'https://graph.windows.net/{}/directoryObjects/' \ 'f604c53a-aa21-44d5-a41f-c1ef0b5304bd/Microsoft.DirectoryServices.Application' asyncoperation_header_template = 'https://management.azure.com/subscriptions/{}/resourceGroups/' \ 'clitest.rg000001/providers/Microsoft.Sql/servers/clitestserver000002/' \ 'databases/cliautomationdb01/azureAsyncOperation/' \ '6ec6196b-fbaa-415f-8c1a-6cb634a96cb2?api-version=2014-04-01-Preview' for template in uri_templates: mock_sub_id = str(uuid.uuid4()) mock_response = dict({'body': {}}) mock_response['body']['string'] = template.format(mock_sub_id) mock_response['headers'] = {'Location': [location_header_template.format(mock_sub_id)], 'azure-asyncoperation': [asyncoperation_header_template.format(mock_sub_id)]} rp.process_response(mock_response) self.assertEqual(mock_response['body']['string'], template.format(replaced_subscription_id)) self.assertSequenceEqual(mock_response['headers']['Location'], [location_header_template.format(replaced_subscription_id)]) self.assertSequenceEqual(mock_response['headers']['azure-asyncoperation'], [asyncoperation_header_template.format(replaced_subscription_id)]) test_utilities.py000066400000000000000000000072001314566166100365700ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests/tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import unittest from azure_devtools.scenario_tests.utilities import create_random_name, get_sha1_hash class TestUtilityFunctions(unittest.TestCase): def test_create_random_name_default_value(self): default_generated_name = create_random_name() self.assertTrue(default_generated_name.startswith('aztest')) self.assertEqual(24, len(default_generated_name)) self.assertTrue(isinstance(default_generated_name, str)) def test_create_random_name_randomness(self): self.assertEqual(100, len(set([create_random_name() for _ in range(100)]))) def test_create_random_name_customization(self): customized_name = create_random_name(prefix='pauline', length=61) self.assertTrue(customized_name.startswith('pauline')) self.assertEqual(61, len(customized_name)) self.assertTrue(isinstance(customized_name, str)) def test_create_random_name_exception_long_prefix(self): prefix = 'prefix-too-long' with self.assertRaises(ValueError) as cm: create_random_name(prefix, length=len(prefix)-1) self.assertEqual(str(cm.exception), 'The length of the prefix must not be longer than random name length') self.assertTrue(create_random_name(prefix, length=len(prefix)+4).startswith(prefix)) def test_create_random_name_exception_not_enough_space_for_randomness(self): prefix = 'prefix-too-long' for i in range(4): with self.assertRaises(ValueError) as cm: create_random_name(prefix, length=len(prefix) + i) self.assertEqual(str(cm.exception), 'The randomized part of the name is shorter than 4, which may not be ' 'able to offer enough randomness') def test_get_sha1_hash(self): import tempfile with tempfile.NamedTemporaryFile() as f: content = b""" All the world's a stage, And all the men and women merely players; They have their exits and their entrances, And one man in his time plays many parts, His acts being seven ages. At first, the infant, Mewling and puking in the nurse's arms. Then the whining schoolboy, with his satchel And shining morning face, creeping like snail Unwillingly to school. And then the lover, Sighing like furnace, with a woeful ballad Made to his mistress' eyebrow. Then a soldier, Full of strange oaths and bearded like the pard, Jealous in honor, sudden and quick in quarrel, Seeking the bubble reputation Even in the cannon's mouth. And then the justice, In fair round belly with good capon lined, With eyes severe and beard of formal cut, Full of wise saws and modern instances; And so he plays his part. The sixth age shifts Into the lean and slippered pantaloon, With spectacles on nose and pouch on side; His youthful hose, well saved, a world too wide For his shrunk shank, and his big manly voice, Turning again toward childish treble, pipes And whistles in his sound. Last scene of all, That ends this strange eventful history, Is second childishness and mere oblivion, Sans teeth, sans eyes, sans taste, sans everything. William Shakespeare """ f.write(content) f.seek(0) hash_value = get_sha1_hash(f.name) self.assertEqual('6487bbdbd848686338d729e6076da1a795d1ae747642bf906469c6ccd9e642f9', hash_value) utilities.py000066400000000000000000000023471314566166100343760ustar00rootroot00000000000000azure-python-devtools-9c3ceae5fc673d5d96f0529bac531eba9527a762/src/azure_devtools/scenario_tests# -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import hashlib import math import os import base64 def create_random_name(prefix='aztest', length=24): if len(prefix) > length: raise ValueError('The length of the prefix must not be longer than random name length') padding_size = length - len(prefix) if padding_size < 4: raise ValueError('The randomized part of the name is shorter than 4, which may not be able to offer enough ' 'randomness') random_bytes = os.urandom(int(math.ceil(float(padding_size) / 8) * 5)) random_padding = base64.b32encode(random_bytes)[:padding_size] return str(prefix + random_padding.decode().lower()) def get_sha1_hash(file_path): sha1 = hashlib.sha256() with open(file_path, 'rb') as f: while True: data = f.read(65536) if not data: break sha1.update(data) return sha1.hexdigest()