"""conftest.""" import io from typing import Any, Dict, Generator from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from owsclient import OwsClient from python_pdp_sdk.backends.authorization_backend import AuthorizationBackend from types_aiobotocore_s3 import S3Client from product_staging.allowed_value_sets.connectors.ows_pricing import PricingTier from product_staging.allowed_value_sets.connectors.ows_product import ( LocalizationLanguage, ) from product_staging.allowed_value_sets.connectors.ows_product_digital import Language from product_staging.allowed_value_sets.connectors.ows_project_manager import ( Genre, Subgenre, ) from product_staging.allowed_value_sets.connectors.ows_territories import Country from product_staging.api.auth import ( identity_uuid_from_scope, profiles_from_scope, # updated import ) from product_staging.api.datasources import ( get_authorization_backend, get_ows_client, get_s3_client, ) from product_staging.api.main import app as _app from product_staging.api.schemas.asset_report import BulkSessionAssetReport from product_staging.api.schemas.bulk_session_asset_file import BulkSessionAssetFileInfo from product_staging.api.schemas.metadata_error_report import ( Error, MetadataErrorReport, Product, Project, Track, ) @pytest.fixture def app( mock_s3_client: S3Client, mock_ows_client: OwsClient, mock_authorization_backend: AuthorizationBackend, identity_uuid: str, profiles_from_scope_fixture: list[tuple[str, int]], ) -> Generator[FastAPI, None, None]: _app.dependency_overrides[identity_uuid_from_scope] = lambda: identity_uuid _app.dependency_overrides[profiles_from_scope] = lambda: profiles_from_scope_fixture _app.dependency_overrides[get_s3_client] = lambda: mock_s3_client _app.dependency_overrides[get_ows_client] = lambda: mock_ows_client _app.dependency_overrides[get_authorization_backend] = ( lambda: mock_authorization_backend ) yield _app _app.dependency_overrides = {} @pytest.fixture def test_client(app: FastAPI) -> Generator[TestClient, None, None]: """Fastapi test client fixture.""" with TestClient(app) as client: yield client @pytest.fixture() def mock_s3_client(s3_upload_id: str, s3_filename: str) -> MagicMock: """Mock s3 client.""" s3_client = MagicMock(spec=S3Client) s3_client.create_multipart_upload = AsyncMock( return_value={"UploadId": s3_upload_id, "Key": s3_filename} ) return s3_client @pytest.fixture def mock_ows_client() -> OwsClient: """Mock ows_client.""" return MagicMock(spec=OwsClient) @pytest.fixture def mock_authorization_backend() -> AuthorizationBackend: """Mock authorization backend.""" mock_backend = MagicMock(spec=AuthorizationBackend) return mock_backend @pytest.fixture(scope="session", autouse=True) def anyio_backend() -> str: """Return AnyIO backend.""" return "asyncio" @pytest.fixture() def identity_uuid() -> str: """Return a valid identity_uuid for testing.""" return "10436b38-5e11-472d-b6a4-bf1ee2b1b438" @pytest.fixture() def profile_type() -> str: """Return a valid profile_type for testing.""" return "SettingsProfile" @pytest.fixture() def profile_id() -> int: """Return a valid profile_id for testing.""" return 12345 @pytest.fixture() def profiles_from_scope_fixture( profile_type: str, profile_id: int ) -> list[tuple[str, int]]: """Return a list of profiles for testing.""" return [(profile_type, profile_id)] @pytest.fixture() def bulk_session_uuids() -> list[str]: return [ "a642e216-e9d8-470a-9cc7-9959339fdb52", "8f2d830a-9d3f-4b1e-ada3-88ae22643b99", "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", ] @pytest.fixture() def bulk_session_uuid(bulk_session_uuids) -> str: """Return a valid bulk_session_id for testing.""" return bulk_session_uuids[0] @pytest.fixture() def bulk_session_ingestion_uuid() -> str: """Return a valid bulk_session_ingestion_uuid for testing.""" return "f3c2a1d4-8b6e-4a7f-9c2d-1e5f3a7b9c0d" @pytest.fixture() def bulk_session_metadata_file_uuid() -> str: """Return a valid bulk_session_metadata_file_id for testing.""" return "587c52d6-4b5b-4888-b5a0-f7905253899a" @pytest.fixture() def s3_upload_id() -> str: """Return an AWS upload id.""" return "XQJ_kUfUezUUKeaLZuFY6Z2Wfuh3xFkWFD5q4SLaFD8qNRFDbIzSV8qL7zWD1_9gT1aFhtfXZAhyvJaYQVBjP92uSyC1mrOfROOiI9YZfJtXS7Btkf3EJGeGYY7ef7_4" # noqa @pytest.fixture() def s3_filename() -> str: """Return an s3 filename.""" return "metadata/8b1eac57-1580-421a-be4a-d98226a42571" @pytest.fixture() def s3_asset_filename() -> str: """Return an s3 asset filename.""" return "assets/8b1eac57-1580-421a-be4a-d98226a42572" @pytest.fixture() def vendor_uuid() -> str: """Return a valid vendor_uuid for testing.""" return "35289af1-d291-44a9-96c1-72fce8ee1c48" @pytest.fixture def decoded_jwt(identity_uuid) -> Dict[str, Any]: """Return a JWT for testing.""" return { "https://grass.theorchard.com/profiles": [ { "brand": "orchard", "full_catalog_access": False, "profile_id": profile_id, "profile_name": "Test Profile Name", "profile_type": profile_type, "uuid": "123ab1a1-123a-12b2-1234-e12ce34aeefb", } ], "https://grass.theorchard.com/version": "0.2", "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_uuid }, "https://grass.theorchard.com/app_metadata": {}, "https://grass.theorchard.com/internal_employee": False, "https://grass.theorchard.com/brand": "orchard", "iss": "https://qalogin.theorchard.com/", "sub": "google-apps|test@theorchard.com", "aud": [ "https://workstation.qaorch.com/api", "https://qa-orchard.auth0.com/userinfo", ], "iat": 1234567891, "exp": 9876543210, "azp": "test123", "scope": "openid profile email offline_access", "org_id": "org_test123456", } @pytest.fixture() def asset_metadata_json_string() -> str: """Return a partial metadata json string for asset mapping""" return """ [ { "product_info": { "product": { "product_name": "test 1", "artwork": { "filename": "test-1.jpg", "bucket": null, "key": null } }, "tracks": [ { "asset": { "filename": "test-1-track-1.wav", "bucket": null, "key": null } }, { "asset": { "filename": "test-1-track-2.wav", "bucket": null, "key": null } } ] } }, { "product_info": { "product": { "product_name": "test 2", "artwork": { "filename": "test-2.jpg", "bucket": null, "key": null } }, "tracks": [ { "asset": { "filename": "test-2-track-1.wav", "bucket": null, "key": null } } ] } }, { "product_info": { "product": { "product_name": "test 3", "artwork": { "filename": "test-3.jpg", "bucket": null, "key": null } }, "tracks": [ { "asset": { "filename": "test-3-track-1.wav", "bucket": null, "key": null } }, { "asset": { "filename": "test-3-track-2.wav", "bucket": null, "key": null } }, { "asset": { "filename": "test-3-track-3.wav", "bucket": null, "key": null } } ] } } ] """ @pytest.fixture() def bulk_session_asset_files_uploading() -> list[BulkSessionAssetFileInfo]: return [ BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "s3_filename", "original_filename": "test.jpg", "file_status": "uploading", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "s3_filename", "original_filename": "test-1.wav", "file_status": "uploading", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "s3_filename", "original_filename": "test-2.wav", "file_status": "uploading", } ), ] @pytest.fixture() def bulk_session_asset_files_validating() -> list[BulkSessionAssetFileInfo]: return [ BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "s3_filename", "original_filename": "test.jpg", "file_status": "validating", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "s3_filename", "original_filename": "test-1.wav", "file_status": "validating", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "s3_filename", "original_filename": "test-2.wav", "file_status": "success", } ), ] @pytest.fixture() def bulk_session_asset_files_failures() -> list[BulkSessionAssetFileInfo]: return [ BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "test-1.jpg", "original_filename": "test-1.jpg", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-1-track-1.wav", "original_filename": "test-1-track-1.wav", "file_status": "failure", "error_message": "Audio file has 1 channel(s). Channels must equal 2.", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-2-track-1.wav", "original_filename": "test-2-track-1.wav", "file_status": "failure", "error_message": "Audio file has mime type of image/jpeg. " + "Mime type must be one of the following: audio/mp4, audio/flac, audio/wav;" + "Audio file has 1 channel(s). Channels must equal 2.", } ), ] @pytest.fixture() def bulk_session_asset_files_success_incomplete() -> list[BulkSessionAssetFileInfo]: return [ BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "test-1.jpg", "original_filename": "test-1.jpg", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-1-track-1.wav", "original_filename": "test-1-track-1.wav", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-2-track-1.wav", "original_filename": "test-2-track-1.wav", "file_status": "success", } ), ] @pytest.fixture() def bulk_session_asset_files_success_complete() -> list[BulkSessionAssetFileInfo]: return [ BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "test-1.jpg", "original_filename": "test-1.jpg", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-1-track-1.wav", "original_filename": "test-1-track-1.wav", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-1-track-2.wav", "original_filename": "test-1-track-2.wav", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "artwork", "s3_filename": "test-2.jpg", "original_filename": "test-2.jpg", "file_status": "success", } ), BulkSessionAssetFileInfo.model_validate( { "bulk_session_asset_file_id": "042d768e-fe11-4d93-8c70-88228c4c6516", "bulk_session_id": "13d8ed7b-5f2b-4492-9ab0-ed24eac37206", "type": "audio", "s3_filename": "test-2-track-1.wav", "original_filename": "test-2-track-1.wav", "file_status": "success", } ), ] @pytest.fixture() def metadata_json_for_asset_status() -> str: """Return a partial metadata json string for asset mapping""" return """ [ { "product_info": { "product": { "product_name": "test 1", "artwork": { "filename": "test-1.jpg", "bucket": "qa-ows-product-staging", "key": "test-1.jpg" } }, "tracks": [ { "asset": { "filename": "test-1-track-1.wav", "bucket": "qa-ows-product-staging", "key": "test-1-track-1.wav" } }, { "asset": { "filename": "test-1-track-2.wav", "bucket": "qa-ows-product-staging", "key": "test-1-track-2.wav" } } ] } }, { "product_info": { "product": { "product_name": "test 2", "artwork": { "filename": "test-2.jpg", "bucket": "qa-ows-product-staging", "key": "test-2.jpg" } }, "tracks": [ { "asset": { "filename": "test-2-track-1.wav", "bucket": "qa-ows-product-staging", "key": "test-2-track-1.wav" } } ] } } ] """ @pytest.fixture() def metadata_json_missing_assets() -> str: """Return a partial metadata json string with missing assets for asset mapping""" return """ [ { "product_info": { "product": { "product_name": "test 1", "release_reference": "test1", "artwork": { "filename": "test-1.jpg", "bucket": "qa-ows-product-staging", "key": "test-1.jpg" } }, "tracks": [ { "track_name": "test 1 track 1", "asset": { "filename": "test-1-track-1.wav", "bucket": "qa-ows-product-staging", "key": "test-1-track-1.wav" } }, { "track_name": "test 1 track 2", "asset": { "filename": "test-1-track-2.wav", "bucket": null, "key": null } } ] } }, { "product_info": { "product": { "product_name": "test 2", "release_reference": "test2", "artwork": { "filename": "test-2.jpg", "bucket": null, "key": null } }, "tracks": [ { "track_name": "test 2 track 1", "asset": { "filename": "test-2-track-1.wav", "bucket": "qa-ows-product-staging", "key": "test-2-track-1.wav" } } ] } } ] """ @pytest.fixture() def asset_error_report() -> BulkSessionAssetReport: return BulkSessionAssetReport.model_validate( { "products": [ { "product_name": "test 1", "product_code": "test1", "artwork": { "status": "Success", "errors": None, "filename": "test-1.jpg", }, "tracks": [ { "track_name": "test 1 track 1", "status": "Fail", "errors": [ "Audio file has 1 channel(s). Channels must equal 2." ], "filename": "test-1-track-1.wav", }, { "track_name": "test 1 track 2", "status": "Fail", "errors": [ "No audio file uploaded with the filename test-1-track-2.wav" ], "filename": "test-1-track-2.wav", }, ], }, { "product_name": "test 2", "product_code": "test2", "artwork": { "status": "Fail", "errors": ["No image uploaded with the filename test-2.jpg"], "filename": "test-2.jpg", }, "tracks": [ { "track_name": "test 2 track 1", "status": "Fail", "errors": [ "Audio file has mime type of image/jpeg. " + "Mime type must be one of the following: audio/mp4, audio/flac, audio/wav", "Audio file has 1 channel(s). Channels must equal 2.", ], "filename": "test-2-track-1.wav", }, ], }, ], "total_product_count": 2, "complete_product_count": 0, "download_link": "test.com", } ) @pytest.fixture() def asset_error_report_json() -> str: return """ { "products": [ { "product_name": "test 1", "product_code": "test1", "artwork": { "status": "Success", "errors": null, "filename": "test-1.jpg" }, "tracks": [ { "track_name": "test 1 track 1", "status": "Fail", "errors": [ "Audio file has 1 channel(s). Channels must equal 2." ], "filename": "test-1-track-1.wav" }, { "track_name": "test 1 track 2", "status": "Fail", "errors": [ "No audio file uploaded with the filename test-1-track-2.wav" ], "filename": "test-1-track-2.wav" } ] }, { "product_name": "test 2", "product_code": "test2", "artwork": { "status": "Fail", "errors": ["No image uploaded with the filename test-2.jpg"], "filename": "test-2.jpg" }, "tracks": [ { "track_name": "test 2 track 1", "status": "Fail", "errors": [ "Audio file has mime type of image/jpeg. Mime type must be one of the following: audio/mp4, audio/flac, audio/wav", "Audio file has 1 channel(s). Channels must equal 2." ], "filename": "test-2-track-1.wav" } ] } ], "total_product_count": 2, "complete_product_count": 0, "download_link": "test.com" } """ @pytest.fixture def bulk_session_ingestion_product_list() -> list[dict[str, Any]]: return [ { "bulk_session_ingestion_product_id": "67eb3227-2898-4395-a32c-c4dd3c777e24", "bulk_session_ingestion_id": "d5bbb02c-29dc-45f3-a5fd-3ce81ebe65f3", "product_id": 6044661, "upc": "8960068512345", "product_code": "ABC", "ingestion_status": "failure", "submit_status": "failure", "created_on": "2025-07-08T11:12:52", "project_code": "KW-100", "product_name": "Test product 1", }, { "bulk_session_ingestion_product_id": "67eb3227-2898-4395-a32c-c4dd3c777e25", "bulk_session_ingestion_id": "d5bbb02c-29dc-45f3-a5fd-3ce81ebe65f3", "product_id": 6044662, "upc": "8960068523456", "product_code": "BCD", "ingestion_status": "failure", "submit_status": "failure", "created_on": "2025-07-08T11:12:53", "project_code": "KW-101", "product_name": "Test product 2", }, { "bulk_session_ingestion_product_id": "67eb3227-2898-4395-a32c-c4dd3c777e26", "bulk_session_ingestion_id": "d5bbb02c-29dc-45f3-a5fd-3ce81ebe65f3", "product_id": 6044663, "upc": "8960068534567", "product_code": "CDE", "ingestion_status": "failure", "submit_status": "failure", "created_on": "2025-07-08T11:12:54", "project_code": "KW-102", "product_name": "Test product 3", }, { "bulk_session_ingestion_product_id": "67eb3227-2898-4395-a32c-c4dd3c777e27", "bulk_session_ingestion_id": "d5bbb02c-29dc-45f3-a5fd-3ce81ebe65f3", "product_id": 6044664, "upc": "8960068545678", "product_code": "DEF", "ingestion_status": "failure", "submit_status": "failure", "created_on": "2025-07-08T11:12:55", "project_code": "KW-103", "product_name": "Test product 4", }, ] @pytest.fixture def ows_project_manager_get_genres_result() -> list[Genre]: return [ Genre( id=1, name="Rock", subgenres=[ Subgenre( id=3, name="Classic Rock", composer=False, ), Subgenre( id=7, name="Indie Rock", composer=False, ), ], ) ] @pytest.fixture def ows_product_digital_get_languages_result() -> list[Language]: return [ Language.model_validate( { "apple_compatible": True, "code": "ENG", "iso_639_1_code": "EN", "iso_639_3_code": "", "iso_code": "EN", "name": "English", } ), Language.model_validate( { "apple_compatible": True, "code": "ITA", "iso_639_1_code": "IT", "iso_639_3_code": "", "iso_code": "IT", "name": "Italian", } ), ] @pytest.fixture def ows_product_get_localization_languages_result() -> list[LocalizationLanguage]: return [ LocalizationLanguage.model_validate( { "language": "English", "language_code": "en", "language_id": 12, "meta_language_code": "ENG", } ), LocalizationLanguage.model_validate( { "language": "Italian", "language_code": "it", "language_id": 23, "meta_language_code": "ITA", } ), ] @pytest.fixture def ows_territories_get_territories_result() -> list[Country]: return [ Country.model_validate( { "continent": "Europe", "orch_id": 3, "standard": "Orch_1_2016", "territory_code_a2": "GB", "territory_code_a3": "GBR", "territory_code_numeric": 826, "territory_name": "United Kingdom", } ), Country.model_validate( { "continent": "North America", "orch_id": 1, "standard": "Orch_1_2016", "territory_code_a2": "US", "territory_code_a3": "USA", "territory_code_numeric": 840, "territory_name": "USA", } ), ] @pytest.fixture def ows_pricing_get_families_pricing_tiers_result() -> dict[int, list[PricingTier]]: return { 2: [ PricingTier.model_validate( {"orchard_pricing_tier_id": 1, "name": "Deluxe Four"} ), PricingTier.model_validate( {"orchard_pricing_tier_id": 2, "name": "Deluxe Three"} ), ], 3: [ PricingTier.model_validate({"orchard_pricing_tier_id": 3, "name": "Front"}), PricingTier.model_validate({"orchard_pricing_tier_id": 4, "name": "Back"}), ], } @pytest.fixture def metadata_error_report_json() -> str: return """ { "projects": [ { "project_code": "KWR-003", "name": "Vanishing Point", "errors": [] }, { "project_code": "KWR-005", "name": "Test Project", "errors": [ { "sheet": "Projects", "row": 5, "column": "Project Artist", "message": "This value cannot be blank.", "value": "None" } ] } ], "products": [ { "product_code": "PR-001", "name": "Test 1", "upc": "89600685980114", "tracks": [ { "volume": 1, "number": 1, "name": "Letter", "errors": [] } ], "errors": [ { "sheet": "Products", "row": 3, "column": "UPC", "message": "This value is too long (>13)", "value": "89600685980114" } ] }, { "product_code": "PR-002", "name": "Test 2", "upc": "8960068598013", "tracks": [ { "volume": 1, "number": 2, "name": "Wedge One", "errors": [ { "sheet": "Tracks", "row": 4, "column": "Track Number", "message": "Track Number* does not start at 1 for Product Code* PR-002, Volume* 1", "value": "2" } ] } ], "errors": [] }, { "product_code": "PR-003", "name": "Test 3", "upc": "None", "tracks": [ { "volume": 1, "number": 1, "name": "test", "errors": [] } ] } ], "project_error_count": 1, "product_error_count": 2 } """ @pytest.fixture def metadata_error_report() -> MetadataErrorReport: return MetadataErrorReport( projects=[ Project( project_code="KWR-003", name="Vanishing Point", errors=[], ), Project( project_code="KWR-005", name="Test Project", errors=[ Error( sheet="Projects", row=5, column="Project Artist", message="This value cannot be blank.", value="None", ), ], ), ], products=[ Product( product_code="PR-001", name="Test 1", upc="89600685980114", tracks=[ Track( volume=1, number=1, name="Letter", errors=[], ), ], errors=[ Error( sheet="Products", row=3, column="UPC", message="This value is too long (>13)", value="89600685980114", ), ], ), Product( product_code="PR-002", name="Test 2", upc="8960068598013", tracks=[ Track( volume=1, number=2, name="Wedge One", errors=[ Error( sheet="Tracks", row=4, column="Track Number", message="Track Number* does not start at 1 for Product Code* PR-002, Volume* 1", value="2", ), ], ), ], errors=[], ), Product( product_code="PR-003", name="Test 3", upc="None", tracks=[ Track( volume=1, number=1, name="test", errors=[], ), ], ), ], project_error_count=1, product_error_count=2, ) @pytest.fixture def metadata_error_report_with_limit_offset() -> MetadataErrorReport: return MetadataErrorReport( projects=[ Project( project_code="KWR-005", name="Test Project", errors=[ Error( sheet="Projects", row=5, column="Project Artist", message="This value cannot be blank.", value="None", ), ], ), ], products=[ Product( product_code="PR-003", name="Test 3", upc="None", tracks=[ Track( volume=1, number=1, name="test", errors=[], ) ], ), ], project_error_count=1, product_error_count=2, ) @pytest.fixture def excel_file() -> io.BytesIO: with open("tests/unit/data/example.xlsx", mode="rb") as f: return io.BytesIO(f.read()) @pytest.fixture def metadata_json_from_spreadsheet() -> list: return [ { "product_info": { "product": {"release_reference": "ABC123", "product_name": "Product A"}, "project": {"project_code": "Project 1"}, } } ] @pytest.fixture def execution_arn() -> str: return ( "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:execution-name" )