moto/moto/core/models.py

406 lines
13 KiB
Python
Raw Normal View History

2013-02-18 21:09:40 +00:00
import functools
import inspect
import itertools
import os
2013-02-18 21:09:40 +00:00
import re
import unittest
from types import FunctionType
from unittest.mock import patch
2021-08-05 16:59:25 +00:00
import boto3
import botocore
import responses
2020-06-27 14:11:41 +00:00
from botocore.config import Config
from botocore.handlers import BUILTIN_HANDLERS
2013-02-18 21:09:40 +00:00
from moto import settings
from .botocore_stubber import BotocoreStubber
from .custom_responses_mock import (
get_response_mock,
CallbackResponse,
not_implemented_callback,
reset_responses_mock,
)
from .utils import convert_flask_to_responses_response
2013-02-18 21:09:40 +00:00
2019-12-17 02:25:20 +00:00
ACCOUNT_ID = os.environ.get("MOTO_ACCOUNT_ID", "123456789012")
def _get_default_account_id():
return ACCOUNT_ID
account_id_resolver = _get_default_account_id
def get_account_id():
return account_id_resolver()
2021-07-26 06:40:39 +00:00
class BaseMockAWS:
nested_count = 0
mocks_active = False
def __init__(self, backends):
2020-01-17 01:08:06 +00:00
from moto.instance_metadata import instance_metadata_backend
from moto.moto_api._internal.models import moto_api_backend
2020-01-17 01:08:06 +00:00
self.backends = backends
self.backends_for_urls = {}
default_backends = {
2020-01-17 01:08:06 +00:00
"instance_metadata": instance_metadata_backend,
"moto_api": moto_api_backend,
}
2021-12-24 21:02:45 +00:00
if "us-east-1" in self.backends:
# We only need to know the URL for a single region - they will be the same everywhere
2021-12-24 21:02:45 +00:00
self.backends_for_urls["us-east-1"] = self.backends["us-east-1"]
elif "global" in self.backends:
2021-12-24 21:02:45 +00:00
# If us-east-1 is not available, it's probably a global service
self.backends_for_urls["global"] = self.backends["global"]
self.backends_for_urls.update(default_backends)
self.FAKE_KEYS = {
2019-10-31 15:44:26 +00:00
"AWS_ACCESS_KEY_ID": "foobar_key",
"AWS_SECRET_ACCESS_KEY": "foobar_secret",
}
self.ORIG_KEYS = {}
self.default_session_mock = patch("boto3.DEFAULT_SESSION", None)
if self.__class__.nested_count == 0:
2017-02-16 03:35:45 +00:00
self.reset()
2013-02-28 03:25:15 +00:00
2015-06-27 23:01:01 +00:00
def __call__(self, func, reset=True):
if inspect.isclass(func):
return self.decorate_class(func)
2015-06-27 23:01:01 +00:00
return self.decorate_callable(func, reset)
2013-02-28 03:25:15 +00:00
def __enter__(self):
self.start()
2019-07-10 01:31:43 +00:00
return self
2013-02-28 03:25:15 +00:00
def __exit__(self, *args):
self.stop()
2015-04-03 03:40:40 +00:00
def start(self, reset=True):
if not self.__class__.mocks_active:
self.default_session_mock.start()
self.mock_env_variables()
self.__class__.mocks_active = True
self.__class__.nested_count += 1
2015-04-03 03:40:40 +00:00
if reset:
for backend in self.backends.values():
backend.reset()
self.enable_patching(reset)
2013-02-28 03:25:15 +00:00
def stop(self):
self.__class__.nested_count -= 1
if self.__class__.nested_count < 0:
2019-10-31 15:44:26 +00:00
raise RuntimeError("Called stop() before start().")
2017-02-20 19:31:19 +00:00
if self.__class__.nested_count == 0:
if self.__class__.mocks_active:
try:
self.default_session_mock.stop()
except RuntimeError:
# We only need to check for this exception in Python 3.6 and 3.7
# https://bugs.python.org/issue36366
pass
self.unmock_env_variables()
self.__class__.mocks_active = False
2017-02-20 19:31:19 +00:00
self.disable_patching()
2013-02-28 03:25:15 +00:00
2015-06-27 23:01:01 +00:00
def decorate_callable(self, func, reset):
2013-02-28 03:25:15 +00:00
def wrapper(*args, **kwargs):
2015-06-27 23:01:01 +00:00
self.start(reset=reset)
try:
2013-02-28 03:25:15 +00:00
result = func(*args, **kwargs)
2015-06-27 23:01:01 +00:00
finally:
self.stop()
2013-02-28 03:25:15 +00:00
return result
2019-10-31 15:44:26 +00:00
2013-02-28 03:25:15 +00:00
functools.update_wrapper(wrapper, func)
wrapper.__wrapped__ = func
2013-02-28 03:25:15 +00:00
return wrapper
def decorate_class(self, klass):
direct_methods = get_direct_methods_of(klass)
defined_classes = set(
x for x, y in klass.__dict__.items() if inspect.isclass(y)
)
# Get a list of all userdefined superclasses
superclasses = [
c for c in klass.__mro__ if c not in [unittest.TestCase, object]
]
# Get a list of all userdefined methods
supermethods = list(
itertools.chain(*[get_direct_methods_of(c) for c in superclasses])
)
# Check whether the user has overridden the setUp-method
has_setup_method = (
("setUp" in supermethods and unittest.TestCase in klass.__mro__)
or "setup" in supermethods
or "setup_method" in supermethods
)
for attr in itertools.chain(direct_methods, defined_classes):
if attr.startswith("_"):
continue
attr_value = getattr(klass, attr)
if not hasattr(attr_value, "__call__"):
continue
2021-09-28 09:53:05 +00:00
if not hasattr(attr_value, "__name__"):
continue
# Check if this is a classmethod. If so, skip patching
if inspect.ismethod(attr_value) and attr_value.__self__ is klass:
continue
2018-09-05 09:39:09 +00:00
# Check if this is a staticmethod. If so, skip patching
for cls in inspect.getmro(klass):
if attr_value.__name__ not in cls.__dict__:
continue
bound_attr_value = cls.__dict__[attr_value.__name__]
if not isinstance(bound_attr_value, staticmethod):
break
else:
# It is a staticmethod, skip patching
continue
try:
# Special case for UnitTests-class
is_test_method = attr.startswith(unittest.TestLoader.testMethodPrefix)
should_reset = False
if attr in ["setUp", "setup_method"]:
should_reset = True
elif not has_setup_method and is_test_method:
should_reset = True
else:
# Method is unrelated to the test setup
# Method is a test, but was already reset while executing the setUp-method
pass
setattr(klass, attr, self(attr_value, reset=should_reset))
except TypeError:
# Sometimes we can't set this for built-in types
continue
return klass
def mock_env_variables(self):
# "Mock" the AWS credentials as they can't be mocked in Botocore currently
# self.env_variables_mocks = mock.patch.dict(os.environ, FAKE_KEYS)
# self.env_variables_mocks.start()
for k, v in self.FAKE_KEYS.items():
self.ORIG_KEYS[k] = os.environ.get(k, None)
os.environ[k] = v
def unmock_env_variables(self):
# This doesn't work in Python2 - for some reason, unmocking clears the entire os.environ dict
# Obviously bad user experience, and also breaks pytest - as it uses PYTEST_CURRENT_TEST as an env var
# self.env_variables_mocks.stop()
for k, v in self.ORIG_KEYS.items():
if v:
os.environ[k] = v
else:
del os.environ[k]
2013-02-28 03:25:15 +00:00
def get_direct_methods_of(klass):
return set(
x
for x, y in klass.__dict__.items()
if isinstance(y, (FunctionType, classmethod, staticmethod))
)
2019-10-31 15:44:26 +00:00
RESPONSES_METHODS = [
responses.GET,
responses.DELETE,
responses.HEAD,
responses.OPTIONS,
responses.PATCH,
responses.POST,
responses.PUT,
]
2017-02-16 03:35:45 +00:00
2019-10-31 15:44:26 +00:00
botocore_mock = responses.RequestsMock(
assert_all_requests_are_fired=False,
target="botocore.vendored.requests.adapters.HTTPAdapter.send",
)
responses_mock = get_response_mock()
2019-10-31 15:44:26 +00:00
BOTOCORE_HTTP_METHODS = ["GET", "DELETE", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
botocore_stubber = BotocoreStubber()
2019-10-31 15:44:26 +00:00
BUILTIN_HANDLERS.append(("before-send", botocore_stubber))
2018-10-01 16:45:12 +00:00
2021-11-09 21:39:31 +00:00
def patch_client(client):
"""
Explicitly patch a boto3-client
"""
"""
Adding the botocore_stubber to the BUILTIN_HANDLERS, as above, will mock everything as long as the import ordering is correct
- user: start mock_service decorator
- system: imports core.model
- system: adds the stubber to the BUILTIN_HANDLERS
- user: create a boto3 client - which will use the BUILTIN_HANDLERS
But, if for whatever reason the imports are wrong and the client is created first, it doesn't know about our stub yet
This method can be used to tell a client that it needs to be mocked, and append the botocore_stubber after creation
:param client:
:return:
"""
if isinstance(client, botocore.client.BaseClient):
client.meta.events.register("before-send", botocore_stubber)
else:
raise Exception(f"Argument {client} should be of type boto3.client")
def patch_resource(resource):
"""
Explicitly patch a boto3-resource
"""
if hasattr(resource, "meta") and isinstance(
resource.meta, boto3.resources.factory.ResourceMeta
):
patch_client(resource.meta.client)
else:
raise Exception(f"Argument {resource} should be of type boto3.resource")
class BotocoreEventMockAWS(BaseMockAWS):
def reset(self):
botocore_stubber.reset()
reset_responses_mock(responses_mock)
def enable_patching(self, reset=True): # pylint: disable=unused-argument
botocore_stubber.enabled = True
for method in BOTOCORE_HTTP_METHODS:
for backend in self.backends_for_urls.values():
for key, value in backend.urls.items():
pattern = re.compile(key)
botocore_stubber.register_response(method, pattern, value)
2019-10-31 15:44:26 +00:00
if not hasattr(responses_mock, "_patcher") or not hasattr(
responses_mock._patcher, "target"
):
2018-10-14 17:58:56 +00:00
responses_mock.start()
for method in RESPONSES_METHODS:
# for backend in default_backends.values():
for backend in self.backends_for_urls.values():
for key, value in backend.urls.items():
responses_mock.add(
CallbackResponse(
method=method,
url=re.compile(key),
callback=convert_flask_to_responses_response(value),
)
)
responses_mock.add(
CallbackResponse(
method=method,
url=re.compile(r"https?://.+\.amazonaws.com/.*"),
callback=not_implemented_callback,
)
)
botocore_mock.add(
CallbackResponse(
method=method,
url=re.compile(r"https?://.+\.amazonaws.com/.*"),
callback=not_implemented_callback,
)
)
2018-10-14 17:58:56 +00:00
def disable_patching(self):
botocore_stubber.enabled = False
self.reset()
2018-10-14 17:58:56 +00:00
try:
responses_mock.stop()
except RuntimeError:
pass
MockAWS = BotocoreEventMockAWS
2017-02-16 03:35:45 +00:00
2017-02-20 19:31:19 +00:00
2017-02-20 23:25:10 +00:00
class ServerModeMockAWS(BaseMockAWS):
def __init__(self, *args, **kwargs):
self.test_server_mode_endpoint = settings.test_server_mode_endpoint()
super().__init__(*args, **kwargs)
2017-02-20 23:25:10 +00:00
def reset(self):
2021-10-05 17:11:07 +00:00
call_reset_api = os.environ.get("MOTO_CALL_RESET_API")
if not call_reset_api or call_reset_api.lower() != "false":
import requests
2019-10-31 15:44:26 +00:00
requests.post(f"{self.test_server_mode_endpoint}/moto-api/reset")
2017-02-20 23:25:10 +00:00
def enable_patching(self, reset=True):
if self.__class__.nested_count == 1 and reset:
2017-02-20 23:25:10 +00:00
# Just started
self.reset()
from boto3 import client as real_boto3_client, resource as real_boto3_resource
def fake_boto3_client(*args, **kwargs):
2020-06-27 18:46:26 +00:00
region = self._get_region(*args, **kwargs)
if region:
if "config" in kwargs:
kwargs["config"].__dict__["user_agent_extra"] += " region/" + region
else:
config = Config(user_agent_extra="region/" + region)
kwargs["config"] = config
2019-10-31 15:44:26 +00:00
if "endpoint_url" not in kwargs:
kwargs["endpoint_url"] = self.test_server_mode_endpoint
2017-02-20 23:25:10 +00:00
return real_boto3_client(*args, **kwargs)
2017-02-24 02:37:43 +00:00
2017-02-20 23:25:10 +00:00
def fake_boto3_resource(*args, **kwargs):
2019-10-31 15:44:26 +00:00
if "endpoint_url" not in kwargs:
kwargs["endpoint_url"] = self.test_server_mode_endpoint
2017-02-20 23:25:10 +00:00
return real_boto3_resource(*args, **kwargs)
self._client_patcher = patch("boto3.client", fake_boto3_client)
self._resource_patcher = patch("boto3.resource", fake_boto3_resource)
2017-02-20 23:25:10 +00:00
self._client_patcher.start()
self._resource_patcher.start()
2020-06-27 18:46:26 +00:00
def _get_region(self, *args, **kwargs):
if "region_name" in kwargs:
return kwargs["region_name"]
if type(args) == tuple and len(args) == 2:
_, region = args
2020-06-27 18:46:26 +00:00
return region
return None
2017-02-20 23:25:10 +00:00
def disable_patching(self):
if self._client_patcher:
self._client_patcher.stop()
self._resource_patcher.stop()
2017-02-24 02:37:43 +00:00
2021-07-26 06:40:39 +00:00
class base_decorator:
2017-02-12 05:22:29 +00:00
mock_backend = MockAWS
def __init__(self, backends):
self.backends = backends
def __call__(self, func=None):
if settings.TEST_SERVER_MODE:
mocked_backend = ServerModeMockAWS(self.backends)
else:
mocked_backend = self.mock_backend(self.backends)
2017-02-20 23:25:10 +00:00
2017-02-12 05:22:29 +00:00
if func:
return mocked_backend(func)
2017-02-12 05:22:29 +00:00
else:
return mocked_backend