# pylint: skip-file import json import os from datetime import datetime from os.path import join as path_join from typing import Dict, List, Optional from unittest import mock import boto3 import pytest from _pytest.fixtures import SubRequest from db_schema.postgres.connection import Connection from db_schema.schemas import slz from moto import mock_s3 from mypy_boto3_s3 import S3Client from slz_storage.repository import Repository from slz_apple_music_charts_scrapper.entities import Config, Payload from slz_apple_music_charts_scrapper.manager import ApplicationManager from slz_apple_music_charts_scrapper.services.content_status import ContentStatusService from slz_apple_music_charts_scrapper.services.file_storage import FileStorageService from slz_apple_music_charts_scrapper.services.snapshot import SnapshotService from slz_apple_music_charts_scrapper.services.validator import ValidatorService from slz_apple_music_charts_scrapper.services.vendor_api import VendorAPIService from tests import FIXTURES_PATH from tests.entities import BucketPathsTuple, CommonTestValues @pytest.fixture def common_test_values() -> CommonTestValues: return CommonTestValues( uow_code='apple-20211201-sme-charts_daily-v1', report_date='2021-12-01', report_type='charts_daily', version='v1', test_storefront='us', test_playlist='pl.606afcbb70264d2eb2b51d8dbcfa6a12', dsp='apple', validation_schema_date='2021-10-01' ) @pytest.fixture def config_test() -> Config: return Config( auth_client_id='auth_client_id', auth_client_secret='auth_client_secret', auth_audience='auth_audience', auth_token_url='auth_token_url', vendor_api_host='vendor_api_host', config_bucket='config_bucket', decompressed_bucket='decompressed_bucket', quarantine_bucket='quarantine_bucket', corrupted_bucket='corrupted_bucket', rds_secret_key='rds_secret_key', sentry_secret_key='sentry_secret_key', aws_region='aws_region', aws_sfn_task_token='aws_sfn_task_token', aws_batch_job_id='aws_batch_job_id', ) @pytest.fixture def payload_test(common_test_values: CommonTestValues) -> Payload: return Payload( uow_id=common_test_values.uow_code, job_id='payload_job_id', dsp=common_test_values.dsp, version=common_test_values.version, licensor='sme', report_date=common_test_values.report_date, report_type=common_test_values.report_type, storefronts=[common_test_values.test_storefront], sync=True, ) @pytest.fixture(scope='session') def db() -> Connection: def _loader(): return { 'active_endpoint': os.environ.get('PG_HOST', '0.0.0.0'), 'port': os.environ.get('PG_PORT', 5432), 'database': os.environ.get('PG_DB', 'slz'), 'username': os.environ.get('PG_USER', 'admin'), 'password': os.environ.get('PG_PASSWORD', 'admin'), } connection = Connection( credentials_loader=_loader, name='slz_apple_music_charts_scrapper_testing', version='v1', engine_params={ 'pool_size': 2, 'max_overflow': 1 } ) yield connection connection.session.rollback() for model in (slz.ContentFailureLog, slz.Snapshot, slz.ContentStatus, slz.UnitOfWork): connection.session.query(model).delete() connection.session.commit() connection.disconnect() @pytest.fixture def unit_of_work(common_test_values: CommonTestValues, db: Connection) -> slz.UnitOfWork: report = db.session.query(slz.Report).filter(slz.Report.report_name == 'charts_daily').one() unit_of_work = slz.UnitOfWork( unit_of_work_code=common_test_values.uow_code, reprocess_id='', report_date=common_test_values.report_date, report_id=report.report_id, licensor_id=1, version=common_test_values.version, activity_status='NOT_IN_PROGRESS', completeness_status='ACTIVE', next_run_at=datetime.strptime('2021-12-02 08:26:28+0000', '%Y-%m-%d %H:%M:%S%z'), created_at=datetime.strptime('2021-12-01 01:00:17+0000', '%Y-%m-%d %H:%M:%S%z'), last_updated_at=datetime.strptime('2021-12-01 08:20:58+0000', '%Y-%m-%d %H:%M:%S%z'), is_force_complete=False, priority=5 ) db.session.add(unit_of_work) db.session.flush() yield unit_of_work db.session.query(slz.UnitOfWork).delete() db.session.commit() @pytest.fixture def content_status(db: Connection, unit_of_work: slz.UnitOfWork) -> slz.ContentStatus: content_status = slz.ContentStatus( unit_of_work_id=unit_of_work.unit_of_work_id, context='us', content_status=slz.ContentStatusEnum.ACTIVE, content_name='pl.606afcbb70264d2eb2b51d8dbcfa6a12_us.json', content_size=1, failure_count=0, latest_job_id='content_status_test_job_id__charts_daily_regional', created_at=datetime.strptime('2021-12-01 12:30:17+0000', '%Y-%m-%d %H:%M:%S%z'), last_checked_at=datetime.strptime('2021-12-02 12:30:17+0000', '%Y-%m-%d %H:%M:%S%z'), metadata_process_status='NOT_QUEUED', ) db.session.add(content_status) db.session.flush() yield content_status db.session.query(slz.ContentFailureLog).delete() db.session.query(slz.ContentStatus).delete() db.session.commit() @pytest.fixture def content_status_of_status( db: Connection, unit_of_work: slz.UnitOfWork, request: SubRequest, ) -> slz.ContentStatus: content_status = slz.ContentStatus( unit_of_work_id=unit_of_work.unit_of_work_id, context='us', content_status=request.param, content_name='pl.606afcbb70264d2eb2b51d8dbcfa6a12_us.json', content_size=1, failure_count=0, latest_job_id='content_status_test_job_id__charts_daily_regional', created_at=datetime.strptime('2021-12-01 12:30:17+0000', '%Y-%m-%d %H:%M:%S%z'), last_checked_at=datetime.strptime('2021-12-02 12:30:17+0000', '%Y-%m-%d %H:%M:%S%z'), metadata_process_status='NOT_QUEUED', ) db.session.add(content_status) db.session.flush() yield content_status db.session.query(slz.ContentFailureLog).delete() db.session.query(slz.ContentStatus).delete() db.session.commit() @pytest.fixture def snapshot(db: Connection, content_status: slz.ContentStatus) -> slz.Snapshot: snapshot = slz.Snapshot( content_status_id=content_status.content_status_id, file_name=content_status.content_name, hash='7c00ff0338fcbc78d6a4487fda47c608', created_at=datetime.strptime('2021-12-01 12:08:04+0000', '%Y-%m-%d %H:%M:%S%z'), ) db.session.add(snapshot) db.session.commit() yield snapshot db.session.query(slz.Snapshot).delete() db.session.commit() @pytest.fixture def snapshot_of_hash( db: Connection, content_status: slz.ContentStatus, request: SubRequest, ) -> slz.Snapshot: """Snapshot instance with indirect hash, intended for integration processing testing.""" snapshot = slz.Snapshot( content_status_id=content_status.content_status_id, file_name=content_status.content_name, # This hash is expected for current Spotify response fixture. hash=request.param, created_at=datetime.strptime('2021-12-01 12:08:04+0000', '%Y-%m-%d %H:%M:%S%z'), ) db.session.add(snapshot) db.session.commit() yield snapshot db.session.query(slz.Snapshot).delete() db.session.commit() @pytest.fixture def slz_repository(db: Connection) -> Repository: return Repository( logger=mock.Mock(), connection=db, ) @pytest.fixture def content_status_service( payload_test: Payload, slz_repository: Repository, ) -> ContentStatusService: service = ContentStatusService( logger=mock.Mock(), payload=payload_test, slz_pg_repository=slz_repository, ) yield service slz_repository.connection.query(slz.ContentFailureLog).delete() slz_repository.connection.query(slz.ContentStatus).delete() slz_repository.connection.commit() @pytest.fixture def snapshot_service(db: Connection, payload_test: Payload) -> SnapshotService: service = SnapshotService( logger=mock.Mock(), payload=payload_test, connection=db, ) yield service db.session.query(slz.Snapshot).delete() db.session.commit() @pytest.fixture def s3_client(config_test: Config) -> S3Client: with mock_s3(): buckets = [ config_test.config_bucket, config_test.quarantine_bucket, config_test.decompressed_bucket, config_test.corrupted_bucket ] _s3_client: S3Client = boto3.client('s3') for bucket in buckets: _s3_client.create_bucket(Bucket=bucket) yield _s3_client s3_resource = boto3.resource('s3') for bucket in buckets: bucket = s3_resource.Bucket(bucket) bucket.objects.all().delete() bucket.delete() @pytest.fixture def file_storage_service(s3_client: S3Client, payload_test: Payload) -> FileStorageService: # Fixture s3_client is required to enable boto3 mock. return FileStorageService(logger=mock.Mock(), payload=payload_test) @pytest.fixture def validation_schema__indirect( config_test: Config, common_test_values: CommonTestValues, s3_client: S3Client, # Required for boto3 mocking request: SubRequest, ) -> None: v = common_test_values schema_path = os.path.join(FIXTURES_PATH, 'validation', request.param) with open(schema_path, 'r') as _file: s3_client.put_object( Body=_file.read(), Bucket=config_test.config_bucket, Key=f'schemas/{v.dsp}/{v.report_type}/{v.version}/{v.validation_schema_date}.json', ) @pytest.fixture def validation_schema__valid( config_test: Config, common_test_values: CommonTestValues, s3_client: S3Client, # Required for boto3 mocking ) -> None: v = common_test_values schema_path = os.path.join(FIXTURES_PATH, 'validation', 'valid.json') with open(schema_path, 'r') as _file: s3_client.put_object( Body=_file.read(), Bucket=config_test.config_bucket, Key=f'schemas/{v.dsp}/{v.report_type}/{v.version}/{v.validation_schema_date}.json', ) @pytest.fixture def bucket_paths_stateless( config_test: Config, common_test_values: CommonTestValues, file_storage_service: FileStorageService ) -> BucketPathsTuple: def _concat_path(service: FileStorageService, bucket: str) -> str: return path_join( service._build_target_directory_path(bucket), service._build_file_name( common_test_values.test_storefront, common_test_values.test_playlist ), ) return BucketPathsTuple( quarantine=_concat_path(file_storage_service, config_test.quarantine_bucket), corrupted=_concat_path(file_storage_service, config_test.corrupted_bucket), decompressed=_concat_path(file_storage_service, config_test.decompressed_bucket), ) @pytest.fixture def vendor_api_service( config_test: Config, payload_test: Payload, ) -> VendorAPIService: return VendorAPIService( config=config_test, payload=payload_test, logger=mock.Mock(), ) @pytest.fixture def validator_service( config_test: Config, payload_test: Payload, ): return ValidatorService( logger=mock.Mock(), config=config_test, payload=payload_test, ) @pytest.fixture def vendor_api_service_stateless( config_test: Config, payload_test: Payload, ) -> VendorAPIService: service = VendorAPIService( config=config_test, payload=payload_test, logger=mock.Mock(), ) def _mocked_get_apple_playlist(storefront: str, playlist_id: str) -> Dict: # Replace dot in playlist_id with underscore just to be on the safe side. response_path = os.path.join( f'{FIXTURES_PATH}/data', f'{storefront}_{playlist_id.replace(".", "_")}.json' ) with open(response_path, 'r') as _file: data = _file.read() return json.loads(data.encode('utf-8')) def _mocked_get_apple_songs( storefront: str, ids: List[str], include: Optional[List[str]] = None, ) -> Dict: response_path = os.path.join(f'{FIXTURES_PATH}/data', f'{storefront}_tracks.json') with open(response_path, 'r') as _file: data = _file.read() data = json.loads(data.encode('utf-8')) data['data'] = list(filter(lambda item: item['id'] in ids, data['data'])) return data service._get_auth_token = mock.Mock(return_value='dummy_token') service._send_request = mock.Mock() # Better mock it for safety reasons service.get_apple_playlist = mock.Mock(side_effect=_mocked_get_apple_playlist) service.get_apple_songs = mock.Mock(side_effect=_mocked_get_apple_songs) return service @pytest.fixture def application_manager_stateless( config_test: Config, payload_test: Payload, file_storage_service: FileStorageService, vendor_api_service_stateless: VendorAPIService, ) -> ApplicationManager: return ApplicationManager( logger=mock.Mock(), config=config_test, payload=payload_test, vendor_api_service=vendor_api_service_stateless, fs_service=file_storage_service, snapshot_service=mock.Mock(), content_status_service=mock.Mock(), validator_service=mock.Mock(), slz_pg_repository=mock.Mock(), ) @pytest.fixture def application_manager( config_test: Config, payload_test: Payload, file_storage_service: FileStorageService, vendor_api_service_stateless: VendorAPIService, snapshot_service: SnapshotService, content_status_service: ContentStatusService, validator_service, slz_repository: Repository, ) -> ApplicationManager: return ApplicationManager( logger=mock.Mock(), config=config_test, payload=payload_test, vendor_api_service=vendor_api_service_stateless, fs_service=file_storage_service, snapshot_service=snapshot_service, content_status_service=content_status_service, validator_service=validator_service, slz_pg_repository=slz_repository, )