-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
34be34f
commit 98f3e41
Showing
10 changed files
with
372 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
""" | ||
Mock data for use in tests. | ||
Names should ideally be descriptive enough to recognise what they are without looking at the data itself. | ||
Letters may be appended in places to indicate the data is of the same type, but has different specific values | ||
to others. | ||
_POST_DATA - Is for a `PostSchema` schema. | ||
_IN_DATA - Is for an `In` model. | ||
_GET_DATA - Is for an entity schema - Used in assertions for e2e tests. | ||
_DATA - Is none of the above - likely to be used in post requests as they are likely identical, only with some ids | ||
missing so that they can be added later e.g. for pairing up units that aren't known before hand. | ||
""" | ||
|
||
from bson import ObjectId | ||
|
||
# ---------------------------- ATTACHMENTS ----------------------------- | ||
|
||
# All values | ||
|
||
ATTACHMENT_POST_DATA_ALL_VALUES = { | ||
"entity_id": str(ObjectId()), | ||
"file_name": "report.pdf", | ||
"title": "Report Title", | ||
"description": "A damage report.", | ||
} | ||
|
||
ATTACHMENT_IN_DATA_ALL_VALUES = { | ||
**ATTACHMENT_POST_DATA_ALL_VALUES, | ||
"id": str(ObjectId()), | ||
"object_key": "attachments/65df5ee771892ddcc08bd28f/65e0a624d64aaae884abaaee", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,19 @@ | ||
[pytest] | ||
asyncio_mode=auto | ||
asyncio_mode=auto | ||
env = | ||
API__TITLE=Object Storage Service API | ||
API__DESCRIPTION=This is the API for the Object Storage Service | ||
API__ROOT_PATH= | ||
API__ALLOWED_CORS_HEADERS=["*"] | ||
API__ALLOWED_CORS_ORIGINS=["*"] | ||
API__ALLOWED_CORS_METHODS=["*"] | ||
DATABASE__PROTOCOL=mongodb | ||
DATABASE__USERNAME=root | ||
DATABASE__PASSWORD=example | ||
DATABASE__HOST_AND_OPTIONS=localhost:27017/?authMechanism=SCRAM-SHA-256&authSource=admin | ||
DATABASE__NAME=test-object-storage | ||
OBJECT_STORAGE__ENDPOINT_URL=http://minio:9000 | ||
OBJECT_STORAGE__ACCESS_KEY=root | ||
OBJECT_STORAGE__SECRET_ACCESS_KEY=example_password | ||
OBJECT_STORAGE__BUCKET_NAME=test-object-storage | ||
OBJECT_STORAGE__PRESIGNED_URL_EXPIRY=1800 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
""" | ||
Module for providing common test configuration, test fixtures, and helper functions. | ||
""" | ||
|
||
from unittest.mock import Mock | ||
|
||
import pytest | ||
from bson import ObjectId | ||
from pymongo.collection import Collection | ||
from pymongo.database import Database | ||
from pymongo.results import InsertOneResult | ||
|
||
from object_storage_api.repositories.attachment import AttachmentRepo | ||
|
||
|
||
@pytest.fixture(name="database_mock") | ||
def fixture_database_mock() -> Mock: | ||
""" | ||
Fixture to create a mock of the MongoDB database dependency and its collections. | ||
:return: Mocked MongoDB database instance with the mocked collections. | ||
""" | ||
database_mock = Mock(Database) | ||
database_mock.attachments = Mock(Collection) | ||
return database_mock | ||
|
||
|
||
@pytest.fixture(name="attachment_repository") | ||
def fixture_item_repository(database_mock: Mock) -> AttachmentRepo: | ||
""" | ||
Fixture to create a `AttachmentRepo` instance with a mocked Database dependency. | ||
:param database_mock: Mocked MongoDB database instance. | ||
:return: `AttachmentRepo` instance with the mocked dependency. | ||
""" | ||
return AttachmentRepo(database_mock) | ||
|
||
|
||
class RepositoryTestHelpers: | ||
""" | ||
A utility class containing common helper methods for the repository tests. | ||
This class provides a set of static methods that encapsulate common functionality frequently used in the repository | ||
tests. | ||
""" | ||
|
||
@staticmethod | ||
def mock_insert_one(collection_mock: Mock, inserted_id: ObjectId) -> None: | ||
""" | ||
Mock the `insert_one` method of the MongoDB database collection mock to return an `InsertOneResult` object. The | ||
passed `inserted_id` value is returned as the `inserted_id` attribute of the `InsertOneResult` object, enabling | ||
for the code that relies on the `inserted_id` value to work. | ||
:param collection_mock: Mocked MongoDB database collection instance. | ||
:param inserted_id: The `ObjectId` value to be assigned to the `inserted_id` attribute of the `InsertOneResult` | ||
object | ||
""" | ||
insert_one_result_mock = Mock(InsertOneResult) | ||
insert_one_result_mock.inserted_id = inserted_id | ||
insert_one_result_mock.acknowledged = True | ||
collection_mock.insert_one.return_value = insert_one_result_mock | ||
|
||
@staticmethod | ||
def mock_find_one(collection_mock: Mock, document: dict | None) -> None: | ||
""" | ||
Mocks the `find_one` method of the MongoDB database collection mock to return a specific document. | ||
:param collection_mock: Mocked MongoDB database collection instance. | ||
:param document: The document to be returned by the `find_one` method. | ||
""" | ||
if collection_mock.find_one.side_effect is None: | ||
collection_mock.find_one.side_effect = [document] | ||
else: | ||
documents = list(collection_mock.find_one.side_effect) | ||
documents.append(document) | ||
collection_mock.find_one.side_effect = documents |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
""" | ||
Unit tests for the `AttachmentRepo` repository. | ||
""" | ||
|
||
from test.mock_data import ATTACHMENT_IN_DATA_ALL_VALUES | ||
from test.unit.repositories.conftest import RepositoryTestHelpers | ||
from unittest.mock import MagicMock, Mock | ||
|
||
import pytest | ||
|
||
from object_storage_api.models.attachment import AttachmentIn, AttachmentOut | ||
from object_storage_api.repositories.attachment import AttachmentRepo | ||
|
||
|
||
class AttachmentRepoDSL: | ||
"""Base class for `AttachmentRepo` unit tests.""" | ||
|
||
mock_database: Mock | ||
attachment_repository: AttachmentRepo | ||
attachments_collection: Mock | ||
|
||
mock_session = MagicMock() | ||
|
||
@pytest.fixture(autouse=True) | ||
def setup(self, database_mock): | ||
"""Setup fixtures""" | ||
|
||
self.mock_database = database_mock | ||
self.attachment_repository = AttachmentRepo(database_mock) | ||
self.attachments_collection = database_mock.attachments | ||
|
||
|
||
class CreateDSL(AttachmentRepoDSL): | ||
"""Base class for `create` tests.""" | ||
|
||
_attachment_in: AttachmentIn | ||
_expected_attachment_out: AttachmentOut | ||
_created_attachment: AttachmentOut | ||
_create_exception: pytest.ExceptionInfo | ||
|
||
def mock_create( | ||
self, | ||
attachment_in_data: dict, | ||
) -> None: | ||
""" | ||
Mocks database methods appropriately to test the `create` repo method. | ||
:param attachment_in_data: Dictionary containing the attachment data as would be required for a `AttachmentIn` | ||
database model (i.e. no created and modified times required). | ||
""" | ||
|
||
# Pass through `AttachmentIn` first as need creation and modified times | ||
self._attachment_in = AttachmentIn(**attachment_in_data) | ||
|
||
self._expected_attachment_out = AttachmentOut(**self._attachment_in.model_dump()) | ||
|
||
RepositoryTestHelpers.mock_insert_one(self.attachments_collection, self._attachment_in.id) | ||
RepositoryTestHelpers.mock_find_one( | ||
self.attachments_collection, {**self._attachment_in.model_dump(), "_id": self._attachment_in.id} | ||
) | ||
|
||
def call_create(self) -> None: | ||
"""Calls the `AttachmentRepo` `create` method with the appropriate data from a prior call to `mock_create`.""" | ||
|
||
self._created_attachment = self.attachment_repository.create(self._attachment_in, session=self.mock_session) | ||
|
||
def check_create_success(self) -> None: | ||
"""Checks that a prior call to `call_create` worked as expected.""" | ||
|
||
self.attachments_collection.insert_one.assert_called_once_with( | ||
self._attachment_in.model_dump(by_alias=True), session=self.mock_session | ||
) | ||
self.attachments_collection.find_one.assert_called_once_with( | ||
{"_id": self._attachment_in.id}, session=self.mock_session | ||
) | ||
|
||
assert self._created_attachment == self._expected_attachment_out | ||
|
||
|
||
class TestCreate(CreateDSL): | ||
"""Tests for creating an attachment.""" | ||
|
||
def test_create(self): | ||
"""Test creating an attachment.""" | ||
|
||
self.mock_create(ATTACHMENT_IN_DATA_ALL_VALUES) | ||
self.call_create() | ||
self.check_create_success() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
Module for providing common test configuration, test fixtures, and helper functions. | ||
""" | ||
|
||
from datetime import datetime, timezone | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
|
||
MODEL_MIXINS_FIXED_DATETIME_NOW = datetime(2024, 2, 16, 14, 0, 0, 0, tzinfo=timezone.utc) | ||
|
||
|
||
@pytest.fixture(name="model_mixins_datetime_now_mock") | ||
def fixture_model_mixins_datetime_now_mock(): | ||
""" | ||
Fixture that mocks the `datetime.now` method in the `inventory_management_system_api.models.mixins` module. | ||
""" | ||
with patch("object_storage_api.models.mixins.datetime") as mock_datetime: | ||
mock_datetime.now.return_value = MODEL_MIXINS_FIXED_DATETIME_NOW | ||
yield mock_datetime |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
""" | ||
Unit tests for the `AttachmentStore` store. | ||
""" | ||
|
||
from test.mock_data import ATTACHMENT_POST_DATA_ALL_VALUES | ||
from unittest.mock import MagicMock, patch | ||
|
||
import pytest | ||
from bson import ObjectId | ||
|
||
from object_storage_api.core.exceptions import InvalidObjectIdError | ||
from object_storage_api.core.object_store import object_storage_config | ||
from object_storage_api.models.attachment import AttachmentIn | ||
from object_storage_api.schemas.attachment import AttachmentPostSchema | ||
from object_storage_api.stores.attachment import AttachmentStore | ||
|
||
|
||
class AttachmentStoreDSL: | ||
"""Base class for `AttachmentStore` unit tests.""" | ||
|
||
mock_s3_client: MagicMock | ||
mock_object_id: MagicMock | ||
attachment_store: AttachmentStore | ||
|
||
@pytest.fixture(autouse=True) | ||
def setup( | ||
self, | ||
# Ensures all created and modified times are mocked throughout | ||
# pylint: disable=unused-argument | ||
model_mixins_datetime_now_mock, | ||
): | ||
"""Setup fixtures""" | ||
|
||
with patch("object_storage_api.stores.attachment.s3_client") as s3_client_mock: | ||
with patch("object_storage_api.stores.attachment.ObjectId") as object_id_mock: | ||
self.mock_s3_client = s3_client_mock | ||
self.mock_object_id = object_id_mock | ||
self.attachment_store = AttachmentStore() | ||
yield | ||
|
||
|
||
class CreateDSL(AttachmentStoreDSL): | ||
"""Base class for `create` tests.""" | ||
|
||
_attachment_post: AttachmentPostSchema | ||
_expected_attachment_in: AttachmentIn | ||
_expected_url: str | ||
_created_attachment_in: AttachmentIn | ||
_generated_url: str | ||
_create_exception: pytest.ExceptionInfo | ||
|
||
def mock_create(self, attachment_post_data: dict) -> None: | ||
""" | ||
Mocks object store methods appropriately to test the `create` store method. | ||
:param attachment_post_data: Dictionary containing the attachment data as would be required for an | ||
`AttachmentPost` schema. | ||
""" | ||
self._attachment_post = AttachmentPostSchema(**attachment_post_data) | ||
|
||
attachment_id = ObjectId() | ||
self.mock_object_id.return_value = attachment_id | ||
|
||
expected_object_key = f"attachments/{self._attachment_post.entity_id}/{attachment_id}" | ||
|
||
# Mock presigned url generation | ||
self._expected_url = "http://test-url.com" | ||
self.mock_s3_client.generate_presigned_url.return_value = self._expected_url | ||
|
||
# Expected model data with the object key defined (Ignore if invalid to avoid a premature error) | ||
if self._attachment_post.entity_id != "invalid-id": | ||
self._expected_attachment_in = AttachmentIn( | ||
**self._attachment_post.model_dump(), id=str(attachment_id), object_key=expected_object_key | ||
) | ||
|
||
def call_create(self) -> None: | ||
"""Calls the `AttachmentStore` `create` method with the appropriate data from a prior call to `mock_create`.""" | ||
|
||
self._created_attachment_in, self._generated_url = self.attachment_store.create(self._attachment_post) | ||
|
||
def call_create_expecting_error(self, error_type: type[BaseException]) -> None: | ||
""" | ||
Calls the `AttachmentStore` `create` method with the appropriate data from a prior call to `mock_create` | ||
while expecting an error to be raised. | ||
:param error_type: Expected exception to be raised. | ||
""" | ||
|
||
with pytest.raises(error_type) as exc: | ||
self.attachment_store.create(self._attachment_post) | ||
self._create_exception = exc | ||
|
||
def check_create_success(self) -> None: | ||
"""Checks that a prior call to `call_create` worked as expected.""" | ||
|
||
self.mock_s3_client.generate_presigned_url.assert_called_once_with( | ||
"put_object", | ||
Params={ | ||
"Bucket": object_storage_config.bucket_name.get_secret_value(), | ||
"Key": self._expected_attachment_in.object_key, | ||
}, | ||
ExpiresIn=object_storage_config.presigned_url_expiry, | ||
) | ||
|
||
# Cannot know the expected creation and modified time here, so ignore in comparison | ||
assert self._created_attachment_in == self._expected_attachment_in | ||
assert self._generated_url == self._expected_url | ||
|
||
def check_create_failed_with_exception(self, message: str) -> None: | ||
""" | ||
Checks that a prior call to `call_create_expecting_error` worked as expected, raising an exception | ||
with the correct message. | ||
:param message: Message of the raised exception. | ||
""" | ||
|
||
assert str(self._create_exception.value) == message | ||
|
||
|
||
class TestCreate(CreateDSL): | ||
"""Tests for creating an attachment.""" | ||
|
||
def test_create(self): | ||
"""Test creating an attachment.""" | ||
|
||
self.mock_create(ATTACHMENT_POST_DATA_ALL_VALUES) | ||
self.call_create() | ||
self.check_create_success() | ||
|
||
def test_create_with_invalid_entity_id(self): | ||
"""Test creating an attachment with an invalid `entity_id`.""" | ||
|
||
self.mock_create({**ATTACHMENT_POST_DATA_ALL_VALUES, "entity_id": "invalid-id"}) | ||
self.call_create_expecting_error(InvalidObjectIdError) | ||
self.check_create_failed_with_exception("Invalid ObjectId value 'invalid-id'") |