# pylint: disable=unused-argument,too-many-locals,redefined-outer-name,protected-access,fixme # pylint: disable=too-many-arguments import json import typing as t from contextlib import nullcontext as does_not_raise from unittest import mock import pytest import smart_open from _pytest.fixtures import FixtureRequest from _pytest.python_api import RaisesContext from boto3 import client as RealClient from boto3_type_annotations.s3 import Client as S3Client from structlog import BoundLogger from dapd_transformation_service.entities.config import Config from dapd_transformation_service.entities.worker_control import ConsumerMetrics, KinesisEvent from dapd_transformation_service.exceptions import DataDownloadError from dapd_transformation_service.services.consumer import ConsumerService DEFAULT_KINESIS_STREAM_NAME = 'dev-delphi-dapd-apple_music-albums' # FIXME: This is somehow bad. When we create an s3_client through fixture, we need to know # the name of the bucket. In other words the name of the bucket must be centralized. # Must find better approach. DEFAULT_TEST_FILE_PATH = 's3://test_bucket/test_data.json' INVALID_TEST_FILE_PATH = 's3://test_bucket/test_data_1.json' def boto3_mock_client_side_effect(service_name: str): if service_name == 'kinesis': return mock.Mock() return RealClient(service_name) @pytest.fixture def record_test_old_style() -> t.Dict[str, bytes]: return {'Data': json.dumps({'test': 'data'}).encode('utf-8')} @pytest.fixture def default_event_data() -> t.Dict[str, str]: return { 'id': 'id', 'type': 'type', 'created_at': '2022-01-26T16:46:48', 'storefront': 'global', 'data_source': 'apple_music', 'application': 'dapd_public_api_scraper@1.14.0', 'file_path': 'test_path', } @pytest.fixture def record_test_new_style(default_event_data) -> t.Dict[str, bytes]: return {'Data': json.dumps(default_event_data).encode('utf-8')} @pytest.fixture def default_kinesis_event(default_event_data) -> KinesisEvent: return KinesisEvent(**{**default_event_data, 'file_path': DEFAULT_TEST_FILE_PATH}) @pytest.fixture def prepare_s3_test_data(): with smart_open.open(DEFAULT_TEST_FILE_PATH, 'wb') as _file: _file.write(b'prepared_test_data') @pytest.mark.parametrize( 'records_value, expected', [ ('', ([], None)), (None, ([], None)), ({'Records': ['test_record']}, (['test_record'], None)), ({'Records': ['test_record'], 'NextShardIterator': None}, (['test_record'], None)), ({'Records': ['test_record'], 'NextShardIterator': 'test'}, (['test_record'], 'test')), ] ) # yapf: disable @mock.patch('dapd_transformation_service.services.consumer.boto3.client') def test__get_records_from_shard_iterator( mocked_boto3_client: mock.Mock, # Required during consumer initialization config: Config, logger: BoundLogger, records_value: t.Union[str, None, t.Dict[str, t.Any]], expected: t.Tuple[t.List[str], t.Optional[str]], ): stream_name = DEFAULT_KINESIS_STREAM_NAME config.kinesis_stream_name = stream_name consumer = ConsumerService( logger=logger, config=config, session=mock.Mock(), consumer_metrics=ConsumerMetrics(), upserter_service=mock.MagicMock(), ) consumer.client.get_records.return_value = records_value result = consumer._get_records_from_shard_iterator(shard_iterator='test') assert result == expected @pytest.mark.parametrize( 'record_fixture_name, file_path, expected_result, expected_exception', [ # Case 1: Legacy style. Valid response. ( 'record_test_old_style', '', (None, b'{"test": "data"}'), does_not_raise() ), # Case 2: New style. Provided file path has invalid format. ( 'record_test_new_style', '834975n0235', (None, ''), pytest.raises(DataDownloadError) ), # Case 3: New style. Provided file path leads to the file that does not exist. ( 'record_test_new_style', INVALID_TEST_FILE_PATH, (None, ''), pytest.raises(DataDownloadError), ), # Case 4: New style. Provided file path has duplicated protocol. ( 'record_test_new_style', f's3://{DEFAULT_TEST_FILE_PATH}', (None, 'prepared_test_data'), pytest.raises(DataDownloadError), ), # Case 4: New style. Provided file path leads to the file in local storage, # but file does not exist. ( 'record_test_new_style', './non_existing_folder/test.json', (None, 'prepared_test_data'), pytest.raises(DataDownloadError), ), # Case 4: New style. Valid response. ( 'record_test_new_style', DEFAULT_TEST_FILE_PATH, ('default_kinesis_event', b'prepared_test_data'), does_not_raise(), ), ] ) # yapf: disable @mock.patch('dapd_transformation_service.services.consumer.boto3.client') def test__get_data_from_record( mocked_boto3_client: mock.Mock, # Required during consumer initialization. Should go first s3_client: S3Client, # Required to mock smart_open client config: Config, logger: BoundLogger, prepare_s3_test_data: None, record_fixture_name: str, file_path: str, expected_result: t.Union[t.Optional[str], t.Dict[str, t.Any]], expected_exception: t.Union[does_not_raise, RaisesContext], request: FixtureRequest, ): stream_name = DEFAULT_KINESIS_STREAM_NAME config.kinesis_stream_name = stream_name mocked_boto3_client.side_effect = boto3_mock_client_side_effect consumer = ConsumerService( logger=logger, config=config, session=mock.Mock(), consumer_metrics=ConsumerMetrics(), upserter_service=mock.MagicMock(), ) # Provide expected KinesisEvent from fixtures. expected_event, expected_data = expected_result if expected_event is not None: expected_result = (request.getfixturevalue(expected_event), expected_data) # If Kinesis message is of new style, inject file_path into it. record_test: t.Dict[str, t.Union[bytes, dict]] = request.getfixturevalue(record_fixture_name) if record_fixture_name == 'record_test_new_style': record_test['Data']: t.Dict[str, t.Any] = json.loads(record_test['Data']) record_test['Data']['file_path'] = file_path record_test['Data']: bytes = json.dumps(record_test['Data']).encode('utf-8') with expected_exception: result = consumer._get_data_from_record(record_test) assert result == expected_result