diff --git a/app/imp/sql_data/__init__.py b/app/imp/sql_data/__init__.py index 9810464..2f1d8ba 100644 --- a/app/imp/sql_data/__init__.py +++ b/app/imp/sql_data/__init__.py @@ -2,6 +2,8 @@ SQLDataSource, SQLDataSourceType, SQLExtractMetadata, + SQLUploadChunk, + SQLUploadMetadata, SupportedDBVendors, ) from .exceptions import SQLDataError, SQLDataSourceDisposedError @@ -12,5 +14,7 @@ "SQLDataSourceDisposedError", "SQLDataSourceType", "SQLExtractMetadata", + "SQLUploadChunk", + "SQLUploadMetadata", "SupportedDBVendors", ] diff --git a/app/use_cases/fetch_metadata.py b/app/use_cases/fetch_metadata.py index 194d5a2..eda8c0d 100644 --- a/app/use_cases/fetch_metadata.py +++ b/app/use_cases/fetch_metadata.py @@ -24,7 +24,7 @@ # ============================================================================= -class DoFetchDataSourceTypeSources(Task[DataSourceType, Sequence[DataSource]]): +class DoFetchDataSources(Task[DataSourceType, Sequence[DataSource]]): """Fetches all the data sources of a given data source type.""" def __init__(self, data_source_type: DataSourceType): @@ -45,7 +45,7 @@ def execute(self, an_input: Transport) -> Sequence[DataSource]: return data_sources -class DoFetchDataSourceExtracts(Task[DataSource, Sequence[ExtractMetadata]]): +class DoFetchExtractMetadata(Task[DataSource, Sequence[ExtractMetadata]]): """Fetch all the extract metadata of a given data source.""" def __init__(self, data_source: DataSource): @@ -98,9 +98,9 @@ def execute( @staticmethod def _data_source_types_to_tasks( data_source_types: Iterable[DataSourceType], - ) -> Sequence[DoFetchDataSourceTypeSources]: + ) -> Sequence[DoFetchDataSources]: return tuple( - DoFetchDataSourceTypeSources(data_source_type=_data_source_type) + DoFetchDataSources(data_source_type=_data_source_type) for _data_source_type in data_source_types ) @@ -133,8 +133,8 @@ def execute( @staticmethod def _data_sources_to_tasks( data_sources: Iterable[DataSource], - ) -> Sequence[DoFetchDataSourceExtracts]: + ) -> Sequence[DoFetchExtractMetadata]: return tuple( - DoFetchDataSourceExtracts(data_source=_data_source) + DoFetchExtractMetadata(data_source=_data_source) for _data_source in data_sources ) diff --git a/pyproject.toml b/pyproject.toml index 429c53c..32718bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ strictSetInference = true typeCheckingMode = "basic" [tool.pytest.ini_options] -addopts = "--cov=app --cov-fail-under=85 --cov-report=html --cov-report=term-missing -n auto --junitxml='junitxml_report/report.xml' -v --durations=10 --cache-clear -p no:sugar" +addopts = "--cov=app --cov-fail-under=90 --cov-report=html --cov-report=term-missing -n auto --junitxml='junitxml_report/report.xml' -v --durations=10 --cache-clear" console_output_style = "progress" log_cli = 1 log_cli_date_format = "%Y-%m-%d %H:%M:%S" diff --git a/tests/core/factories.py b/tests/core/factories.py index 9989828..107e7f5 100644 --- a/tests/core/factories.py +++ b/tests/core/factories.py @@ -113,10 +113,17 @@ def execute(self, an_input: Any) -> Any: class FakeTransport(Transport): - """A fake transport that returns empty results.""" + """A fake transport that returns dummy data.""" - def __init__(self): - self._is_closed: bool = False + def __init__( + self, + is_closed: bool = False, + fetch_data_source_extracts_count: int = 0, + fetch_data_sources_count: int = 0, + ): + self._is_closed: bool = is_closed + self._data_sources_count: int = fetch_data_sources_count + self._extracts_count: int = fetch_data_source_extracts_count @property def is_disposed(self) -> bool: @@ -131,12 +138,16 @@ def fetch_data_source_extracts( data_source: DataSource, **options: TransportOptions, ) -> Sequence[ExtractMetadata]: - return tuple() + return tuple( + FakeExtractMetadataFactory.create_batch(size=self._extracts_count) + ) def fetch_data_sources( self, data_source_type: DataSourceType, **options: TransportOptions ) -> Sequence[DataSource]: - return tuple() + return tuple( + FakeDataSourceFactory.create_batch(size=self._data_sources_count) + ) def mark_upload_as_complete( self, upload_metadata: UploadMetadata, **options: TransportOptions @@ -306,7 +317,7 @@ class FakeExtractMetadataFactory(ExtractMetadataFactory): preferred_uploads_name = factory.LazyAttribute( lambda _o: "%s" % _o.name.lower().replace(" ", "_") ) - data_source = factory.SubFactory(FakeDataSource) + data_source = factory.SubFactory(FakeDataSourceFactory) class Meta: model = FakeExtractMetadata @@ -317,6 +328,10 @@ class FakeTransportFactory(factory.Factory): A factory for creating fake transport instances that return empty results. """ + is_closed: bool = False + fetch_data_source_extracts_count: int = 0 + fetch_data_sources_count: int = 0 + class Meta: model = FakeTransport diff --git a/tests/core/test_domain.py b/tests/core/test_domain.py index f159803..29728ed 100644 --- a/tests/core/test_domain.py +++ b/tests/core/test_domain.py @@ -11,6 +11,9 @@ FakeDataSourceType, FakeDataSourceTypeFactory, FakeExtractMetadata, + FakeExtractMetadataFactory, + FakeUploadChunk, + FakeUploadMetadata, ) @@ -89,21 +92,9 @@ def test_get_required_fields_class_method(self) -> None: ) -class TestFakeDataSourceInterface(TestCase): +class TestDataSourceInterface(TestCase): """Tests for the ``DataSource`` interface default implementations.""" - def test_string_representation(self) -> None: - """ - Assert that the default ``DataSource.__str__()`` implementation - returns the expected value. - """ - data_source = FakeDataSource( - id="1", - name="Some data source", - data_source_type=FakeDataSourceTypeFactory(), - ) - assert str(data_source) == "1::Some data source" - def test_of_mapping_class_method(self) -> None: """ Assert that the ``DataSource.of_mapping()`` class method returns @@ -136,8 +127,20 @@ def test_of_mapping_class_method(self) -> None: assert data_source2.name == "Some other data source" assert data_source2.description is None + def test_string_representation(self) -> None: + """ + Assert that the default ``DataSource.__str__()`` implementation + returns the expected value. + """ + data_source = FakeDataSource( + id="1", + name="Some data source", + data_source_type=FakeDataSourceTypeFactory(), + ) + assert str(data_source) == "1::Some data source" + -class TestFakeDataSourceTypeInterface(TestCase): +class TestDataSourceTypeInterface(TestCase): """Tests for the ``DataSourceType`` interface default implementations.""" def test_string_representation(self) -> None: @@ -152,15 +155,20 @@ def test_string_representation(self) -> None: class TestExtractMetadataInterface(TestCase): """Tests for the ``ExtractMetadata`` interface default implementations.""" - def test_string_representation(self) -> None: - """ - Assert that the default ``ExtractMetadata.__str__()`` implementation - returns the expected value. - """ - extract = FakeExtractMetadata( + def setUp(self) -> None: + super().setUp() + self._extract = FakeExtractMetadata( id="1", name="Some data", data_source=FakeDataSourceFactory() ) - assert str(extract) == "1::Some data" + + def test_get_upload_meta_extra_init_kwargs(self) -> None: + """ + Assert that the default implementation of + ``ExtractMetadata.get_upload_meta_extra_init_kwargs()`` instance + method returns ``None``. + """ + + assert self._extract.get_upload_meta_extra_init_kwargs() is None def test_of_mapping_class_method(self) -> None: """ @@ -195,3 +203,113 @@ def test_of_mapping_class_method(self) -> None: assert extract2.name == "Some other data" assert extract2.description is None assert extract2.preferred_uploads_name == "some_data" + + def test_string_representation(self) -> None: + """ + Assert that the default ``ExtractMetadata.__str__()`` implementation + returns the expected value. + """ + assert str(self._extract) == "1::Some data" + + +class TestUploadChunkInterface(TestCase): + """Tests for the ``UploadChunk`` interface default implementations.""" + + def setUp(self) -> None: + super().setUp() + self._upload_chunk = FakeUploadChunk( + id="1", chunk_index=0, chunk_content=b"Bla bla bla ..." + ) + + def test_of_mapping_class_method(self) -> None: + """ + Assert that the ``UploadChunk.of_mapping()`` class method returns + the expected value. + """ + + upload_chunk1 = FakeUploadChunk.of_mapping( + {"id": "1", "chunk_index": 0, "chunk_content": b"Bla bla bla ..."} + ) + upload_chunk2 = FakeUploadChunk.of_mapping( + { + "id": "2", + "chunk_index": 1, + "chunk_content": b"Bla bla bla bla ...", + } + ) + + assert upload_chunk1 is not None + assert upload_chunk1.id == "1" + assert upload_chunk1.chunk_index == 0 + assert upload_chunk1.chunk_content == b"Bla bla bla ..." + assert upload_chunk2 is not None + assert upload_chunk2.id == "2" + assert upload_chunk2.chunk_index == 1 + assert upload_chunk2.chunk_content == b"Bla bla bla bla ..." + + def test_string_representation(self) -> None: + """ + Assert that the default ``UploadChunk.__str__()`` implementation + returns the expected value. + """ + assert str(self._upload_chunk) == "Chunk 0" + + +class TestUploadMetadataInterface(TestCase): + """Tests for the ``UploadMetadata`` interface default implementations.""" + + def setUp(self) -> None: + super().setUp() + self._extract_metadata = FakeExtractMetadataFactory() + self._upload_metadata = FakeUploadMetadata( + id="1", + org_unit_code="12345", + org_unit_name="Test Facility", + content_type="application/json", + extract_metadata=self._extract_metadata, + ) + + def test_get_upload_meta_extra_init_kwargs(self) -> None: + """ + Assert that the default implementation of + ``UploadMetadata.get_upload_chunk_extra_init_kwargs()`` instance + method returns ``None``. + """ + + kwargs = self._upload_metadata.get_upload_chunk_extra_init_kwargs() + assert kwargs is None + + def test_of_mapping_class_method(self) -> None: + """ + Assert that the ``UploadMetadata.of_mapping()`` class method returns + the expected value. + """ + + org_unit_code = "12345" + org_unit_name = "Test Facility" + content_type = "application/json" + upload_metadata = FakeUploadMetadata.of_mapping( + { + "id": "1", + "org_unit_code": org_unit_code, + "org_unit_name": org_unit_name, + "content_type": content_type, + "extract_metadata": self._extract_metadata, + } + ) + + assert upload_metadata is not None + assert upload_metadata.id == "1" + assert upload_metadata.org_unit_code == org_unit_code + assert upload_metadata.org_unit_name == org_unit_name + assert upload_metadata.content_type == content_type + assert upload_metadata.extract_metadata == self._extract_metadata + + def test_string_representation(self) -> None: + """ + Assert that the default ``UploadMetadata.__str__()`` implementation + returns the expected value. + """ + assert str(self._upload_metadata) == "Upload {} for extract {}".format( + self._upload_metadata.id, str(self._extract_metadata) + ) diff --git a/tests/imp/sql_data/factories.py b/tests/imp/sql_data/factories.py index 83a128c..2999305 100644 --- a/tests/imp/sql_data/factories.py +++ b/tests/imp/sql_data/factories.py @@ -6,12 +6,16 @@ SQLDataSource, SQLDataSourceType, SQLExtractMetadata, + SQLUploadChunk, + SQLUploadMetadata, SupportedDBVendors, ) from tests.core.factories import ( DataSourceFactory, DataSourceTypeFactory, ExtractMetadataFactory, + UploadChunkFactory, + UploadMetadataFactory, ) @@ -85,3 +89,19 @@ class SQLExtractMetadataFactory(ExtractMetadataFactory): class Meta: model = SQLExtractMetadata + + +class SQLUploadChunkFactory(UploadChunkFactory): + """A factory for ``SQLUploadChunk`` instances.""" + + class Meta: + model = SQLUploadChunk + + +class SQLUploadMetadataFactory(UploadMetadataFactory): + """A factory for ``SQLUploadMetadata`` instances.""" + + extract_metadata = factory.SubFactory(SQLExtractMetadataFactory) + + class Meta: + model = SQLUploadMetadata diff --git a/tests/imp/sql_data/test_domain.py b/tests/imp/sql_data/test_domain.py index a378ccc..a66b2bc 100644 --- a/tests/imp/sql_data/test_domain.py +++ b/tests/imp/sql_data/test_domain.py @@ -1,9 +1,10 @@ import os -from typing import Any, Mapping +from typing import Any, Mapping, Sequence from unittest import TestCase from unittest.mock import patch import pytest +from pandas import DataFrame from sqlalchemy.engine import Connection from app.imp.sql_data import ( @@ -12,6 +13,8 @@ SQLDataSourceDisposedError, SQLDataSourceType, SQLExtractMetadata, + SQLUploadChunk, + SQLUploadMetadata, SupportedDBVendors, ) from app.lib import ImproperlyConfiguredError @@ -20,6 +23,7 @@ SQLDataSourceFactory, SQLDataSourceTypeFactory, SQLExtractMetadataFactory, + SQLUploadMetadataFactory, ) @@ -224,6 +228,13 @@ def test_accessors(self) -> None: self._data_source_type.imp_extract_metadata_klass() == SQLExtractMetadata ) # noqa + assert ( + self._data_source_type.imp_upload_chunk_klass() == SQLUploadChunk + ) + assert ( + self._data_source_type.imp_upload_metadata_klass() + == SQLUploadMetadata + ) class TestSQLExtractMetadata(TestCase): @@ -239,3 +250,43 @@ def test_accessors(self) -> None: task = self._extract_meta.to_task() assert task is not None assert self._extract_meta.data_source is not None + + +class TestSQLUploadMetadata(TestCase): + """Test for the :class:`SQLUploadMetadata` class.""" + + def setUp(self) -> None: + super().setUp() + self._upload_meta: SQLUploadMetadata = SQLUploadMetadataFactory() + + def test_accessors(self) -> None: + """Assert that accessors return the expected value.""" + + content_type = "application/vnd.apache-parquet" + assert self._upload_meta.extract_metadata is not None + assert isinstance( + self._upload_meta.extract_metadata, SQLExtractMetadata + ) + assert self._upload_meta.get_content_type() == content_type + assert self._upload_meta.to_task() is not None + + def test_to_task(self) -> None: + """ + Assert that the ``SQLUploadMetadata.to_task()`` method returns a task + with the expected implementation. + """ + + extract_meta: SQLExtractMetadata = self._upload_meta.extract_metadata + data_source: SQLDataSource = extract_meta.data_source + with data_source: + extracted_data: DataFrame = extract_meta.to_task().execute( + data_source.get_extract_task_args() + ) + + upload_task = self._upload_meta.to_task() + processed_extract: Sequence[bytes] = upload_task.execute( + extracted_data + ) + + assert len(processed_extract) > 0 + assert isinstance(processed_extract[0], bytes) diff --git a/tests/lib/transports/http/factories.py b/tests/lib/transports/http/factories.py index 131026d..316d52b 100644 --- a/tests/lib/transports/http/factories.py +++ b/tests/lib/transports/http/factories.py @@ -104,7 +104,7 @@ def post_upload_chunk( ) -> HTTPRequestParams: return { "headers": {"Accept": "application/json"}, - "expected_http_status_code": 200, + "expected_http_status_code": 201, "method": "POST", "url": "%s/create_upload_chunk" % self._host, } @@ -128,7 +128,7 @@ def post_upload_metadata( ) -> HTTPRequestParams: return { "headers": {"Accept": "application/json"}, - "expected_http_status_code": 200, + "expected_http_status_code": 201, "method": "POST", "url": "%s/create_upload_metadata" % self._host, } diff --git a/tests/lib/transports/http/test_api_v1_dialect.py b/tests/lib/transports/http/test_api_v1_dialect.py index 9482952..c34f049 100644 --- a/tests/lib/transports/http/test_api_v1_dialect.py +++ b/tests/lib/transports/http/test_api_v1_dialect.py @@ -13,6 +13,10 @@ from tests.core.factories import ( FakeDataSourceFactory, FakeDataSourceTypeFactory, + FakeExtractMetadataFactory, + FakeUploadChunkFactory, + FakeUploadMetadataFactory, + UploadMetadata, ) @@ -168,3 +172,106 @@ def test_response_to_data_sources_return_value(self) -> None: ), [], ) + + def test_mark_upload_as_complete_return_value(self) -> None: + """ + Assert that the ``mark_upload_as_complete`` method returns the expected + value. + """ + upload_meta = FakeUploadMetadataFactory() + request_params = self._api_dialect.mark_upload_as_complete( + upload_metadata=upload_meta + ) + + assert request_params # Should not be None or empty. + assert request_params["expected_http_status_code"] == 200 + assert request_params["method"].upper() == "PATCH" + + def test_post_upload_chunk_return_value(self) -> None: + """ + Assert that the ``post_upload_chunk`` method returns the expected + value. + """ + upload_meta = FakeUploadMetadataFactory() + chunk_index = 0 + chunk_content = b"Bla bla bla ..." + request_params = self._api_dialect.post_upload_chunk( + upload_metadata=upload_meta, + chunk_index=chunk_index, + chunk_content=chunk_content, + ) + + assert request_params # Should not be None or empty. + assert request_params.get("data") # Should not be None or empty. + assert request_params["expected_http_status_code"] == 201 + assert request_params["method"].upper() == "POST" + + def test_response_to_upload_chunk_return_value(self) -> None: + """ + Assert that the ``response_to_upload_chunk`` method returns the + expected value. + """ + source_upload_chunk = FakeUploadChunkFactory() + upload_meta = FakeUploadMetadataFactory() + response_content = { + "id": source_upload_chunk.id, + "chunk_index": source_upload_chunk.chunk_index, + "chunk_content": source_upload_chunk.chunk_content, + } + upload_chunk = self._api_dialect.response_to_upload_chunk( + json.dumps(response_content).encode("ascii"), + upload_metadata=upload_meta, + ) + + assert upload_chunk is not None + assert upload_chunk.id == source_upload_chunk.id + assert upload_chunk.chunk_index == source_upload_chunk.chunk_index + + def test_post_upload_metadata_return_value(self) -> None: + """ + Assert that the ``post_upload_metadata`` method returns the expected + value. + """ + content_type = "application/json" + extract_meta = FakeExtractMetadataFactory() + org_unit_code = "12345" + org_unit_name = "Test Facility" + request_params = self._api_dialect.post_upload_metadata( + extract_metadata=extract_meta, + content_type=content_type, + org_unit_code=org_unit_code, + org_unit_name=org_unit_name, + ) + + assert request_params # Should not be None or empty. + assert request_params.get("data") # Should not be None or empty. + assert request_params["expected_http_status_code"] == 201 + assert request_params["method"].upper() == "POST" + + def test_response_to_upload_metadata_return_value(self) -> None: + """ + Assert that the ``response_to_upload_metadata`` method returns the + expected value. + """ + source_upload_meta: UploadMetadata = FakeUploadMetadataFactory() + response_content = { + "id": source_upload_meta.id, + "extract_metadata": source_upload_meta.extract_metadata.id, + "org_unit_code": source_upload_meta.org_unit_code, + "org_unit_name": source_upload_meta.org_unit_name, + "content_type": source_upload_meta.content_type, + } + + upload_meta = self._api_dialect.response_to_upload_metadata( + json.dumps(response_content).encode("ascii"), + extract_metadata=source_upload_meta.extract_metadata, + ) + + assert upload_meta is not None + assert upload_meta.id == source_upload_meta.id + assert ( + upload_meta.extract_metadata == source_upload_meta.extract_metadata + ) + assert upload_meta.org_unit_code == source_upload_meta.org_unit_code + assert upload_meta.org_unit_name == source_upload_meta.org_unit_name + assert upload_meta.content_type == source_upload_meta.content_type diff --git a/tests/lib/transports/http/test_http_transport.py b/tests/lib/transports/http/test_http_transport.py index 70790e0..7e2e96b 100644 --- a/tests/lib/transports/http/test_http_transport.py +++ b/tests/lib/transports/http/test_http_transport.py @@ -8,6 +8,8 @@ from tests.core.factories import ( FakeDataSourceFactory, FakeDataSourceTypeFactory, + FakeExtractMetadataFactory, + FakeUploadMetadataFactory, ) from tests.lib.transports.http.factories import FakeHTTPAPIDialectFactory @@ -63,6 +65,8 @@ def test_a_disposed_transport_raises_expected_errors(self) -> None: """ data_source = FakeDataSourceFactory() data_source_type = FakeDataSourceTypeFactory() + extract_meta = FakeExtractMetadataFactory() + upload_meta = FakeUploadMetadataFactory() self._transport.dispose() with patch("requests.sessions.Session.request", autospec=True) as s: s.return_value = self._mock_response_factory() @@ -70,11 +74,29 @@ def test_a_disposed_transport_raises_expected_errors(self) -> None: self._transport.fetch_data_sources( data_source_type=data_source_type ) - with pytest.raises(TransportClosedError): self._transport.fetch_data_source_extracts( data_source_type=data_source_type, data_source=data_source ) + with pytest.raises(TransportClosedError): + self._transport.mark_upload_as_complete( + upload_metadata=upload_meta + ) + + s.return_value = self._mock_response_factory(status_code=201) + with pytest.raises(TransportClosedError): + self._transport.post_upload_chunk( + upload_metadata=upload_meta, + chunk_content=b"Bla bla bla ...", + chunk_index=0, + ) + with pytest.raises(TransportClosedError): + self._transport.post_upload_metadata( + extract_metadata=extract_meta, + content_type="application/json", + org_unit_code="12345", + org_unit_name="Test Facility", + ) def test_fetch_data_source_extracts_returns_expected_value(self) -> None: """ @@ -101,6 +123,51 @@ def test_fetch_data_sources_returns_expected_value(self) -> None: results = self._transport.fetch_data_sources(data_source_type) self.assertListEqual(list(results), []) + def test_mark_upload_as_complete_exits_cleanly_on_valid_data(self) -> None: + """ + Assert that the ``mark_upload_as_complete()`` method returns without + raising any errors when given the correct data. + """ + upload_meta = FakeUploadMetadataFactory() + with patch("requests.sessions.Session.request", autospec=True) as s: + s.return_value = self._mock_response_factory() + self._transport.mark_upload_as_complete( + upload_metadata=upload_meta + ) + + def test_post_upload_chunk_returns_expected_value(self) -> None: + """ + Assert that the ``post_upload_chunk`` method returns the expected + value. + """ + upload_meta = FakeUploadMetadataFactory() + with patch("requests.sessions.Session.request", autospec=True) as s: + s.return_value = self._mock_response_factory(status_code=201) + result = self._transport.post_upload_chunk( + upload_metadata=upload_meta, + chunk_index=0, + chunk_content=b"Bla bla bla ...", + ) + + assert result # Should not be None or empty. + + def test_post_upload_metadata_returns_expected_value(self) -> None: + """ + Assert that the ``post_upload_metadata`` method returns the expected + value. + """ + extract_meta = FakeExtractMetadataFactory() + with patch("requests.sessions.Session.request", autospec=True) as s: + s.return_value = self._mock_response_factory(status_code=201) + result = self._transport.post_upload_metadata( + extract_metadata=extract_meta, + content_type="application/json", + org_unit_code="12345", + org_unit_name="Test Facility", + ) + + assert result # Should not be None or empty. + def test_transport_re_authentication_failure(self) -> None: """ Assert that if transport re-authenticates fails, the expected error is diff --git a/tests/use_cases/__init__.py b/tests/use_cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/use_cases/test_fetch_metadata.py b/tests/use_cases/test_fetch_metadata.py new file mode 100644 index 0000000..1165dc6 --- /dev/null +++ b/tests/use_cases/test_fetch_metadata.py @@ -0,0 +1,146 @@ +from typing import Sequence +from unittest import TestCase + +from app.core import DataSource, DataSourceType, Transport +from app.use_cases.fetch_metadata import ( + DoFetchDataSources, + DoFetchExtractMetadata, + FetchDataSources, + FetchExtractMetadata, +) +from tests.core.factories import ( + FakeDataSourceFactory, + FakeDataSourceTypeFactory, + FakeTransportFactory, +) + + +class TestDoFetchDataSources(TestCase): + """Tests for the :class:`DoFetchDataSources` class.""" + + def setUp(self) -> None: + super().setUp() + self._data_source_type: DataSourceType = FakeDataSourceTypeFactory() + self._instance: DoFetchDataSources = DoFetchDataSources( + data_source_type=self._data_source_type + ) + self._transport: Transport = FakeTransportFactory( + fetch_data_sources_count=5 + ) + + def test_execute_return_value(self) -> None: + """Assert that the ``execute()`` method returns the expected value.""" + data_sources = self._instance.execute(self._transport) + + assert len(data_sources) == 5 + + def test_execute_side_effects(self) -> None: + """ + Assert that the ``execute()`` method results in the ``data_sources`` + property of the target *data source type* being set. + """ + self._instance.execute(self._transport) + + assert len(self._data_source_type.data_sources) == 5 + + +class TestDoFetchExtractMetadata(TestCase): + """Tests for the :class:`DoFetchExtractMetadata` class.""" + + def setUp(self) -> None: + super().setUp() + self._data_source: DataSource = FakeDataSourceFactory() + self._instance: DoFetchExtractMetadata = DoFetchExtractMetadata( + data_source=self._data_source + ) + self._transport: Transport = FakeTransportFactory( + fetch_data_source_extracts_count=5 + ) + + def test_execute_return_value(self) -> None: + """Assert that the ``execute()`` method returns the expected value.""" + extracts = self._instance.execute(self._transport) + + assert len(extracts) == 5 + + def test_execute_side_effects(self) -> None: + """ + Assert that the ``execute()`` method results in the + ``extract_metadata`` property of the target *data source* being set. + """ + self._instance.execute(self._transport) + + assert len(self._data_source.extract_metadata) == 5 + + +class TestFetchDataSources(TestCase): + """Tests for the :class:`FetchDataSources` class.""" + + def setUp(self) -> None: + super().setUp() + self._max_data_source_types: int = 5 + self._max_data_sources: int = 3 + self._data_source_types: Sequence[DataSourceType] + self._data_source_types = tuple( + FakeDataSourceTypeFactory.create_batch( + size=self._max_data_source_types + ) + ) + self._transport: Transport = FakeTransportFactory( + fetch_data_sources_count=self._max_data_sources + ) + self._instance: FetchDataSources = FetchDataSources( + transport=self._transport + ) + + def test_execute_return_value(self) -> None: + """Assert that the ``execute()`` method returns the expected value.""" + data_sources = self._instance.execute(self._data_source_types) + + assert ( + len(data_sources) + == self._max_data_sources * self._max_data_source_types + ) + + def test_execute_side_effects(self) -> None: + """ + Assert that the ``execute()`` method results in the ``data_sources`` + property of each target *data source type* being set. + """ + self._instance.execute(self._data_source_types) + for _ds_type in self._data_source_types: + assert len(_ds_type.data_sources) == self._max_data_sources + + +class TestFetchExtractMetadata(TestCase): + """Tests for the :class:`FetchExtractMetadata` class.""" + + def setUp(self) -> None: + super().setUp() + self._max_data_sources: int = 4 + self._max_extracts: int = 7 + self._data_sources: Sequence[DataSource] + self._data_sources = tuple( + FakeDataSourceFactory.create_batch(size=self._max_data_sources) + ) + self._transport: Transport = FakeTransportFactory( + fetch_data_source_extracts_count=self._max_extracts + ) + self._instance: FetchExtractMetadata = FetchExtractMetadata( + transport=self._transport + ) + + def test_execute_return_value(self) -> None: + """Assert that the ``execute()`` method returns the expected value.""" + extracts = self._instance.execute(self._data_sources) + + assert len(extracts) == self._max_data_sources * self._max_extracts + + def test_execute_side_effects(self) -> None: + """ + Assert that the ``execute()`` method results in the + ``extract_metadata`` property of each target *data source* being set. + """ + self._instance.execute(self._data_sources) + for _data_source in self._data_sources: + assert len(_data_source.extract_metadata) == self._max_extracts diff --git a/tests/use_cases/test_run_extraction.py b/tests/use_cases/test_run_extraction.py new file mode 100644 index 0000000..e69de29