Work-around for import order (#4103)

This commit is contained in:
Bert Blommers 2021-11-09 20:39:31 -01:00 committed by GitHub
parent 0e6922a4a4
commit c62a34528e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 11 deletions

View File

@ -1,5 +1,8 @@
.. _contributing urls: .. _contributing urls:
.. role:: raw-html(raw)
:format: html
*********************** ***********************
Intercepting URL's Intercepting URL's
*********************** ***********************
@ -8,8 +11,7 @@ Intercepting URL's
Determining which URLs to intercept Determining which URLs to intercept
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In order for Moto to know which requests to intercept, Moto needs to know which URLs to intercept. In order for Moto to know which requests to intercept, Moto needs to know which URLs to intercept. :raw-html:`<br />` But how do we know which URL's should be intercepted? There are a few ways of doing it:
| But how do we know which URL's should be intercepted? There are a few ways of doing it:
- For an existing service, copy/paste the url-path for an existing feature and cross your fingers and toes - For an existing service, copy/paste the url-path for an existing feature and cross your fingers and toes
- Use the service model that is used by botocore: https://github.com/boto/botocore/tree/develop/botocore/data - Use the service model that is used by botocore: https://github.com/boto/botocore/tree/develop/botocore/data

View File

@ -200,18 +200,18 @@ How do I avoid tests from mutating my real infrastructure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You need to ensure that the mocks are actually in place. You need to ensure that the mocks are actually in place.
#. Ensure that your tests have dummy environment variables set up: #. Ensure that your tests have dummy environment variables set up:
.. sourcecode:: bash .. sourcecode:: bash
export AWS_ACCESS_KEY_ID='testing' export AWS_ACCESS_KEY_ID='testing'
export AWS_SECRET_ACCESS_KEY='testing' export AWS_SECRET_ACCESS_KEY='testing'
export AWS_SECURITY_TOKEN='testing' export AWS_SECURITY_TOKEN='testing'
export AWS_SESSION_TOKEN='testing' export AWS_SESSION_TOKEN='testing'
#. **VERY IMPORTANT**: ensure that you have your mocks set up *BEFORE* your `boto3` client is established. #. **VERY IMPORTANT**: ensure that you have your mocks set up *BEFORE* your `boto3` client is established.
This can typically happen if you import a module that has a `boto3` client instantiated outside of a function. This can typically happen if you import a module that has a `boto3` client instantiated outside of a function.
See the pesky imports section below on how to work around this. See the pesky imports section below on how to work around this.
Example on usage Example on usage
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
@ -269,6 +269,29 @@ Example:
some_func() # The mock has been established from the "s3" pytest fixture, so this function that uses some_func() # The mock has been established from the "s3" pytest fixture, so this function that uses
# a package-level S3 client will properly use the mock and not reach out to AWS. # a package-level S3 client will properly use the mock and not reach out to AWS.
Patching the client or resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If it is not possible to rearrange imports, we can patch the boto3-client or resource after the mock has started. See the following code sample:
.. sourcecode:: python
# The client can come from an import, an __init__-file, wherever..
client = boto3.client("s3")
s3 = boto3.resource("s3")
@mock_s3
def test_mock_works_with_client_or_resource_created_outside():
from moto.core import patch_client, patch_resource
patch_client(outside_client)
patch_resource(s3)
assert client.list_buckets()["Buckets"] == []
assert list(s3.buckets.all()) == []
This will ensure that the boto3 requests are still mocked.
Other caveats Other caveats
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
For Tox, Travis CI, and other build systems, you might need to also perform a `touch ~/.aws/credentials` For Tox, Travis CI, and other build systems, you might need to also perform a `touch ~/.aws/credentials`

View File

@ -1,5 +1,6 @@
from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa
from .models import CloudFormationModel # noqa from .models import CloudFormationModel # noqa
from .models import patch_client, patch_resource # noqa
from .responses import ActionAuthenticatorMixin from .responses import ActionAuthenticatorMixin
moto_api_backends = {"global": moto_api_backend} moto_api_backends = {"global": moto_api_backend}

View File

@ -1,3 +1,5 @@
import botocore
import boto3
import functools import functools
import inspect import inspect
import os import os
@ -407,6 +409,40 @@ botocore_stubber = BotocoreStubber()
BUILTIN_HANDLERS.append(("before-send", botocore_stubber)) BUILTIN_HANDLERS.append(("before-send", botocore_stubber))
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")
def not_implemented_callback(request): def not_implemented_callback(request):
status = 400 status = 400
headers = {} headers = {}

View File

@ -0,0 +1,109 @@
import boto3
import pytest
import sure # noqa # pylint: disable=unused-import
from moto import mock_s3
from moto import settings
from os import environ
from unittest import SkipTest
@pytest.fixture(scope="function")
def aws_credentials():
if settings.TEST_SERVER_MODE:
raise SkipTest("No point in testing this in ServerMode.")
"""Mocked AWS Credentials for moto."""
environ["AWS_ACCESS_KEY_ID"] = "testing"
environ["AWS_SECRET_ACCESS_KEY"] = "testing"
environ["AWS_SECURITY_TOKEN"] = "testing"
environ["AWS_SESSION_TOKEN"] = "testing"
def test_mock_works_with_client_created_inside(aws_credentials):
m = mock_s3()
m.start()
client = boto3.client("s3", region_name="us-east-1")
b = client.list_buckets()
b["Buckets"].should.be.empty
m.stop()
def test_mock_works_with_client_created_outside(aws_credentials):
# Create the boto3 client first
outside_client = boto3.client("s3", region_name="us-east-1")
# Start the mock afterwards - which does not mock an already created client
m = mock_s3()
m.start()
# So remind us to mock this client
from moto.core import patch_client
patch_client(outside_client)
b = outside_client.list_buckets()
b["Buckets"].should.be.empty
m.stop()
def test_mock_works_with_resource_created_outside(aws_credentials):
# Create the boto3 client first
outside_resource = boto3.resource("s3", region_name="us-east-1")
# Start the mock afterwards - which does not mock an already created resource
m = mock_s3()
m.start()
# So remind us to mock this client
from moto.core import patch_resource
patch_resource(outside_resource)
b = list(outside_resource.buckets.all())
b.should.be.empty
m.stop()
def test_patch_client_does_not_work_for_random_parameters():
from moto.core import patch_client
with pytest.raises(Exception, match="Argument sth should be of type boto3.client"):
patch_client("sth")
def test_patch_resource_does_not_work_for_random_parameters():
from moto.core import patch_resource
with pytest.raises(
Exception, match="Argument sth should be of type boto3.resource"
):
patch_resource("sth")
class ImportantBusinessLogic:
def __init__(self):
self._s3 = boto3.client("s3", region_name="us-east-1")
def do_important_things(self):
return self._s3.list_buckets()["Buckets"]
def test_mock_works_when_replacing_client(aws_credentials):
logic = ImportantBusinessLogic()
m = mock_s3()
m.start()
# This will fail, as the S3 client was created before the mock was initialized
try:
logic.do_important_things()
except Exception as e:
str(e).should.contain("InvalidAccessKeyId")
client_initialized_after_mock = boto3.client("s3", region_name="us-east-1")
logic._s3 = client_initialized_after_mock
# This will work, as we now use a properly mocked client
logic.do_important_things().should.equal([])
m.stop()