"""Unit tests for audio product validation.""" import json from unittest.mock import call from flexmock import flexmock from oto import response import pytest from product_digital.constants import error from product_digital.models import audio_product as audio_product_model, ows_pricing from product_digital.models import content_review_account_blocklist as content_review_account_blocklist_model from product_digital.models import ows_assets from product_digital.models import ows_blocklist_manager from product_digital.models import ows_product from product_digital.models import ows_product_workflow from product_digital.models import ows_track from product_digital.models import project as project_model from product_digital.models import release from product_digital.models import release_spatial from product_digital.validation import audio_product as product_validation from product_digital.logic.validate_audio_product_feature_flag_context import ValidateAudioProductFeatureFlagContext @pytest.fixture def valid_product_blocklist_response(): """Return a fake response for successful blocklist validation.""" return response.Response(message={}) @pytest.fixture def invalid_product_blocklist_payload(): """Return a fake response payload for product blocklist validation.""" return { 'validation_error': { 'matched_blacklist_words': ['word'], 'items': [{ 'word': 'word', 'reason': 'some reason', 'alert': None, 'contact': None }] } } @pytest.fixture def invalid_product_blocklist_response(invalid_product_blocklist_payload): """Return a fake response for product blocklist validation.""" return response.Response(message=invalid_product_blocklist_payload) @pytest.fixture def product_code_error(): """Return a product code error response.""" return response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=error.ERROR_MESSAGE_PRODUCT_CODE_NOT_AVAILABLE) @pytest.fixture def upc_error(): """Return a product code error response.""" return response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=error.ERROR_MESSAGE_UPC_NOT_AVAILABLE) @pytest.fixture def valid_tracks_payload(): """Return a fake response payload for successful tracks validation.""" return { 'errors': [], 'total_tracks': 2, 'valid_tracks': 2, 'valid': True} @pytest.fixture def valid_tracks_response(valid_tracks_payload): """Return a fake response for successful tracks validation.""" return response.Response(message=valid_tracks_payload) def test_validate_product_valid_dates(mocker): """Test that a payload with valid dates generates a successful response.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '2016-04-22', 'release_date': '2016-04-23', 'sale_start_date': '2016-04-24'} result = product_validation.validate_product_on_update(product_data) assert result.status == 200 def test_validate_product_on_update_invalid_dates( mocker, invalid_date_message): """Test that a payload with nonexistent dates generates errors.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '1999-13-32', 'release_date': '1999-00-00', 'sale_start_date': '2017-02-29'} result = product_validation.validate_product_on_update(product_data) assert result.status == 400 invalid_date_message.update( {'error_code': 'invalid_preorder_date'} ) assert result.errors['message']['preorder_date'] == invalid_date_message invalid_date_message.update( {'error_code': 'invalid_release_date'} ) assert result.errors['message']['release_date'] == invalid_date_message invalid_date_message.update( {'error_code': 'invalid_sale_start_date'} ) assert result.errors['message']['sale_start_date'] == invalid_date_message def test_validate_product_on_update_dates_too_early( mocker, early_late_date_message): """Test that dates before the expected range are rejected.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '1899-12-31', 'release_date': '1234-05-06', 'sale_start_date': '1111-11-11'} result = product_validation.validate_product_on_update(product_data) assert result.status == 400 early_late_date_message.update( {'error_code': 'invalid_preorder_date'} ) assert result.errors['message']['preorder_date'] == early_late_date_message early_late_date_message.update( {'error_code': 'invalid_release_date'} ) assert result.errors['message']['release_date'] == early_late_date_message early_late_date_message.update( {'error_code': 'invalid_sale_start_date'} ) assert result.errors['message']['sale_start_date'] == \ early_late_date_message def test_validate_product_on_update_dates_too_late( mocker, early_late_date_message): """Test that dates after the expected range are rejected.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '3000-12-31', 'release_date': '2101-01-01', 'sale_start_date': '2222-11-11'} result = product_validation.validate_product_on_update(product_data) assert result.status == 400 early_late_date_message.update( {'error_code': 'invalid_preorder_date'} ) assert result.errors['message']['preorder_date'] == early_late_date_message early_late_date_message.update( {'error_code': 'invalid_release_date'} ) assert result.errors['message']['release_date'] == early_late_date_message early_late_date_message.update( {'error_code': 'invalid_sale_start_date'} ) assert result.errors['message']['sale_start_date'] == early_late_date_message # noqa def test_validate_product_on_update_date_field_blank(mocker): """Test that date values left blank pass through validation.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '', 'release_date': '2016-04-23', 'sale_start_date': '2016-04-24'} result = product_validation.validate_product_on_update(product_data) assert result.status == 200 def test_validate_product_on_update_all_date_field_blank(mocker): """Test that date values left blank pass through validation.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = { 'preorder_date': '', 'release_date': '', 'sale_start_date': ''} result = product_validation.validate_product_on_update(product_data) assert result.status == 200 def test_validate_product_on_update_product_code_not_available( mocker, product_code_not_available, product_code_error): """Test that errors are returned when a product code is in use.""" mocker.patch.object( ows_product, 'is_product_code_available', return_value=product_code_error) product_data = {'product_code': 'abc123'} result = product_validation.validate_product_on_update(product_data) assert result.status == 400 assert result.errors['message'] == product_code_not_available def test_validate_product_on_update_product_code_fatal_error( mocker, product_code_not_available): """Test that errors are returned when a product code is in use.""" mocker.patch.object( ows_product, 'is_product_code_available', return_value=response.create_fatal_response()) product_data = {'product_code': 'abc123'} result = product_validation.validate_product_on_update(product_data) assert result.status == 500 def test_validate_product_on_update_when_product_code_unchanged( mocker, product_code_error): """Test a successful response when the product code is unchanged.""" product_code_string = 'abc123' product_data = { 'product_id': 1, 'product_name': 'new name', 'product_code': product_code_string} mocker.patch.object( ows_product, 'is_product_code_available', return_value=product_code_error) mocker.patch.object( release, 'get_release', return_value=response.Response( message={'product_code': product_code_string})) result = product_validation.validate_product_on_update(product_data) assert result.status == 200 def test_validate_product_on_create_success(mocker): """Test successful response when no validation issues.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) mocker.patch.object(ows_product, 'check_upc_available', return_value=response.Response()) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 200 def test_validate_product_on_create_when_product_code_fatal_error(mocker): """Test successful response when no validation issues.""" mocker.patch.object( ows_product, 'is_product_code_available', return_value=response.create_fatal_response()) mocker.patch.object(ows_product, 'check_upc_available', return_value=response.Response()) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 500 def test_validate_product_on_create_when_upc_fatal_error(mocker): """Test successful response when no validation issues.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) mocker.patch.object( ows_product, 'check_upc_available', return_value=response.create_fatal_response()) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 500 def test_validate_product_on_create_product_code_not_available( mocker, product_code_not_available, product_code_error): """Test successful response when no validation issues.""" mocker.patch.object(ows_product, 'is_product_code_available', return_value=product_code_error) mocker.patch.object(ows_product, 'check_upc_available', return_value=response.Response()) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 400 assert result.errors['message'] == product_code_not_available def test_validate_product_on_create_product_code_missing(mocker): """Test successful response when product_code is omitted.""" mocker.patch.object( ows_product, 'check_upc_available', return_value=response.Response()) product_data = {'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 200 def test_validate_product_on_create_upc_not_available( mocker, upc_not_available, upc_error): """Test successful response when no validation issues.""" mocker.patch.object(ows_product, 'check_upc_available', return_value=upc_error) mocker.patch.object(ows_product, 'is_product_code_available', return_value=response.Response()) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) assert result.status == 400 assert result.errors['message'] == upc_not_available def test_validate_product_on_create_upc_and_product_code_not_available( mocker, product_code_not_available, upc_not_available, product_code_error, upc_error): """Test successful response when no validation issues.""" mocker.patch.object(ows_product, 'check_upc_available', return_value=upc_error) mocker.patch.object(ows_product, 'is_product_code_available', return_value=product_code_error) product_data = {'product_code': 'abc123', 'upc': '123456789012'} result = product_validation.validate_product_on_create(product_data) expected_message = product_code_not_available.copy() expected_message.update(upc_not_available) assert result.status == 400 assert result.errors['message'] == expected_message def test_validate_product_basics(mock_language_model_response): """Test successful validation of product basics payload.""" valid_product_basics_payload = { 'product_name': 'new product', 'meta_language': 'eng', 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'artist_id': 123, 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( valid_product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 def test_validate_product_basics_missing_entry(mock_language_model_response): """Test an error validation of product basics payload.""" product_basics_payload = { 'meta_language': 'eng', 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'product_name': { 'validator_value': True, 'validator': 'required', 'message': "'product_name' is a required property", 'error_code': 'product_name_required', } } } } def test_validate_product_basics_none_entry(mock_language_model_response): """Test an error validation of product basics payload.""" valid_product_basics_payload = { 'product_name': None, 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'meta_language': 'eng', 'artist_id': 123, 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( valid_product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'product_name': { 'validator': 'required', 'message': "'product_name' is a required property", 'validator_value': True, 'error_code': 'product_name_required', } } } } def test_validate_product_basics_missing_entries(mock_language_model_response): """Test validation with multiple errors for product basics payload.""" valid_product_basics_payload = { 'meta_language': 'eng', 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( valid_product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'product_name': { 'validator': 'required', 'message': "'product_name' is a required property", 'validator_value': True, 'error_code': 'product_name_required', } } } } def test_validate_product_basics_invalid_product_code(mock_language_model_response): """Test validation with invalid product code.""" valid_product_basics_payload = { 'meta_language': 'eng', 'product_code': 'test 12', 'product_name': 'Test', 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( valid_product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'product_code': { 'validator': 'pattern', 'validator_value': '^[a-zA-Z0-9-]*$', 'message': "'test 12' does not match '^[a-zA-Z0-9-]*$'", 'error_code': 'product_code_pattern', } } } } def test_validate_product_basics_no_primary_artist(mock_language_model_response): """Test validation with missing primary artist.""" product_basics_payload = { 'product_artists': [{ 'role': 'featuring', 'name': 'Trump', 'id': 45}], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'primary_artist': { 'validator_value': True, 'message': "'primary_artist' is a required property", 'validator': 'required', 'error_code': 'primary_artist_required', } } } } def test_validate_product_basics_missing_entries_and_primary_artist(): """Test validation with missing basics and primary artist.""" product_basics_payload = { 'product_artists': [{ 'role': 'featuring', 'name': 'Trump', 'id': 45}], 'product_name': 'new product', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full'} expected_basics_validation = { 'valid': False, 'errors': { 'primary_artist': { 'message': "'primary_artist' is a required property", 'validator': 'required', 'validator_value': True, 'error_code': 'primary_artist_required', }, 'meta_language': { 'message': "'meta_language' is a required property", 'validator': 'required', 'validator_value': True, 'error_code': 'meta_language_required', }, 'c_line': { 'message': "'c_line' is a required property", 'validator': 'required', 'validator_value': True, 'error_code': 'c_line_required', }, 'imprint': { 'message': "'imprint' is a required property", 'validator': 'required', 'validator_value': True, 'error_code': 'imprint_required', } } } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message.get('product_basics') == expected_basics_validation def test_validate_product_basics_single_track_primary_artist_various_artists( mock_language_model_response): """Test validation with missing primary artist.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Various Artists', 'id': 27 }, { 'role': 'featuring', 'name': 'Trump', 'id': 45 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} result = product_validation.validate_product_basics( product_basics_payload, single_track_product=True, ff_context=ValidateAudioProductFeatureFlagContext( single_track_product_various_artists=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'primary_artist': { 'validator': 'stVariousArtists', 'validator_value': True, 'message': 'Various Artists is not allowed on single track products', 'error_code': 'st_various_artists_res' } } } } def test_validate_product_basics_featuring_artists_mismatch_error( mock_language_model_response): """Test validation with featuring artist mismatch error.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Goan Artists', 'id': 27 }, { 'role': 'featuring', 'name': 'Trump', 'id': 45 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'featuring', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'featuring', 'name': 'Jarvis Q. Hinkleberry VII' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( featuring_artist_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': False, 'errors': { 'featuring': { 'validator': 'featuringArtistMismatch', 'validator_value': True, 'message': ( 'A featuring artist on 100% of tracks must be listed as ' 'a featuring artist on the product level' ), 'error_code': 'featuring_artist_mismatch' } } } } def test_validate_product_basics_featuring_artists_mismatch_last_track_valid( mock_language_model_response): """Test validation with featuring artist mismatch error.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Goan Artists', 'id': 27 }, { 'role': 'featuring', 'name': 'Trump', 'id': 45 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'remixer', 'name': 'Bobby' }] }, { 'tuid': '124', 'artists': [{ 'type': 'remixer', 'name': 'Yo Bobby' }] }, { 'tuid': '125', 'artists': [{ 'type': 'featuring', 'name': 'Jarvis Q. Hinkleberry VII' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( featuring_artist_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': True, 'errors': {} } } def test_validate_product_basics_featuring_artists_mismatch_valid( mock_language_model_response): """Test validation with featuring artist mismatch valid.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Goan Artists', 'id': 27 }, { 'role': 'featuring', 'name': 'Jarvis Q. Hinkleberry VII', 'id': 45 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} tracks_data_payload = [ { 'tuid': '123', 'artists': [ { 'type': 'featuring', 'name': 'Person A' } ] }, { 'tuid': '124', 'artists': [ { 'type': 'featuring', 'name': 'Person C' } ] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( featuring_artist_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'valid': True, 'errors': {} } } def test_validate_artwork_when_valid(product_id): """When valid returns True.""" (flexmock(ows_assets).should_receive('validate_artwork_v2').with_args( product_id, False).and_return(response.Response()).once()) artwork_validation_response = product_validation.validate_artwork( product_id) assert artwork_validation_response.status == 200 assert artwork_validation_response.message == { 'artwork': { 'valid': True, 'errors': []}} def test_validate_artwork_when_invalid(product_id): """When invalid returns False.""" (flexmock(ows_assets).should_receive('validate_artwork_v2').with_args( product_id, False).and_return( response.create_not_found_response()).once()) artwork_validation_response = product_validation.validate_artwork( product_id) assert artwork_validation_response.status == 200 assert artwork_validation_response.message == { 'artwork': { 'valid': False, 'errors': ['Artwork has not been uploaded successfully.']}} def test_validate_artwork_v2_success(product_id): """Test for a successful response when ows_assets model returns success.""" v2_response = response.Response('okie dokie') (flexmock(ows_assets) .should_receive('validate_artwork_v2') .with_args(product_id, require_v1=False) .once() .and_return(v2_response)) result = product_validation.validate_artwork( product_id) assert result.status == 200 assert result.message == {'artwork': {'valid': True, 'errors': []}} def test_validate_artwork_v2_not_found(product_id): """Test for error message in response when model returns a 404.""" v2_response = response.create_not_found_response('nothing to see here') (flexmock(ows_assets) .should_receive('validate_artwork_v2') .with_args(product_id, require_v1=False) .once() .and_return(v2_response)) result = product_validation.validate_artwork( product_id) assert result.status == 200 assert result.message == { 'artwork': { 'valid': False, 'errors': ['Artwork has not been uploaded successfully.']}} def test_validate_artwork_v2_error(product_id): """Test that an error is returned when model returns a 500.""" v2_response = response.Response(status=500, errors={'foo': 'bar'}) (flexmock(ows_assets) .should_receive('validate_artwork_v2') .with_args(product_id, require_v1=False) .once() .and_return(v2_response)) result = product_validation.validate_artwork( product_id) assert result == v2_response def test_validate_artwork_v2_require_v1(product_id): """Test that require_v1 is passed to the ows-assets model.""" v2_response = response.Response({'foo': 'bar'}) (flexmock(ows_assets) .should_receive('validate_artwork_v2') .with_args(product_id, require_v1=True) .once() .and_return(v2_response)) result = product_validation.validate_artwork( product_id, require_v1_artwork=True) assert result.status == 200 assert result.message == {'artwork': {'valid': True, 'errors': []}} def test_validate_product_dates_when_valid(valid_product_payload): """Test that a 200 response is returned when dates are present.""" result = product_validation.validate_product_dates(valid_product_payload) assert result.status == 200 def test_validate_product_dates_when_no_dates_provided(): """Test that a 200 response is returned when dates are present.""" result = product_validation.validate_product_dates({}) assert result.status == 200 assert result.message == { 'scheduling_and_pricing': { 'valid': False, 'errors': { 'release_date': { 'message': "'release_date' is a required property", 'validator_value': True, 'validator': 'required', 'error_code': 'release_date_required', }, 'sale_start_date': { 'message': "'sale_start_date' is a required property", 'validator_value': True, 'validator': 'required', 'error_code': 'sale_start_date_required', } } } } def test_validate_product_dates_when_no_sales_date(): """Test that a 200 response is returned when no sales date present.""" result = product_validation.validate_product_dates({'release_date': 1}) assert result.status == 200 assert result.message == { 'scheduling_and_pricing': { 'valid': False, 'errors': { 'sale_start_date': { 'validator_value': True, 'validator': 'required', 'message': "'sale_start_date' is a required property", 'error_code': 'sale_start_date_required', } } } } def test_validate_product_dates_when_no_release_date(): """Test that a 200 response is returned when no release date present.""" result = product_validation.validate_product_dates({'sale_start_date': 1}) assert result.status == 200 assert result.message == { 'scheduling_and_pricing': { 'valid': False, 'errors': { 'release_date': { 'validator_value': True, 'validator': 'required', 'message': "'release_date' is a required property", 'error_code': 'release_date_required', } } } } def test_validate_product_basics_with_subgenre_composer_required_and_missing( mock_language_model_response): """Test validation with composer required for subgenres.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 1235, 'subgenre_id': 1059, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} expected_basics_validation = { 'valid': False, 'errors': { 'composer': { 'validator': 'required', 'validator_value': True, 'message': "'composer' is a required property", 'error_code': 'composer_required', } } } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message.get('product_basics') == expected_basics_validation def test_validate_product_basics_with_genre_composer_required_and_missing( mock_language_model_response): """Test validation with composer for classical genre.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 12, 'subgenre_id': 17, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} expected_basics_validation = { 'valid': False, 'errors': { 'composer': { 'validator': 'required', 'validator_value': True, 'message': "'composer' is a required property", 'error_code': 'composer_required', } } } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 assert result.message.get('product_basics') == expected_basics_validation def test_validate_product_basics_with_subgenre_composer_required_and_present( mock_language_model_response): """Test validation with composer required and present for subgenres.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'jarvis q. hinkleberry vii' }, { 'role': 'composer', 'name': 'jacob van beethoven' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 1235, 'subgenre_id': 1059, 'format': 'full', 'imprint': '2017 something', 'c_line': '2017 something'} result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 def test_validate_product_basics_with_genre_composer_required_and_present( mock_language_model_response): """Test validation with composer for classical genre.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'jarvis q. hinkleberry vii' }, { 'role': 'composer', 'name': 'jacob van beethoven' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 12, 'subgenre_id': 17, 'format': 'full', 'imprint': '2017 something', 'c_line': '2017 something'} result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 def test_validate_product_basics_with_genre_and_subgenre_composer(mock_language_model_response): """Test composer validation for required subgenres and classical genre.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'jarvis q. hinkleberry vii' }, { 'role': 'composer', 'name': 'jacob van beethoven' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 12, 'subgenre_id': 1059, 'format': 'full', 'imprint': '2017 something', 'c_line': '2017 something'} result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext() ) assert result.status == 200 def test_validate_product_basics_lyricist_required_and_missing( mock_language_model_response): """Test validation when lyricist is required for a genre/subgenre and it is missing.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 11, 'subgenre_id': 644, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something', 'release_date': '2026-01-01', } expected_basics_validation = { 'valid': False, 'errors': { 'lyricist': { 'validator': 'required', 'validator_value': True, 'message': "'lyricist' is a required property", 'error_code': 'lyricist_required', } } } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext(release_level_lyricist=True) ) assert result.status == 200 assert result.message.get('product_basics') == expected_basics_validation def test_validate_product_basics_lyricist_required_and_present( mock_language_model_response): """Test validation when lyricist is required for a genre/subgenre and it is present.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'jarvis q. hinkleberry vii' }, { 'role': 'lyricist', 'name': 'jacob van beethoven' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 11, 'subgenre_id': 644, 'format': 'full', 'imprint': '2017 something', 'c_line': '2017 something', 'release_date': '2026-01-01', } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext(release_level_lyricist=True) ) assert result.status == 200 def test_validate_product_basics_lyricist_not_required_release_date( mock_language_model_response): """Test validation when lyricist is not required because it is not a new release.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 11, 'subgenre_id': 644, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something', 'release_date': '1990-01-01', } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext(release_level_lyricist=True) ) assert result.status == 200 def test_validate_product_basics_lyricist_not_required_genre( mock_language_model_response): """Test validation when lyricist is not required for that genre and subgenre.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'jarvis q. hinkleberry vii' } ], 'meta_language': 'eng', 'artist_id': 123, 'product_name': 'new product', 'genre_id': 123, 'subgenre_id': 456, 'format': 'full', 'imprint': '2017 something', 'c_line': '2017 something', 'release_date': '2026-01-01', } result = product_validation.validate_product_basics( product_basics_payload, ff_context=ValidateAudioProductFeatureFlagContext(release_level_lyricist=True) ) assert result.status == 200 def test_validate_product_basics_remixer_mismatch_valid( mock_language_model_response): """Test validation with remixer_mismatch is valid.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Goan Artists', 'id': 27 }, { 'role': 'remixer', 'name': 'somebody', 'id': 45 }, { 'role': 'remixer', 'name': 'Jarvis Q. Hinkleberry VII', id: 47 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'remixer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'remixer', 'name': 'person' }] }, { 'tuid': '124', 'artists': [{ 'type': 'remixer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( remixer_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': {}, 'valid': True } } def test_validate_product_basics_remixer_mismatch_error( mock_language_model_response): """Test validation with remixer_mismatch errors.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Goan Artists', 'id': 27 }, { 'role': 'remixer', 'name': 'someone', 'id': 45 }, { 'role': 'remixer', 'name': 'person', 'id': 46 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something'} tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'remixer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'remixer', 'name': 'somebody' }, { 'type': 'remixer', 'name': 'person' }] }, { 'tuid': '124', 'artists': [{ 'type': 'remixer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'remixer', 'name': 'person' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( remixer_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': { 'remixer': { 'validator': 'productAndTrackRemixersMismatch', 'validator_value': True, 'message': ( 'A remixer on 100% of tracks must be listed as ' 'a remixer on the product level' ), 'error_code': 'remixer_mismatch' } }, 'valid': False } } def test_get_track_artist_names_by_type_excludes_blank_remixer_artists(): """Test that remixer artists with empty or whitespace-only names are excluded.""" artists = [ {'type': 'remixer', 'name': ''}, {'type': 'remixer', 'name': ' '}, {'type': 'remixer', 'name': ' '}, {'type': 'remixer', 'name': 'Real Remixer'}, ] result = product_validation._get_track_artist_names_by_type(artists, 'remixer') assert result == {'Real Remixer'} def test_validate_tracks_with_warnings(product_id, orchard_user_id): """Test validate_tracks with warnings.""" (flexmock(ows_track) .should_receive('validate_tracks') .with_args(product_id, orchard_user_id) .and_return(response.Response({ 'errors': [], 'warnings': [ 'some track warning' ], 'total_tracks': 1, 'valid_tracks': 1, })) .once()) result = product_validation.validate_tracks( product_id, orchard_user_id=orchard_user_id ) tracks_message = result.message.get('tracks') assert result.status == 200 assert tracks_message == { 'valid': True, 'errors': [], 'warnings': ['some track warning'], 'total_tracks': 1, 'valid_tracks': 1, } def test_validate_product_blocklist_valid(product_id, valid_product_blocklist_response): """Test blocklist validation for a valid product.""" ( flexmock(ows_blocklist_manager).should_receive('validate_product') .with_args(product_id) .and_return(valid_product_blocklist_response) .once() ) result = product_validation.validate_product_blocklist(product_id) message = result.message.get('product_blocklist') assert result.status == 200 assert message == {'valid': True, 'errors': []} def test_validate_product_blocklist_invalid( product_id, invalid_product_blocklist_response, invalid_product_blocklist_payload): """Test blocklist validation for a invalid product.""" ( flexmock(ows_blocklist_manager).should_receive('validate_product') .with_args(product_id) .and_return(invalid_product_blocklist_response) .once() ) result = product_validation.validate_product_blocklist(product_id) message = result.message.get('product_blocklist') assert result.status == 200 assert message == { 'valid': False, 'errors': invalid_product_blocklist_payload['validation_error']['items'] } @pytest.mark.parametrize(( 'test_description', 'get_product_response', 'expected_get_project_by_id_calls', 'get_project_by_id_response', 'expected_is_vendor_or_subaccount_on_blocklist_calls', 'is_vendor_or_subaccount_on_blocklist_response', 'expected_result', ), [ ( 'product fetch error', response.Response('product fetch error', status=500), [], None, [], None, response.Response('product fetch error', status=500), ), ( 'project fetch error', response.Response({'project_id': 456}), [call(456)], response.Response('project fetch error', status=500), [], None, response.Response('project fetch error', status=500), ), ( 'blocklist check error', response.Response({'project_id': 456}), [call(456)], response.Response({'vendor_id': 789, 'subaccount_id': 101112}), [call(vendor_id=789, subaccount_id=101112)], response.Response('blocklist check error', status=500), response.Response('blocklist check error', status=500), ), ( 'Account is on blocklist', response.Response({'project_id': 456}), [call(456)], response.Response({'vendor_id': 789, 'subaccount_id': 101112}), [call(vendor_id=789, subaccount_id=101112)], response.Response(True), response.Response({ 'account': { 'valid': False, 'errors': [ { 'code': 'ACCOUNT_BLOCKLIST', 'reason': ( 'This content is from a blocked account and cannot be approved. ' 'Please reach out to QC, Operations or Contract Admin for more information.' ) } ], } }), ), ( 'Account is not on blocklist', response.Response({'project_id': 456}), [call(456)], response.Response({'vendor_id': 789, 'subaccount_id': 101112}), [call(vendor_id=789, subaccount_id=101112)], response.Response(False), response.Response({'account': {'errors': [], 'valid': True}}), ), ]) def test_validate_account( mocker, test_description, get_product_response, expected_get_project_by_id_calls, get_project_by_id_response, expected_is_vendor_or_subaccount_on_blocklist_calls, is_vendor_or_subaccount_on_blocklist_response, expected_result, ): """Test validate_account.""" mocker.patch.object( audio_product_model, 'get_product', return_value=get_product_response, ) mocker.patch.object( project_model, 'get_project_by_id', return_value=get_project_by_id_response, ) mocker.patch.object( content_review_account_blocklist_model, 'is_vendor_or_subaccount_on_blocklist', return_value=is_vendor_or_subaccount_on_blocklist_response, ) result = product_validation.validate_account(123) assert audio_product_model.get_product.mock_calls == [call(123)] assert project_model.get_project_by_id.mock_calls == expected_get_project_by_id_calls assert ( content_review_account_blocklist_model.is_vendor_or_subaccount_on_blocklist.mock_calls ) == expected_is_vendor_or_subaccount_on_blocklist_calls assert result.status == expected_result.status assert result.message == expected_result.message @pytest.mark.parametrize(( 'test_description', 'expected_get_release_correction_calls', 'get_release_correction_response', 'expected_result', ), [ ( 'success', [call(releas_correction_id=456)], response.Response({'release_id': 123}), (True, {'release_id': 123}), ), ( 'error', [call(releas_correction_id=456)], response.Response(status=12321), (False, {}), ), ]) def test_validate_release_correction( mocker, test_description, expected_get_release_correction_calls, get_release_correction_response, expected_result, ): """Test validate_release_correction.""" mocker.patch.object( ows_product_workflow, 'get_release_correction', return_value=get_release_correction_response, ) product_id = 123 release_correction_id = 456 result = ( product_validation.validate_release_correction( product_id, release_correction_id, ) ) ows_product_workflow.get_release_correction.mock_calls == ( expected_get_release_correction_calls) assert result == expected_result @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'product_basics': { 'valid': True, 'errors': {}, } } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }], { 'product_basics': { 'errors': { 'primary_artist': { 'error_code': 'primary_artist_mismatch_error', 'message': ( 'A primary artist on 100% of tracks must be listed as a ' 'primary artist on the product level' ), 'validator': 'primaryArtistMismatch', 'validator_value': True } }, 'valid': False } } ), ]) def test_validate_primary_artists_mismatch_error( product_id, track_response, expected_results, mock_language_model_response ): """Test validate_product_artists for primary artist mismatch.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }], 'product_id': product_id, 'product_name': 'new product', 'meta_language': 'eng', 'c_line': '2017 Something', 'imprint': '2017 Something', 'format': 'full', 'genre_id': 1235, 'subgenre_id': 827, 'artist_id': 123 } result = product_validation.validate_product_basics( valid_product_basics_payload, track_response, ff_context= ValidateAudioProductFeatureFlagContext( primary_artist_mismatch_error=True ) ) validation_result = result.message assert result.status == 200 assert validation_result == expected_results @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }, ] }], { 'product_basics': { 'valid': True, 'errors': {}, } } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }], { 'product_basics': { 'errors': {}, 'valid': True } } ), ]) def test_validate_primary_artists_mismatch_error_with_theatre_scores_subgenre( product_id, track_response, expected_results, mock_language_model_response ): """Test validate_product_artists for primary artist mismatch.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'role': 'composer', 'name': 'jacob van beethoven' }], 'product_id': product_id, 'product_name': 'new product', 'meta_language': 'eng', 'c_line': '2017 Something', 'imprint': '2017 Something', 'format': 'full', 'genre_id': 27, 'subgenre_id': 545, 'artist_id': 123 } result = product_validation.validate_product_basics( valid_product_basics_payload, track_response, ff_context= ValidateAudioProductFeatureFlagContext( primary_artist_mismatch_error=True ) ) validation_result = result.message assert result.status == 200 assert validation_result == expected_results @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry X' }] }], { 'product_basics': { 'errors': { 'primary_artist': { 'error_code': 'primary_artist_mismatch_error', 'message': ( 'A primary artist on 100% of tracks must be listed as a ' 'primary artist on the product level' ), 'validator': 'primaryArtistMismatch', 'validator_value': True } }, 'valid': False } } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '125', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'product_basics': { 'valid': True, 'errors': {}, } } ) ]) def test_validate_primary_artists_mismatch_error_with_corrections( product_id, track_response, expected_results, mock_language_model_response ): """Test validate_product_artists for primary artist mismatch with corrections.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }], 'product_id': product_id, 'product_name': 'new product', 'meta_language': 'eng', 'c_line': '2017 Something', 'imprint': '2017 Something', 'format': 'full', 'genre_id': 1235, 'subgenre_id': 827, 'artist_id': 123, 'corrections_by_track_id': { '123': { 'track_artist': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry X' }] } } } result = product_validation.validate_product_basics( valid_product_basics_payload, track_response, ff_context= ValidateAudioProductFeatureFlagContext( primary_artist_mismatch_error=True ) ) validation_result = result.message assert result.status == 200 assert validation_result == expected_results @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'valid': True, 'errors': [], } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '125', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'valid': True, 'errors': [], } ) ]) def test_validate_awal_artists( product_id, track_response, expected_results ): """Test validate_product_artists for AWAL artist mismatch.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }], 'product_id': product_id, } result = product_validation.validate_product_artists( valid_product_basics_payload, track_response, 'Jarvis Q. Hinkleberry VII', ff_context=ValidateAudioProductFeatureFlagContext() ) validation_result = result.message.get('product_artists') assert result.status == 200 assert validation_result == expected_results @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'valid': True, 'errors': [], 'warnings': [ { 'code': 'AWAL_ARTIST_APPLICATION_MISMATCH', 'reason': '{"awal_application_artist": "Jarvis Q. Hinkleberry X"}' } ], } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VIII' }] }], { 'valid': True, 'warnings': [ { 'code': 'AWAL_ARTIST_APPLICATION_MISMATCH', 'reason': '{"awal_application_artist": "Jarvis Q. Hinkleberry X"}' } ], 'errors': [], } ), ]) def test_validate_awal_artists_fail( product_id, track_response, expected_results ): """Test validate_product_artists for AWAL artist mismatch.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }], 'product_id': product_id, } result = product_validation.validate_product_artists( valid_product_basics_payload, track_response, 'Jarvis Q. Hinkleberry X', ff_context=ValidateAudioProductFeatureFlagContext() ) validation_result = result.message.get('product_artists') assert result.status == 200 assert validation_result == expected_results def test_validate_product_basics_producer_mismatch_valid(mock_language_model_response): """Test validation with producer_mismatch is valid.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Primary Artist', 'id': 27 }, { 'role': 'producer', 'name': 'Producer 1', 'id': 45 }, { 'role': 'producer', 'name': 'Producer 2', id: 47 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something' } tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'producer', 'name': 'Producer 1' }, { 'type': 'producer', 'name': 'Producer 2' }] }, { 'tuid': '124', 'artists': [{ 'type': 'producer', 'name': 'Producer 1' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( producer_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': {}, 'valid': True } } def test_validate_product_basics_producer_mismatch_with_no_tracks_producers_valid(mock_language_model_response): """Test validation with producer_mismatch is valid.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Primary Artist', 'id': 27 }, { 'role': 'producer', 'name': 'Producer 1', 'id': 45 }, { 'role': 'producer', 'name': 'Producer 2', id: 47 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something' } tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'remixer', 'name': 'Remixer 1' }] }, { 'tuid': '124', 'artists': [{ 'type': 'remixer', 'name': 'Remixer 2' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( producer_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': {}, 'valid': True } } def test_validate_product_basics_producer_mismatch_error(mock_language_model_response): """Test validation with producer_mismatch errors.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Primary Artist', 'id': 27 }, { 'role': 'producer', 'name': 'Producer 2', 'id': 46 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something' } tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'producer', 'name': 'Producer 1' }, { 'type': 'producer', 'name': 'Producer 2' }, { 'type': 'producer', 'name': 'Producer 3' }] }, { 'tuid': '124', 'artists': [{ 'type': 'producer', 'name': 'Producer 1' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( producer_mismatch=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': { 'producer': { 'validator': 'productAndTrackProducersMismatch', 'validator_value': True, 'message': error.ERROR_MESSAGE_PRODUCER_MISMATCH, 'error_code': error.ERROR_CODE_PRODUCER_MISMATCH } }, 'valid': False } } def test_validate_product_name_length_error(mock_language_model_response): """Test validation with product name length error.""" long_product_name = ( "Some insanely large product name that does not fit the length " "requirements in any shape or form and should trigger a validation error " "because it is way too long and exceeds the maximum allowed length for " "product names which is usually around 100 characters or so, but this one " "is way beyond that limit and should definitely not be allowed to pass validation." ) product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Primary Artist', 'id': 27 } ], 'product_name': long_product_name, 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something' } tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'producer', 'name': 'Producer 1' }] }, ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( product_name_length_check=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': { 'product_name': { 'validator': 'productNameLengthError', 'validator_value': True, 'message': error.ERROR_MESSAGE_PRODUCT_NAME_LENGTH, 'error_code': error.ERROR_CODE_PRODUCT_NAME_LENGTH } }, 'valid': False } } def test_validate_product_basics_various_artists_on_featuring(mock_language_model_response): """Test validation error for various artists on featuring artist.""" product_basics_payload = { 'product_artists': [ { 'role': 'primary_artist', 'name': 'Primary Artist', 'id': 27 }, { 'role': 'featuring', 'name': 'Various Artists', 'id': 45 } ], 'product_name': 'new product', 'meta_language': 'eng', 'genre_id': 1235, 'subgenre_id': 827, 'format': 'full', 'imprint': '2017 Something', 'c_line': '2017 Something' } tracks_data_payload = [ { 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Primary Artist' }] } ] result = product_validation.validate_product_basics( product_basics_payload, tracks_data_payload, ff_context=ValidateAudioProductFeatureFlagContext( various_artist_on_featuring=True ) ) assert result.status == 200 assert result.message == { 'product_basics': { 'errors': { 'featuring': { 'validator': 'variousArtistsOnFeaturingArtist', 'validator_value': True, 'message': 'Please list all artists individually and do not use "Various Artists".', 'error_code': 'various_artists_on_featuring_artist' } }, 'valid': False } } @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'featuring', 'name': 'Bobby' }] }], { 'product_basics': { 'errors': { 'featuring': { 'error_code': 'featuring_artist_mismatch', 'message': ( 'A featuring artist on 100% of tracks must be listed as a ' 'featuring artist on the product level' ), 'validator': 'featuringArtistMismatch', 'validator_value': True } }, 'valid': False } } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'featuring', 'name': 'Bobby' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '125', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'product_basics': { 'valid': True, 'errors': {}, } } ) ]) def test_validate_featuring_artists_mismatch_error_with_corrections_of_no_artist( product_id, track_response, expected_results, mock_language_model_response ): """Test validate_product_artists for primary artist mismatch with corrections.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'role': 'featuring', 'name': 'Tommy' }], 'product_id': product_id, 'product_name': 'new product', 'meta_language': 'eng', 'c_line': '2017 Something', 'imprint': '2017 Something', 'format': 'full', 'genre_id': 1235, 'subgenre_id': 827, 'artist_id': 123, 'corrections_by_track_id': { '123': { 'featuring': [{ 'type': 'featuring', 'name': 'Jarvis Q. Hinkleberry X' }] } } } result = product_validation.validate_product_basics( valid_product_basics_payload, track_response, ff_context= ValidateAudioProductFeatureFlagContext( featuring_artist_mismatch=True ) ) validation_result = result.message assert result.status == 200 assert validation_result == expected_results @pytest.mark.parametrize(( 'track_response', 'expected_results' ), [ ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'featuring', 'name': 'Bobby' }] }], { 'product_basics': { 'errors': {}, 'valid': True } } ), ( [{ 'tuid': '123', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }, { 'type': 'featuring', 'name': 'Bobby' }] }, { 'tuid': '124', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }, { 'tuid': '125', 'artists': [{ 'type': 'performer', 'name': 'Jarvis Q. Hinkleberry VII' }] }], { 'product_basics': { 'valid': True, 'errors': {}, } } ) ]) def test_validate_featuring_artists_match_with_corrections_of_no_artist( product_id, track_response, expected_results, mock_language_model_response ): """Test validate_product_artists for primary artist mismatch with corrections.""" valid_product_basics_payload = { 'product_artists': [{ 'role': 'primary_artist', 'name': 'Jarvis Q. Hinkleberry VII' }], 'product_id': product_id, 'product_name': 'new product', 'meta_language': 'eng', 'c_line': '2017 Something', 'imprint': '2017 Something', 'format': 'full', 'genre_id': 1235, 'subgenre_id': 827, 'artist_id': 123, 'corrections_by_track_id': { '123': { 'featuring': [] } } } result = product_validation.validate_product_basics( valid_product_basics_payload, track_response, ff_context= ValidateAudioProductFeatureFlagContext( featuring_artist_mismatch=True ) ) validation_result = result.message assert result.status == 200 assert validation_result == expected_results def test_validate_product_artists_spotify_watchlist_flag_off(mocker, product_id): """Test validate_product_artists when the spotify_watchlist_artist flag is off.""" product_artists = [{'role': 'primary_artist', 'name': 'Test Artist'}] product_data = {'product_artists': product_artists, 'product_id': product_id} tracks_data = [{'tuid': '123', 'artists': [{'type': 'performer', 'name': 'Test Artist'}]}] mock_validate = mocker.patch.object(ows_blocklist_manager, 'validate_artists') result = product_validation.validate_product_artists( product_data, tracks_data, None, ff_context=ValidateAudioProductFeatureFlagContext(spotify_watchlist_artist=False), ) validation_result = result.message.get('product_artists') assert result.status == 200 assert validation_result['valid'] is True assert validation_result.get('warnings', []) == [] mock_validate.assert_not_called() @pytest.mark.parametrize(( 'test_description', 'product_artists', 'validate_artists_response', 'expected_warnings', ), [ ( 'flag on, no matches — no warnings', [{'role': 'primary_artist', 'name': 'Test Artist'}], response.Response({}), [], ), ( 'flag on, matches returned — SPOTIFY_WATCHLIST_ARTIST warning added', [{'role': 'primary_artist', 'name': 'Flagged Artist'}], response.Response( { 'validation_errors': { 'product_artists': { 'matches': [{'name': 'Flagged Artist', 'role': 'primary_artist'}] } } }), [{ 'code': 'SPOTIFY_WATCHLIST_ARTIST', 'reason': '[{"name": "Flagged Artist", "role": "primary_artist"}]' }], ), ]) def test_validate_product_artists_spotify_watchlist_flag_on( mocker, product_id, test_description, product_artists, validate_artists_response, expected_warnings ): """When the spotify_watchlist flag is on, validate_artists is called and warnings reflect matches.""" product_data = {'product_artists': product_artists, 'product_id': product_id} tracks_data = [{'tuid': '123', 'artists': [{'type': 'performer', 'name': product_artists[0]['name']}]}] mock_validate = mocker.patch.object( ows_blocklist_manager, 'validate_artists', return_value=validate_artists_response ) result = product_validation.validate_product_artists( product_data, tracks_data, None, ff_context=ValidateAudioProductFeatureFlagContext(spotify_watchlist_artist=True), ) validation_result = result.message.get('product_artists') assert result.status == 200 assert validation_result['valid'] is True assert validation_result.get('warnings', []) == expected_warnings assert mock_validate.mock_calls == [ call({'product_id': product_id, 'product_artists': product_artists}) ] def test_validate_product_artists_spotify_watchlist_service_error(mocker, product_id): """When validate_artists returns an error response, no warnings are added and validation still passes.""" product_artists = [{'role': 'primary_artist', 'name': 'Test Artist'}] product_data = {'product_artists': product_artists, 'product_id': product_id} tracks_data = [{'tuid': '123', 'artists': [{'type': 'performer', 'name': 'Test Artist'}]}] mock_validate_artist = mocker.patch.object( ows_blocklist_manager, 'validate_artists', return_value=response.create_error_response(code='SERVICE_ERROR', message='upstream failure') ) result = product_validation.validate_product_artists( product_data, tracks_data, None, ff_context=ValidateAudioProductFeatureFlagContext(spotify_watchlist_artist=True), ) validation_result = result.message.get('product_artists') mock_validate_artist.assert_called_once() assert result.status == 200 assert validation_result['valid'] is True assert validation_result.get('warnings', []) == [] @pytest.mark.parametrize('ff_mock_value, expected_matches, expected_warnings, expect_inner_called', [ (False, [], [], 0), (True, [], [], 1), (True, [{'foo': 'baz'}], [ { 'code': 'ARTWORK_METADATA_MISMATCH', 'reason': json.dumps({'matches': [{'foo': 'baz'}]}) } ], 1), ]) def test_validate_artwork_compliance_metadata_mismatch( mocker, ff_mock_value, expected_matches, expected_warnings, expect_inner_called ): """validate_artwork_compliance only warns when ff is on and matches are found.""" product_data = {'product_name': 'My Album', 'product_artists': []} tracks_data = [{'tuid': 'abc', 'track_name': 'Track 1'}] mock_validate = mocker.patch.object( product_validation, '_validate_artwork_metadata_mismatch', return_value=expected_matches, ) result = product_validation.validate_artwork_compliance( product_data, tracks_data, ff_context=ValidateAudioProductFeatureFlagContext(artwork_metadata_mismatch=ff_mock_value), ) validation_result = result.message.get('artwork_compliance') assert result.status == 200 assert validation_result['valid'] is True assert validation_result.get('warnings', []) == expected_warnings assert mock_validate.call_count == expect_inner_called @pytest.mark.parametrize('search_sequence, expected_matches', [ ( 'Blee Album |001E\\n"Sung Title" |\\nJoan Smith |\\nJane Dae |\\nDJ REMOX', [ { 'metadata_value': 'Blue Album', 'metadata_field': 'product_name', 'fuzzy_matches_found': ['Blee Album'], }, { 'metadata_value': 'Song Title', 'metadata_field': 'track_name', 'fuzzy_matches_found': ['Sung Title'], }, { 'metadata_value': 'John Smith', 'metadata_field': 'primary_artist', 'fuzzy_matches_found': ['Joan Smith'], }, { 'metadata_value': 'Jane Doe', 'metadata_field': 'featuring', 'fuzzy_matches_found': ['Jane Dae'], }, { 'metadata_value': 'DJ Remix', 'metadata_field': 'remixer', 'fuzzy_matches_found': ['DJ REMOX'], } ], ), ( '', [], ), ( 'z' * 100, [], ), ]) def test_validate_artwork_metadata_mismatch(mocker, search_sequence, expected_matches): """Test _validate_artwork_metadata_mismatch.""" product_data = { 'product_id': 42, 'product_name': 'Blue Album', 'product_artists': [ {'name': 'John Smith', 'role': 'primary_artist'}, {'name': 'Jane Doe', 'role': 'featuring'}, {'name': 'DJ Remix', 'role': 'remixer'}, ], } tracks_data = [{'tuid': '1', 'track_name': 'Song Title'}] mocker.patch.object( ows_assets, 'get_image_text_extract', return_value=response.Response({'result': {'block_text': search_sequence}}), ) result = product_validation._validate_artwork_metadata_mismatch(product_data, tracks_data) assert result == expected_matches @pytest.mark.parametrize('product_data, block_text', [ pytest.param( {'product_id': 42, 'product_name': 'Blue', 'product_artists': [{'name': 'Jo', 'role': 'primary_artist'}]}, 'blue joh', id='term_too_short_skipped', ), pytest.param( {'product_id': 42, 'product_name': 'Blue Room', 'product_artists': [{'name': 'Blue Day', 'role': 'primary_artist'}]}, 'blue room blue day', id='query_completed_skipped', ), pytest.param( {'product_id': 42, 'product_name': 'Blue Album', 'product_artists': []}, 'blue album', id='exact_match_not_flagged', ), pytest.param( {'product_id': 42, 'product_name': 'Abc', 'product_artists': []}, 'ab something', id='short_fuzzy_match_not_flagged', ), pytest.param( {'product_id': 42, 'product_name': 'Blue Album', 'product_artists': [{'name': 'Bluer Album', 'role': 'primary_artist'}]}, 'bluer album', id='dictionary_match_not_flagged', ), ]) def test_validate_artwork_metadata_mismatch_filter_branches( mocker, product_data, block_text ): """Each filter branch inside _validate_artwork_metadata_mismatch produces no mismatch.""" mocker.patch.object( ows_assets, 'get_image_text_extract', return_value=response.Response({'result': {'block_text': block_text}}), ) result = product_validation._validate_artwork_metadata_mismatch(product_data, []) assert result == [] def test_build_metadata_search_terms_no_correction( search_terms_product_data, search_terms_tracks_data, expected_search_terms_base ): """Without track corrections, track_name is used directly.""" result = product_validation._build_metadata_search_terms( search_terms_product_data, search_terms_tracks_data ) assert result == expected_search_terms_base def test_build_metadata_search_terms_with_correction( search_terms_product_data, search_terms_tracks_data, expected_search_terms_base ): """With a track_name correction, the corrected value replaces the original.""" product_data = { **search_terms_product_data, 'corrections_by_track_id': {'1': {'track_name': 'Corrected Title'}}, } expected = [ {**entry, 'metadata_value': 'Corrected Title', 'dictionary': ['corrected title', 'corrected', 'title']} if entry['metadata_field'] == 'track_name' else entry for entry in expected_search_terms_base ] result = product_validation._build_metadata_search_terms( product_data, search_terms_tracks_data ) assert result == expected def test_validate_pricing(mocker, product_id): """Test validate_pricing.""" mock_pricing = mocker.patch.object( ows_pricing, 'get_product_pricing_validations', return_value=response.Response( {'valid': False, 'warnings': [{'warning_code': 'code', 'warning_message': 'message'}]}) ) result = product_validation.get_validate_pricing_warnings(product_id) assert result == [{'code': 'CODE', 'reason': 'message'}] assert mock_pricing.call_count == 1 @pytest.mark.parametrize(( 'test_description', 'get_assets_response', 'get_release_spatial_response', 'expected_valid', 'expected_errors', ), [ ( 'no assets — valid, no spatial UPC needed', response.Response(message={'assets': []}), None, True, [], ), ( 'stereo asset only — valid, no spatial UPC needed', response.Response(message={'assets': [{'asset_upload_type': 'stereo'}]}), None, True, [], ), ( 'atmos asset, spatial record exists — valid', response.Response(message={'assets': [{'asset_upload_type': 'atmos'}]}), response.Response(message={'release_id': 1, 'upc': 9876543210123}), True, [], ), ( 'atmos asset, no spatial record — invalid', response.Response(message={'assets': [{'asset_upload_type': 'atmos'}]}), response.create_not_found_response(), False, [{ 'code': 'SPATIAL_UPC_REQUIRED', 'reason': 'A spatial UPC record is required for products with spatial audio assets.', }], ), ( 'mixed assets with atmos, no spatial record — invalid', response.Response(message={'assets': [ {'asset_upload_type': 'stereo'}, {'asset_upload_type': 'atmos'}, ]}), response.create_not_found_response(), False, [{ 'code': 'SPATIAL_UPC_REQUIRED', 'reason': 'A spatial UPC record is required for products with spatial audio assets.', }], ), ]) def test_validate_spatial_upc( mocker, product_id, orchard_user_id, account_type, account_id, test_description, get_assets_response, get_release_spatial_response, expected_valid, expected_errors, ): """Test validate_spatial_upc.""" mocker.patch.object(ows_assets, 'get_assets', return_value=get_assets_response) if get_release_spatial_response is not None: mocker.patch.object( release_spatial, 'get_release_spatial', return_value=get_release_spatial_response, ) result = product_validation.validate_spatial_upc( product_id, orchard_user_id, account_type, account_id) assert result.status == 200 spatial_upc_result = result.message.get('spatial_upc') assert spatial_upc_result['valid'] is expected_valid assert spatial_upc_result['errors'] == expected_errors def test_validate_spatial_upc_get_assets_error(mocker, product_id, orchard_user_id, account_type, account_id): """When get_assets returns an error, it is propagated.""" mocker.patch.object( ows_assets, 'get_assets', return_value=response.create_fatal_response('assets service error'), ) result = product_validation.validate_spatial_upc( product_id, orchard_user_id, account_type, account_id) assert result.status == 500 def test_validate_spatial_upc_get_release_spatial_error(mocker, product_id, orchard_user_id, account_type, account_id): """When get_release_spatial returns a non-404 error, it is propagated.""" mocker.patch.object( ows_assets, 'get_assets', return_value=response.Response(message={'assets': [{'asset_upload_type': 'atmos'}]}), ) mocker.patch.object( release_spatial, 'get_release_spatial', return_value=response.create_fatal_response('db error'), ) result = product_validation.validate_spatial_upc( product_id, orchard_user_id, account_type, account_id) assert result.status == 500