import uuid from collections.abc import Generator from typing import Any import pytest from aws_testing_utils.lambda_handler import LambdaHandler from test_fixtures.mysql import MySQLConnection from config import ENSURE_ARTISTS_FUNCTION_NAME _ORIGINATING_VENDOR_ID = 6971 _DESTINATION_VENDOR_ID = 7123 @pytest.fixture def transfer_job_with_history( art_relations_db: MySQLConnection, ) -> Generator[dict[str, Any], None, None]: # Find a project for the source vendor that has at least one release project = art_relations_db.fetchone( """ SELECT p.project_id, r.release_id FROM project p JOIN releases r ON r.project_id = p.project_id WHERE p.vendor_id = %s LIMIT 1 """, (_ORIGINATING_VENDOR_ID,), ) assert project, f"No project with releases found for vendor_id {_ORIGINATING_VENDOR_ID}" # Resolve a valid source_artist_id that ows-artist already knows for this vendor artist = art_relations_db.fetchone( "SELECT artist_id FROM artist_info WHERE vendor_id = %s LIMIT 1", (_ORIGINATING_VENDOR_ID,), ) assert artist, f"No artist found for vendor_id {_ORIGINATING_VENDOR_ID} in artist_info" # Seed the transfer job. originating_artist_id is required by the lambda # (it raises if NULL) and is included alongside product source artists in the # bulk-ensure call; the destination mapping for it is written back to the job. created_by = str(uuid.uuid4()) art_relations_db.execute( """ INSERT INTO project_transfer_job (project_id, originating_vendor_id, originating_artist_id, destination_vendor_id, created_by_identity_id) VALUES (%s, %s, %s, %s, %s) """, ( project["project_id"], _ORIGINATING_VENDOR_ID, artist["artist_id"], _DESTINATION_VENDOR_ID, created_by, ), ) job = art_relations_db.fetchone( "SELECT * FROM project_transfer_job WHERE job_id = LAST_INSERT_ID()", ) assert job is not None # Seed one product_transfer_history row with the known-good release + artist art_relations_db.execute( """ INSERT INTO product_transfer_history (job_id, release_id, source_artist_id) VALUES (%s, %s, %s) """, (job["job_id"], project["release_id"], artist["artist_id"]), ) yield job # Teardown — child rows before parent (FK constraint) art_relations_db.execute( "DELETE FROM product_transfer_history WHERE job_id = %s", (job["job_id"],), ) art_relations_db.execute( "DELETE FROM project_transfer_job WHERE job_id = %s", (job["job_id"],), ) @pytest.fixture def transfer_job_with_duplicate_source_artist( art_relations_db: MySQLConnection, ) -> Generator[dict[str, Any], None, None]: """Seed a transfer job with three product_transfer_history rows that all share the same source_artist_id (across three distinct releases). This drives the dedup path: the lambda must collapse the three identical source_artist_ids into a single bulk-ensure entry, and every history row must end up mapped to the same destination_artist_id. """ project = art_relations_db.fetchone( """ SELECT p.project_id FROM project p WHERE p.vendor_id = %s AND (SELECT COUNT(*) FROM releases WHERE project_id = p.project_id) >= 3 LIMIT 1 """, (_ORIGINATING_VENDOR_ID,), ) assert project, f"No project with >=3 releases found for vendor_id {_ORIGINATING_VENDOR_ID}" releases = art_relations_db.fetchall( "SELECT release_id FROM releases WHERE project_id = %s LIMIT 3", (project["project_id"],), ) assert len(releases) == 3 artist = art_relations_db.fetchone( "SELECT artist_id FROM artist_info WHERE vendor_id = %s LIMIT 1", (_ORIGINATING_VENDOR_ID,), ) assert artist, f"No artist found for vendor_id {_ORIGINATING_VENDOR_ID} in artist_info" created_by = str(uuid.uuid4()) art_relations_db.execute( """ INSERT INTO project_transfer_job (project_id, originating_vendor_id, originating_artist_id, destination_vendor_id, created_by_identity_id) VALUES (%s, %s, %s, %s, %s) """, ( project["project_id"], _ORIGINATING_VENDOR_ID, artist["artist_id"], _DESTINATION_VENDOR_ID, created_by, ), ) job = art_relations_db.fetchone( "SELECT * FROM project_transfer_job WHERE job_id = LAST_INSERT_ID()", ) assert job is not None for release in releases: art_relations_db.execute( """ INSERT INTO product_transfer_history (job_id, release_id, source_artist_id) VALUES (%s, %s, %s) """, (job["job_id"], release["release_id"], artist["artist_id"]), ) yield job art_relations_db.execute( "DELETE FROM product_transfer_history WHERE job_id = %s", (job["job_id"],), ) art_relations_db.execute( "DELETE FROM project_transfer_job WHERE job_id = %s", (job["job_id"],), ) @pytest.fixture def transfer_job_with_multiple_distinct_artists( art_relations_db: MySQLConnection, ) -> Generator[dict[str, Any], None, None]: """Seed a transfer job with three product_transfer_history rows, each carrying a distinct source_artist_id owned by the originating vendor. All seed data is created per-run (fresh job_id via LAST_INSERT_ID, fresh UUID for created_by). Bulk-ensure is idempotent at the destination vendor, so the destination artist mappings persist across runs — the test asserts on IS NOT NULL rather than specific destination_artist_id values to stay stable. """ # Need a project with at least three releases so we can seed three history rows. project = art_relations_db.fetchone( """ SELECT p.project_id FROM project p WHERE p.vendor_id = %s AND (SELECT COUNT(*) FROM releases WHERE project_id = p.project_id) >= 3 LIMIT 1 """, (_ORIGINATING_VENDOR_ID,), ) assert project, f"No project with >=3 releases found for vendor_id {_ORIGINATING_VENDOR_ID}" releases = art_relations_db.fetchall( "SELECT release_id FROM releases WHERE project_id = %s LIMIT 3", (project["project_id"],), ) assert len(releases) == 3 artists = art_relations_db.fetchall( "SELECT artist_id FROM artist_info WHERE vendor_id = %s LIMIT 3", (_ORIGINATING_VENDOR_ID,), ) assert len(artists) == 3, f"Need >=3 artists for vendor_id {_ORIGINATING_VENDOR_ID} in artist_info" # Use the first of the seeded artists as originating_artist_id (required by the # lambda). It's already in the history-row set, so bulk-ensure receives the same # list either way. originating_artist_id = artists[0]["artist_id"] created_by = str(uuid.uuid4()) art_relations_db.execute( """ INSERT INTO project_transfer_job (project_id, originating_vendor_id, originating_artist_id, destination_vendor_id, created_by_identity_id) VALUES (%s, %s, %s, %s, %s) """, ( project["project_id"], _ORIGINATING_VENDOR_ID, originating_artist_id, _DESTINATION_VENDOR_ID, created_by, ), ) job = art_relations_db.fetchone( "SELECT * FROM project_transfer_job WHERE job_id = LAST_INSERT_ID()", ) assert job is not None for release, artist in zip(releases, artists, strict=True): art_relations_db.execute( """ INSERT INTO product_transfer_history (job_id, release_id, source_artist_id) VALUES (%s, %s, %s) """, (job["job_id"], release["release_id"], artist["artist_id"]), ) yield job # Teardown — child rows before parent (FK constraint) art_relations_db.execute( "DELETE FROM product_transfer_history WHERE job_id = %s", (job["job_id"],), ) art_relations_db.execute( "DELETE FROM project_transfer_job WHERE job_id = %s", (job["job_id"],), ) pytestmark = pytest.mark.lambda_name("ensure-artists") def test_destination_artist_ids_populated( art_relations_db: MySQLConnection, transfer_job_with_history: dict[str, Any], lambda_handler: LambdaHandler, ) -> None: # Omit destination_vendor_id so the lambda fetches it from ows-project-manager, # exercising the full code path lambda_handler.invoke( ENSURE_ARTISTS_FUNCTION_NAME, {"job_id": transfer_job_with_history["job_id"]}, ) rows = art_relations_db.fetchall( "SELECT destination_artist_id FROM product_transfer_history WHERE job_id = %s", (transfer_job_with_history["job_id"],), ) assert rows, "Expected at least one product_transfer_history row" for row in rows: assert row["destination_artist_id"] is not None, ( f"destination_artist_id is NULL for job {transfer_job_with_history['job_id']}" ) def test_destination_artist_ids_populated_for_multiple_distinct_source_artists( art_relations_db: MySQLConnection, transfer_job_with_multiple_distinct_artists: dict[str, Any], lambda_handler: LambdaHandler, ) -> None: """End-to-end Scenario: a job with multiple known source artists in QA. After invocation, every product_transfer_history row must have its destination_artist_id populated — proving the lambda ensured a destination artist for each distinct source artist via ows-artist's bulk-ensure endpoint. """ job_id = transfer_job_with_multiple_distinct_artists["job_id"] lambda_handler.invoke(ENSURE_ARTISTS_FUNCTION_NAME, {"job_id": job_id}) rows = art_relations_db.fetchall( """ SELECT source_artist_id, destination_artist_id FROM product_transfer_history WHERE job_id = %s """, (job_id,), ) assert len(rows) == 3, f"Expected 3 product_transfer_history rows for job {job_id}, found {len(rows)}" for row in rows: assert row["destination_artist_id"] is not None, ( f"destination_artist_id is NULL for source_artist_id={row['source_artist_id']} on job {job_id}" ) def test_duplicate_source_artists_map_to_same_destination_artist_id( art_relations_db: MySQLConnection, transfer_job_with_duplicate_source_artist: dict[str, Any], lambda_handler: LambdaHandler, ) -> None: """Scenario: Duplicate artist IDs across releases are deduplicated. At the integration layer we can't intercept the bulk-ensure HTTP call, but a consistent destination_artist_id across all three history rows is proof of dedup: the lambda must have asked ows-artist to ensure the same source artist exactly once (otherwise distinct ensures would yield distinct destinations, or the identical-source assumption would fail). """ job_id = transfer_job_with_duplicate_source_artist["job_id"] lambda_handler.invoke(ENSURE_ARTISTS_FUNCTION_NAME, {"job_id": job_id}) rows = art_relations_db.fetchall( """ SELECT destination_artist_id FROM product_transfer_history WHERE job_id = %s """, (job_id,), ) assert len(rows) == 3, f"Expected 3 product_transfer_history rows for job {job_id}, found {len(rows)}" destination_artist_ids = {row["destination_artist_id"] for row in rows} assert None not in destination_artist_ids, ( f"At least one destination_artist_id is NULL for job {job_id}: {destination_artist_ids}" ) assert len(destination_artist_ids) == 1, ( f"Expected one unique destination_artist_id across all history rows (proof of dedup), " f"got {destination_artist_ids} for job {job_id}" )