Skip to content

Commit

Permalink
fix: Adjust moto fixtures to work correctly with scopes and to propag… (
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin committed Jul 12, 2023
1 parent 14c09b8 commit ef68ba9
Show file tree
Hide file tree
Showing 9 changed files with 844 additions and 776 deletions.
15 changes: 11 additions & 4 deletions docs/source/moto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ A user could test this as follows:
# tests/some_test.py
from pytest_mock_resources import create_moto_fixture
from pytest_mock_resources.fixture.moto import Session
from some_module import list_files
moto = create_moto_fixture()
def test_list_files(moto):
def test_list_files(moto: Session):
s3_client = moto.client("s3")
files = list_files(s3_client)
assert ...
Expand All @@ -46,9 +48,13 @@ object. Namely you would generally want to call `.client(...)` or `.resource(...
.. code-block:: python
import boto3
from pytest_mock_resources import create_moto_fixture
from pytest_mock_resources.fixture.moto import Session
moto = create_moto_fixture()
def test_list_files(pmr_moto_credentials):
kwargs = pmr_moto_credentials.as_kwargs()
def test_list_files(moto: Session):
kwargs = moto.pmr_credentials.as_kwargs()
s3_client = boto3.client("s3", **kwargs)
Expand Down Expand Up @@ -85,6 +91,7 @@ These objects help reduce boilerplate around setting up buckets/files among test
.. code-block:: python
from pytest_mock_resources import create_moto_fixture, S3Bucket, S3Object
from pytest_mock_resources.fixture.moto import Session
bucket = S3Bucket("test")
moto = create_moto_fixture(
Expand All @@ -93,7 +100,7 @@ These objects help reduce boilerplate around setting up buckets/files among test
bucket.object("test.csv", "a,b,c\n1,2,3"),
)
def test_ls(pmr_moto_credentials):
def test_ls(moto: Session):
resource = moto.resource("s3")
objects = resource.Bucket("test").objects.all()
assert len(objects) == 1
Expand Down
1,474 changes: 753 additions & 721 deletions poetry.lock

Large diffs are not rendered by default.

21 changes: 14 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytest-mock-resources"
version = "2.7.0"
version = "2.9.0"
description = "A pytest plugin for easily instantiating reproducible mock resources."
authors = [
"Omar Khan <oakhan3@gmail.com>",
Expand All @@ -27,6 +27,8 @@ python = ">=3.7, <4"
pytest = {version = ">=1.0"}
sqlalchemy = {version = ">1.0, !=1.4.0, !=1.4.1, !=1.4.2, !=1.4.3, !=1.4.4, !=1.4.5, !=1.4.6, !=1.4.7, !=1.4.8, !=1.4.9, !=1.4.10, !=1.4.11, !=1.4.12, !=1.4.13, !=1.4.14, !=1.4.15, !=1.4.16, !=1.4.17, !=1.4.18, !=1.4.19, !=1.4.20, !=1.4.21, !=1.4.22, !=1.4.23"}

typing_extensions = "*"

# extra [postgres]
psycopg2 = {version = "*", optional = true}
psycopg2-binary = {version = "*", optional = true}
Expand All @@ -52,21 +54,24 @@ python-on-whales = {version = ">=0.22.0", optional = true}

[tool.poetry.dev-dependencies]
black = "22.3.0"
botocore = ">=1.10.84"
coverage = "*"
flake8 = "*"
isort = ">=5.0"
moto = ">=2.3.2"
mypy = {version = "0.982"}
pydocstyle = {version = "*"}
sqlalchemy-stubs = {version = "*"}
pytest-xdist = "*"
pytest-asyncio = "*"
types-six = "^1.16.0"
types-PyMySQL = "^1.0.2"
types-redis = "^3.5.6"
pytest-xdist = "*"
responses = ">=0.23.0"
sqlalchemy-stubs = {version = "*"}
sqlalchemy2-stubs = "^0.0.2-alpha.19"
types-filelock = "^3.2.7"
types-PyMySQL = "^1.0.2"
types-dataclasses = "^0.6.5"
types-filelock = "^3.2.7"
types-redis = "^3.5.6"
types-requests = "*"
types-six = "^1.16.0"

[tool.poetry.extras]
docker = ['python-on-whales', 'filelock']
Expand Down Expand Up @@ -120,6 +125,8 @@ markers = [
filterwarnings = [
"error",
"ignore:There is no current event loop:DeprecationWarning",
"ignore:stream argument is deprecated. Use stream parameter in request directly:DeprecationWarning",
"ignore:Boto3 will no longer support Python 3.7.*::boto3"
]

[build-system]
Expand Down
2 changes: 0 additions & 2 deletions src/pytest_mock_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
pmr_mongo_container,
pmr_moto_config,
pmr_moto_container,
pmr_moto_credentials,
pmr_mysql_config,
pmr_mysql_container,
pmr_postgres_config,
Expand Down Expand Up @@ -65,7 +64,6 @@
"pmr_mongo_container",
"pmr_moto_config",
"pmr_moto_container",
"pmr_moto_credentials",
"pmr_mysql_config",
"pmr_mysql_container",
"pmr_postgres_config",
Expand Down
2 changes: 0 additions & 2 deletions src/pytest_mock_resources/fixture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
create_moto_fixture,
pmr_moto_config,
pmr_moto_container,
pmr_moto_credentials,
S3Bucket,
S3Object,
)
Expand Down Expand Up @@ -47,7 +46,6 @@
"pmr_mongo_container",
"pmr_moto_config",
"pmr_moto_container",
"pmr_moto_credentials",
"pmr_mysql_config",
"pmr_mysql_container",
"pmr_postgres_config",
Expand Down
11 changes: 11 additions & 0 deletions src/pytest_mock_resources/fixture/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import uuid
from typing import Union

import pytest
from typing_extensions import Literal


def generate_fixture_id(enabled: bool = True, name=""):
Expand All @@ -19,3 +21,12 @@ def asyncio_fixture(async_fixture, scope="function"):

fixture = pytest.fixture(scope=scope)
return fixture(async_fixture)


Scope = Union[
Literal["session"],
Literal["package"],
Literal["module"],
Literal["class"],
Literal["function"],
]
6 changes: 4 additions & 2 deletions src/pytest_mock_resources/fixture/moto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from pytest_mock_resources.fixture.moto.action import MotoAction, S3Bucket, S3Object
from pytest_mock_resources.fixture.moto.base import (
create_moto_fixture,
Credentials,
pmr_moto_config,
pmr_moto_container,
pmr_moto_credentials,
Session,
)

__all__ = [
"Credentials",
"MotoAction",
"S3Bucket",
"S3Object",
"Session",
"create_moto_fixture",
"pmr_moto_config",
"pmr_moto_container",
"pmr_moto_credentials",
]
83 changes: 48 additions & 35 deletions src/pytest_mock_resources/fixture/moto/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pytest_mock_resources.compat import boto3
from pytest_mock_resources.container.base import get_container
from pytest_mock_resources.container.moto import endpoint_url, MotoConfig
from pytest_mock_resources.fixture.base import Scope
from pytest_mock_resources.fixture.moto.action import apply_ordered_actions, MotoAction


Expand All @@ -29,7 +30,11 @@ def pmr_moto_container(pytestconfig, pmr_moto_config):
yield from get_container(pytestconfig, pmr_moto_config)


def create_moto_fixture(*ordered_actions: MotoAction, scope="function"):
def create_moto_fixture(
*ordered_actions: MotoAction,
region_name: str = "us-east-1",
scope: Scope = "function",
):
"""Produce a Moto fixture.
Any number of fixture functions can be created. Under the hood they will all share the same
Expand All @@ -41,7 +46,7 @@ def create_moto_fixture(*ordered_actions: MotoAction, scope="function"):
boto3 ``client``/``resource`` objects outside of the one handed to the test (for example,
in the code under test), they should be sure to use the ``aws_access_key_id``,
``aws_secret_access_key``, ``aws_session_token``, and ``endpoint_url`` given by the
``pmr_moto_credentials`` fixture.
``<fixturename>.pmr_credentials`` attribute.
.. note::
Expand All @@ -51,60 +56,67 @@ def create_moto_fixture(*ordered_actions: MotoAction, scope="function"):
Args:
ordered_actions: Any number of ordered actions to be run on test setup.
region_name (str): The name of the AWS region to use, defaults to "us-east-1".
scope (str): The scope of the fixture can be specified by the user, defaults to "function".
"""
validate_actions(ordered_actions, fixture="moto")

@pytest.fixture(scope=scope)
def _fixture(pmr_moto_credentials) -> Session:
def _fixture(pmr_moto_container, pmr_moto_config) -> Session:
url = endpoint_url(pmr_moto_config)
credentials = Credentials.from_endpoint_url(url, region_name=region_name)

session = Session(
boto3.Session(
aws_access_key_id=pmr_moto_credentials.aws_access_key_id,
aws_secret_access_key=pmr_moto_credentials.aws_secret_access_key,
aws_session_token=pmr_moto_credentials.aws_session_token,
aws_access_key_id=credentials.aws_access_key_id,
aws_secret_access_key=credentials.aws_secret_access_key,
aws_session_token=credentials.aws_session_token,
region_name=region_name,
),
endpoint_url=pmr_moto_credentials.endpoint_url,
endpoint_url=credentials.endpoint_url,
pmr_credentials=credentials,
)
apply_ordered_actions(session, ordered_actions)
return session

return _fixture


@pytest.fixture()
def pmr_moto_credentials(pmr_moto_container, pmr_moto_config) -> Credentials:
# Attempt at a cross-process way of generating unique 12-character integers.
account_id = str(time.time_ns())[:12]

url = endpoint_url(pmr_moto_config)

sts = boto3.client(
"sts",
endpoint_url=url,
aws_access_key_id="test",
aws_secret_access_key="test",
)
response = sts.assume_role(
RoleArn=f"arn:aws:iam::{account_id}:role/my-role",
RoleSessionName="test-session-name",
ExternalId="test-external-id",
)

return Credentials(
aws_access_key_id=response["Credentials"]["AccessKeyId"],
aws_secret_access_key=response["Credentials"]["SecretAccessKey"],
aws_session_token=response["Credentials"]["SessionToken"],
endpoint_url=url,
)


@dataclass
class Credentials:
aws_access_key_id: str
aws_secret_access_key: str
aws_session_token: str
endpoint_url: str
region_name = "us-east-1"
region_name: str = "us-east-1"

@classmethod
def from_endpoint_url(
cls, url: str, account_id: str | None = None, region_name: str = "us-east-1"
):
if account_id is None:
# Attempt at a cross-process way of generating unique 12-character integers.
account_id = str(time.time_ns())[:12]

sts = boto3.client(
"sts",
endpoint_url=url,
aws_access_key_id="test",
aws_secret_access_key="test",
)
response = sts.assume_role(
RoleArn=f"arn:aws:iam::{account_id}:role/my-role",
RoleSessionName="test-session-name",
ExternalId="test-external-id",
)

return cls(
aws_access_key_id=response["Credentials"]["AccessKeyId"],
aws_secret_access_key=response["Credentials"]["SecretAccessKey"],
aws_session_token=response["Credentials"]["SessionToken"],
endpoint_url=url,
region_name=region_name,
)

def as_kwargs(self):
return {
Expand All @@ -122,6 +134,7 @@ class Session:

session: boto3.Session
endpoint_url: str
pmr_credentials: Credentials

def client(self, service_name, **kwargs):
return self.session.client(service_name, endpoint_url=self.endpoint_url, **kwargs)
Expand Down
6 changes: 3 additions & 3 deletions tests/fixture/test_pmr_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ def test_mongo_pmr_credentials(mongo):
assert mongo.pmr_credentials


def test_moto_pmr_credentials(moto, pmr_moto_credentials):
def test_moto_pmr_credentials(moto):
assert moto
assert pmr_moto_credentials.aws_access_key_id
assert pmr_moto_credentials.aws_secret_access_key
assert moto.pmr_credentials.aws_access_key_id
assert moto.pmr_credentials.aws_secret_access_key


def test_mysql_pmr_credentials(mysql):
Expand Down

0 comments on commit ef68ba9

Please sign in to comment.