"""Tests for bulk split ingestion handlers.""" from unittest.mock import patch from collaborator.constants.features import BULK_SPLIT_INGESTION from collaborator.constants.split import RateType from collaborator.constants.split_type import SplitTypeId from collaborator.schemas.split import ( BulkIngestTemplateRowSchema, IngestSummarySchema, ReplaceSplitsRequestSchema, ) from tests.testutils import mock_auth MODULE_HANDLERS = "collaborator.handlers.split" # --------------------------------------------------------------------------- # GET /splits/bulk-ingest-template # --------------------------------------------------------------------------- @patch(f"{MODULE_HANDLERS}.get_bulk_ingest_template") def test_get_bulk_ingest_template_returns_rows( mock_template, auth_client, mock_account, mocker, mock_features, ): """Returns 200 with template rows when FF is enabled.""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: True}) mock_template.return_value = [] result = auth_client.get( f"/splits/bulk-ingest-template?vendor_id={mock_account.id}" ) mock_template.assert_called_once_with(mock_account.id) assert result.status_code == 200 @patch(f"{MODULE_HANDLERS}.get_bulk_ingest_template") def test_get_bulk_ingest_template_serializes_rows( mock_template, auth_client, mock_account, mocker, mock_features, ): """Response body contains the serialized template rows.""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: True}) mock_template.return_value = [ BulkIngestTemplateRowSchema( vendor_id=mock_account.id, product_id=10, product_upc="UPC001", product_title="Album", tuid="tuid-a", track_name="My Song", track_isrc="USABC1234567", ) ] result = auth_client.get( f"/splits/bulk-ingest-template?vendor_id={mock_account.id}" ) assert result.status_code == 200 data = result.get_json() assert isinstance(data, list) assert len(data) == 1 assert data[0]["tuid"] == "tuid-a" assert data[0]["track_name"] == "My Song" assert data[0]["track_isrc"] == "USABC1234567" @patch(f"{MODULE_HANDLERS}.get_bulk_ingest_template") def test_get_bulk_ingest_template_forbidden_when_ff_disabled( mock_template, auth_client, mock_account, mocker, mock_features, ): """Returns 403 when the FF is disabled.""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: False}) result = auth_client.get( f"/splits/bulk-ingest-template?vendor_id={mock_account.id}" ) mock_template.assert_not_called() assert result.status_code == 403 # --------------------------------------------------------------------------- # POST /splits/bulk-ingest # --------------------------------------------------------------------------- @patch(f"{MODULE_HANDLERS}.run_bulk_ingest") def test_bulk_ingest_splits_forbidden_when_ff_disabled( mock_ingest, auth_client, mock_account, mocker, mock_features, ): """Returns 403 when the FF is disabled.""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: False}) result = auth_client.post( "/splits/bulk-ingest", json={"splits_config": [], "dry_run": True}, ) mock_ingest.assert_not_called() assert result.status_code == 403 @patch(f"{MODULE_HANDLERS}.run_bulk_ingest") def test_bulk_ingest_splits_dry_run_returns_summary( mock_ingest, auth_client, mock_account, mocker, mock_features, ): """Returns 200 with the summary when FF is enabled and logic succeeds.""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: True}) mock_ingest.return_value = IngestSummarySchema( vendor_id=mock_account.id, dry_run=True, input_rows=1, expanded_rows=1, existing_collaborators_matched=1, new_collaborators=0, tracks_with_existing_splits_updated=0, tracks_with_new_splits=1, tracks_with_unaffected_splits=0, existing_splits_updated=0, new_splits_created=1, products_skipped_no_tracks=0, vendor_currency="USD", splits_ingested=1, tracks_impacted=1, products_impacted=1, collaborators_receiving_new_splits=1, overwritten_splits=[], ) payload = { "splits_config": [ { "vendor_id": mock_account.id, "collaborator_id": 1, "collaborator_name": "Artist", "split_rate": 0.2, "rate_type": "NET", "tuid": "tuid-a", } ], "dry_run": True, } result = auth_client.post("/splits/bulk-ingest", json=payload) assert result.status_code == 200 mock_ingest.assert_called_once() @patch(f"{MODULE_HANDLERS}.run_bulk_ingest") def test_bulk_ingest_splits_returns_400_on_value_error( mock_ingest, auth_client, mock_account, mocker, mock_features, ): """Returns 400 when logic raises ValueError (e.g. validation failure).""" mock_auth(mocker, mock_account.id) mock_features({BULK_SPLIT_INGESTION: True}) mock_ingest.side_effect = ValueError("TUIDs not found in Snowflake: ['bad-tuid']") result = auth_client.post( "/splits/bulk-ingest", json={"splits_config": [], "dry_run": True}, ) assert result.status_code == 400 # --------------------------------------------------------------------------- # PUT /splits # --------------------------------------------------------------------------- @patch(f"{MODULE_HANDLERS}.split.replace_splits") def test_replace_splits_returns_updated_splits(mock_replace, auth_client, mocker): """Returns 200 with the splits returned by the logic layer.""" mock_auth(mocker, 24601) updated = [ { "id": 1, "identifier": "sub-1", "split_rate": 0.5, "split_type_id": SplitTypeId.SUBACCOUNT, "collaborator_id": 1, "rate_type": RateType.NET, "created_date": "2024-01-01T11:22:33", "updated_date": None, "source": None, } ] mock_replace.return_value = updated payload = { "vendor_id": 24601, "dp_splits_agreed": True, "replacements": [ { "identifier": "sub-1", "split_type_id": SplitTypeId.SUBACCOUNT, "splits": [ { "collaborator_id": 1, "split_rate": 0.5, "rate_type": RateType.NET, } ], } ], } result = auth_client.put("/splits", json=payload) assert result.status_code == 200 assert result.get_json() == updated mock_replace.assert_called_once() @patch(f"{MODULE_HANDLERS}.split.replace_splits") def test_replace_splits_parses_body_into_request_schema( mock_replace, auth_client, mocker ): """The validated body is passed to the logic as a ReplaceSplitsRequestSchema.""" mock_auth(mocker, 24601) mock_replace.return_value = [] payload = { "vendor_id": 24601, "dp_splits_agreed": True, "replacements": [ { "identifier": "tuid-1", "split_type_id": SplitTypeId.TRACK, "splits": [ { "collaborator_id": 7, "split_rate": 0.25, "rate_type": RateType.NET, } ], }, { "identifier": "sub-1", "split_type_id": SplitTypeId.SUBACCOUNT, "splits": [ { "collaborator_id": 8, "split_rate": 0.75, "rate_type": RateType.NET, } ], }, ], } result = auth_client.put("/splits", json=payload) assert result.status_code == 200 body = mock_replace.call_args.args[1] assert isinstance(body, ReplaceSplitsRequestSchema) assert body.vendor_id == 24601 assert body.dp_splits_agreed is True assert [r.identifier for r in body.replacements] == ["tuid-1", "sub-1"] assert [r.split_type_id for r in body.replacements] == [ SplitTypeId.TRACK, SplitTypeId.SUBACCOUNT, ] @patch(f"{MODULE_HANDLERS}.split.replace_splits") def test_replace_splits_missing_replacements_returns_400( mock_replace, auth_client, mocker ): """A body without the required replacements field fails validation.""" mock_auth(mocker, 24601) result = auth_client.put( "/splits", json={"vendor_id": 24601, "dp_splits_agreed": True} ) assert result.status_code == 400 mock_replace.assert_not_called()