"""Test file for release_digital model.""" import copy import datetime from unittest.mock import MagicMock from oto import error as oto_error from oto import response import pymysql import pytest from product_digital.connectors import mysql from product_digital.constants import error from product_digital.models import release from product_digital.models.sql import product_digital as product_sql from product_digital.utils import error_handling from tests.factories import release as release_factory from tests.testutils import db @pytest.fixture def existing_release(): """Create a release to use when testing updates.""" return release_factory.ReleaseFactory.build() @pytest.fixture def new_props(): """Return a set of properties to modify when testing updates.""" return { 'artist_id': 29166, 'c_line': '2016 Newer, Better Label', 'description': 'New And Improved. ZOMG, So Rad.', 'release_format': 'A totally new format', 'genre_id': 13, 'itunes_previewable': 'yes', 'label': 'Newer, Better Label', 'manufacturer_upc': '123456789012', 'meta_language': 'SUX', 'product_code': 'NEWPRODCODEMUCHBETTER', 'release_name': 'This is a better name. Way better.', 'special_instructions': 'Listen With A Snifter Of Fine Brandy', 'version': 'A better version'} def test_get_upc_stored_procedure(): """Test that stored procedure was called.""" reserved_upc = '012345678901' mock_reserve_upc_response = MagicMock() mock_reserve_upc_response.scalar = MagicMock(return_value=reserved_upc) session = MagicMock() session.execute = MagicMock(return_value=mock_reserve_upc_response) upc = release._get_upc_stored_procedure(session) session.execute.assert_called_once_with(product_sql.RESERVE_UPC) assert upc == reserved_upc def test_get_upc_successful_connection(mocker): """Test when connection to db succeeds.""" reserved_upc = '012345678901' mocker.patch.object( release, '_get_upc_stored_procedure', return_value=reserved_upc) upc_response = release.get_upc() assert upc_response assert upc_response.message == reserved_upc def test_get_failed_connection(): """Test get_upc when connection to db fails.""" upc_response = release.get_upc() assert not upc_response assert upc_response.errors.get('code') == 'internal_error' def test_get_upc_retry(mocker, monkeypatch): """Test that pymysql deadlock errors are retried when getting upc.""" pymysql_deadlock_error = pymysql.err.InternalError( 1213, 'Deadlock found when trying to get lock; try restarting transaction') # speed up time so this test is fast class Datetime: elapsed_seconds = 0 start_time = datetime.datetime.now() @staticmethod def now(): Datetime.elapsed_seconds = Datetime.elapsed_seconds + 1 return ( Datetime.start_time + datetime.timedelta(seconds=Datetime.elapsed_seconds) ) monkeypatch.setattr('time.sleep', lambda x: None) monkeypatch.setattr('datetime.datetime', Datetime) mocker.patch.object( release, '_get_upc_stored_procedure', side_effect=pymysql_deadlock_error) with pytest.raises(pymysql.err.InternalError): release.get_upc() assert release._get_upc_stored_procedure.call_count == 5 def test_get_upc_serializable_isolation_level(mocker): """Test we make a call to get upc using a serializable isolation level.""" mock_db_session = MagicMock() mocker.patch.object( release.mysql, 'db_session', return_value=mock_db_session) release.get_upc() mock_session = mock_db_session.__enter__() mock_session.connection.assert_called_with( execution_options={'isolation_level': 'SERIALIZABLE'}) def test_to_dict_properties(base_release_data): """Test that the expected properties are returned.""" base_release_data['artist_id'] = 12345 new_release = release.Release(**base_release_data) new_release.artist_id = 178508 new_release.release_id = 1234 new_release.preorder_date = datetime.date(2016, 1, 11) new_release.release_date = datetime.date(2016, 2, 21) new_release.release_status = 'orchard_processing' new_release.sale_start_date = datetime.date(2016, 3, 31) new_release.vendor_release_identifier = 'some_kind_of_identifier' assert new_release.to_dict() == { 'release_id': new_release.release_id, 'artist_id': new_release.artist_id, 'c_line': new_release.c_line, 'delivered_version': new_release.delivered_version, 'description': new_release.description, 'distribution_format_id': new_release.distribution_format_id, 'format': new_release.release_format, 'genre_id': new_release.genre_id, 'ingestion_completed': new_release.ingestion_completed, 'itunes_previewable': new_release.itunes_previewable, 'label': new_release.label, 'manufacturer_upc': new_release.manufacturer_upc, 'meta_language': new_release.meta_language, 'not_for_distribution': new_release.not_for_distribution, 'p_line': new_release.p_line, 'preorder_date': new_release.preorder_date, 'product_code': new_release.product_code, 'product_type_id': new_release.product_type_id, 'project_id': new_release.project_id, 'release_date': new_release.release_date, 'release_name': new_release.release_name, 'release_status': new_release.release_status, 'sale_start_date': new_release.sale_start_date, 'special_instructions': new_release.special_instructions, 'subaccount_id': new_release.subaccount_id, 'upc': new_release.upc, 'version': new_release.version, 'vendor_catalog_number': new_release.vendor_catalog_number, 'vendor_release_identifier': new_release.vendor_release_identifier, 'display_upc': new_release.display_upc, 'deletions' : new_release.deletions} @db.test_schema def test_create_release_upc_included(base_release_data): """Test release creation when a upc is included in payload.""" expected_release_data = copy.deepcopy(base_release_data) expected_release_data['display_upc'] = expected_release_data['upc'] expected_release_data['upc'] = int(expected_release_data['upc']) release_response = release.create(base_release_data) message = release_response.message assert release_response.status == 201 assert 'release_id' in release_response.message assert_message = "response key '{}' doesn't matches the expected value {}" for key, expected_value in expected_release_data.items(): formatted_assert_message = assert_message.format(key, expected_value) assert message[key] == expected_value, formatted_assert_message @db.test_schema def test_create_release_persistence_upc_included(base_release_data): """Test release is created in database when upc is included.""" new_release = release.create(base_release_data) with mysql.db_session() as session: last_release = session.query(release.Release).order_by( release.Release.release_id.desc()).first() last_id = last_release.release_id assert new_release.message['release_id'] == last_id @db.test_schema def test_create_release_for_subaccount(base_release_data): """Test release is created for a given subaccount when applicable.""" subaccount_id = 3 new_release_data = base_release_data new_release_data['subaccount_id'] = subaccount_id release_response = release.create(new_release_data) message = release_response.message assert message['subaccount_id'] == subaccount_id @db.test_schema def test_create_release_without_upc_success( test_request_context, base_release_data, monkeypatch): """Test release creation when upc is not included in payload.""" mock_upc_response = response.Response(message='012345678901') monkeypatch.setattr( release, 'get_upc', MagicMock(return_value=mock_upc_response)) del base_release_data['upc'] release_response = release.create(base_release_data) message = release_response.message assert release_response.status == 201 assert 'release_id' in release_response.message assert message['project_id'] == base_release_data['project_id'] assert message['upc'] == int(mock_upc_response.message) @db.test_schema def test_create_release_without_upc_failure( test_request_context, base_release_data, monkeypatch): """Test release creation when get_upc fails.""" mock_reserve_upc_response = response.create_fatal_response('mysql error') monkeypatch.setattr( release, 'get_upc', MagicMock(return_value=mock_reserve_upc_response)) del base_release_data['upc'] release_response = release.create(base_release_data) assert release_response.status == 500 assert release_response.errors.get('message') == 'mysql error' @db.test_schema def test_update_success_response(existing_release, new_props): """Test status code and response payload upon successful update.""" db.seed_models(existing_release) result = release.update(existing_release.release_id, new_props) expected_payload = existing_release.to_dict() expected_payload['format'] = new_props.pop('release_format') expected_payload.update(new_props) assert result.status == 200 assert result.message == expected_payload @db.test_schema def test_update_success_persistence(new_props): """Test that the release is updated in the database.""" existing_release = release_factory.ReleaseFactory.build() db.seed_models(existing_release) release_id = existing_release.release_id release.update(release_id, new_props) with mysql.db_session() as session: reloaded = session.query(release.Release).get(release_id) session.expunge(reloaded) assert reloaded.artist_id == new_props['artist_id'] assert reloaded.c_line == new_props['c_line'] assert reloaded.description == new_props['description'] assert reloaded.genre_id == new_props['genre_id'] assert reloaded.itunes_previewable == new_props['itunes_previewable'] assert reloaded.label == new_props['label'] assert reloaded.manufacturer_upc == new_props['manufacturer_upc'] assert reloaded.meta_language == new_props['meta_language'] assert reloaded.product_code == new_props['product_code'] assert reloaded.release_format == new_props['release_format'] assert reloaded.release_name == new_props['release_name'] assert reloaded.special_instructions == new_props['special_instructions'] assert reloaded.version == new_props['version'] @db.test_schema def test_update_success_persistence_blank_date_field(new_props): """Test that the release is updated in the database.""" existing_release = release_factory.ReleaseFactory.build() existing_release.preorder_date = datetime.date(2016, 1, 11) db.seed_models(existing_release) release_id = existing_release.release_id new_props['preorder_date'] = None release.update(release_id, new_props) with mysql.db_session() as session: reloaded = session.query(release.Release).get(release_id) session.expunge(reloaded) assert reloaded.preorder_date == new_props['preorder_date'] @db.test_schema def test_update_ignores_immutable_or_nonexistent_props( existing_release, new_props): """Test that only allowed properties are modified.""" db.seed_models(existing_release) release_id = existing_release.release_id result = release.update(release_id, new_props) with mysql.db_session() as session: reloaded_release = session.query(release.Release).get(release_id) assert reloaded_release.upc == existing_release.upc assert result.message['upc'] == existing_release.upc assert 'color' not in result.message @db.test_schema def test_update_ignores_status_not_in_status_types( existing_release, new_props): """Test that status update not in the enum is not passed into update.""" db.seed_models(existing_release) release_id = existing_release.release_id existing_release_status = existing_release.release_status existing_release.release_status = 'something_else_not_in_list' result = release.update(release_id, new_props) with mysql.db_session() as session: reloaded_release = session.query(release.Release).get(release_id) assert reloaded_release.upc == existing_release.upc assert result.message['release_status'] == existing_release_status assert result.message['upc'] == existing_release.upc assert 'color' not in result.message @db.test_schema def test_update_does_not_check_status_if_project_id_has_not_changed( existing_release, new_props ): """Test updating a product while including project id. The product should not go through the merge product logic if the included project id is the same as the existing release. """ db.seed_models(existing_release) extended_new_props = dict(new_props) extended_new_props['project_id'] = existing_release.project_id result = release.update(existing_release.release_id, extended_new_props) expected_payload = existing_release.to_dict() expected_payload['format'] = extended_new_props.pop('release_format') expected_payload.update(extended_new_props) assert result.status == 200 assert result.message == expected_payload @db.test_schema def test_update_can_change_project_id( existing_release, new_props ): """Test updating a product's project id.""" db.seed_models(existing_release) extended_new_props = dict(new_props) extended_new_props['project_id'] = existing_release.project_id + 1 extended_new_props['subaccount_id'] = 2424 result = release.update(existing_release.release_id, extended_new_props) expected_payload = existing_release.to_dict() expected_payload['format'] = extended_new_props.pop('release_format') expected_payload.update(extended_new_props) assert result.status == 200 assert result.message == expected_payload @db.test_schema def test_update_can_change_release_status_to_transfer_to_content( existing_release ): """Test updating release status. label_processing -> transfer_to_content """ db.seed_models(existing_release) result = release.update( existing_release.release_id, {'release_status': 'transfer_to_content'} ) assert result.status assert result.message['release_status'] == 'transfer_to_content' @db.test_schema def test_update_can_not_change_release_status_to_transfer_to_content( existing_release ): """Test updating release status. in_content -/> transfer_to_content """ existing_release.release_status = 'in_content' db.seed_models(existing_release) result = release.update( existing_release.release_id, {'release_status': 'transfer_to_content'} ) assert result.status assert result.message['release_status'] == 'in_content' @db.test_schema def test_update_release_not_found(new_props): """Test that a 404 is returned if the product does not exist.""" result = release.update(12345, new_props) assert result.status == 404 def test_update_release_db_error(db_exception, mocker, new_props): """Test that a DB error is logged and a 500 is returned.""" mock_session = db.mock_db_session(mocker) mock_session.query.side_effect = db_exception mocker.patch.object(error_handling, 'log_db_exception', MagicMock()) result = release.update(12345, new_props) assert result.status == 500 error_handling.log_db_exception.assert_called_once_with(db_exception) @db.test_schema def test_get_release(existing_release): """Test that get_release returns a valid release.""" db.seed_models(existing_release) result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('release_id') == existing_release.release_id assert message.get('release_status') == existing_release.release_status @db.test_schema def test_get_release_not_found(): """Test that get_release returns 404 when nothing found.""" result = release.get_release(0) assert result.status == 404 assert result.errors.get('code') == oto_error.ERROR_CODE_NOT_FOUND @db.test_schema def test_create_release_vendor_release_identifier_included(base_release_data): """Test release creation when a vendor_release_identifier is included.""" vendor_release_identifier = 'some_identifier' base_release_data['vendor_release_identifier'] = vendor_release_identifier expected_release_data = copy.deepcopy(base_release_data) expected_release_data['display_upc'] = expected_release_data['upc'] expected_release_data['upc'] = int(expected_release_data['upc']) release_response = release.create(base_release_data) message = release_response.message assert release_response.status == 201 assert 'release_id' in release_response.message assert_message = "response key '{}' doesn't matches the expected value {}" for key, expected_value in expected_release_data.items(): formatted_assert_message = assert_message.format(key, expected_value) assert message[key] == expected_value, formatted_assert_message @db.test_schema def test_update_can_not_change_release_status_to_label_processing( existing_release ): """Test updating release status. transfer_to_content -/> label_processing """ existing_release.release_status = 'transfer_to_content' db.seed_models(existing_release) result = release.update( existing_release.release_id, {'release_status': 'label_processing'} ) assert result.status assert result.message['release_status'] == 'transfer_to_content' @db.test_schema def test_update_does_not_error_submitting_an_error_correction_product( existing_release ): """Test updating release status with derived statuses.""" existing_release.release_status = 'transfer_to_content' db.seed_models(existing_release) result = release.update( existing_release.release_id, {'release_status': 'error_correction'} ) assert result.status assert result.message['release_status'] == 'transfer_to_content' @db.test_schema def test_update_submitted_product(existing_release): """Test update method if product is submitted.""" submitted_status = 'transfer_to_content' update_props = { 'release_status': submitted_status, 'release_name': 'Move submitted product'} existing_release.release_status = submitted_status db.seed_models(existing_release) result = release.update(existing_release.release_id, update_props) expected_payload = existing_release.to_dict() expected_payload['release_status'] = submitted_status expected_payload.update(update_props) assert result.status == 200 assert result.message == expected_payload @db.test_schema def test_set_not_for_distribution(existing_release): """Test update method to set not_for_distribution.""" not_for_distribution = 'N' existing_release.not_for_distribution = not_for_distribution db.seed_models(existing_release) result = release.update_not_for_distribution( existing_release.release_id, 'NotforFurtherDistribution') message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'NotforFurtherDistribution' result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'NotforFurtherDistribution' @db.test_schema def test_set_not_for_distribution_with_release_status(existing_release): """Test update method to set not_for_distribution and release_status.""" not_for_distribution = 'N' release_status = 'in_content' existing_release.not_for_distribution = not_for_distribution existing_release.release_status = release_status db.seed_models(existing_release) result = release.update_not_for_distribution( existing_release.release_id, 'ReviewedWontDeliver', 'label_processing') message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'ReviewedWontDeliver' assert message.get('release_status') == 'label_processing' result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'ReviewedWontDeliver' assert message.get('release_status') == 'label_processing' @db.test_schema def test_set_not_for_distribution_invalid_value(existing_release): """Test update method to set not_for_distribution rejects bad value.""" not_for_distribution = 'N' existing_release.not_for_distribution = not_for_distribution db.seed_models(existing_release) result = release.update_not_for_distribution( existing_release.release_id, 'BadData') assert result.status == 403 assert result.errors == { 'code': error.VALIDATION_ERROR, 'message': error.ERROR_MESSAGE_INVALID_NOT_FOR_DISTRIBUTION } result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'N' @db.test_schema def test_set_not_for_distribution_invalid_product_status(existing_release): """Test update method to set not_for_distribution rejects bad product status.""" not_for_distribution = 'N' release_status = 'in_content' existing_release.not_for_distribution = not_for_distribution existing_release.release_status = release_status db.seed_models(existing_release) result = release.update_not_for_distribution( existing_release.release_id, 'ReviewedWontDeliver', 'bad_data') assert result.status == 403 assert result.errors == { 'code': error.VALIDATION_ERROR, 'message': error.ERROR_MESSAGE_INVALID_RELEASE_STATUS } result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'N' assert message.get('release_status') == 'in_content' @db.test_schema def test_update_release_status(existing_release): """Test update method to set release_status.""" existing_release.not_for_distribution = 'SMEAnalyticsDummy' db.seed_models(existing_release) result = release.update_release_status( existing_release.release_id, 'in_content') message = result.message assert result.status == 200 assert message.get('release_status') == 'in_content' result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('release_status') == 'in_content' @db.test_schema def test_update_release_status_invalid_value(existing_release): """Test update method to update release_status rejects bad value.""" existing_release.not_for_distribution = 'SMEAnalyticsDummy' db.seed_models(existing_release) result = release.update_release_status( existing_release.release_id, 'BadData') assert result.status == 403 assert result.errors == { 'code': error.VALIDATION_ERROR, 'message': error.ERROR_MESSAGE_INVALID_RELEASE_STATUS } result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('release_status') == existing_release.release_status @db.test_schema def test_update_allow_changing_project_id_if_product_is_not_in_progress( existing_release, new_props ): """Test updating a product. There should be no error when trying to change the project id of a delivered product. """ db.seed_models(existing_release) extended_new_props = dict(new_props) extended_new_props['project_id'] = existing_release.project_id + 1 extended_new_props['release_status'] = 'in_content' extended_new_props['subaccount_id'] = '123' result = release.update(existing_release.release_id, extended_new_props) assert result.status == 200 assert result.message.get('release_status') == 'in_content' @db.test_schema def test_not_for_distribution_missingassets(existing_release): """Test check not for distribution set to MissingAssets.""" db.seed_models(existing_release) result = release.update_not_for_distribution(existing_release.release_id, 'MissingAssets') message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'MissingAssets' result = release.get_release(existing_release.release_id) message = result.message assert result.status == 200 assert message.get('not_for_distribution') == 'MissingAssets' @db.test_schema def test_delete(product_id): """Test delete release.""" existing_release = release_factory.ReleaseFactory.build( release_id=product_id) db.seed_models(existing_release) with mysql.db_session() as session: release.delete(existing_release.release_id, session) result = session.query(release.Release).all() assert result == [] @db.test_schema def test_update_release_date_sale_start_date_disabled( existing_release, new_props ): """Test updating a product. release_date or sale_start_date should not be editable when product is approved. """ existing_release.release_status = 'in_content' db.seed_models(existing_release) extended_new_props = dict(new_props) extended_new_props['release_date'] = datetime.date(2022, 8, 22) extended_new_props['sale_start_date'] = datetime.date(2022, 8, 22) result = release.update(existing_release.release_id, extended_new_props) assert result.status == 403 @db.test_schema def test_oa_user_update_release_date_sale_start_date( existing_release, new_props ): """Test updating a product. release_date or sale_start_date should still be editable for OA users, even when product is approved. """ existing_release.release_status = 'in_content' db.seed_models(existing_release) extended_new_props = dict(new_props) extended_new_props['release_date'] = datetime.date(2022, 8, 22) extended_new_props['sale_start_date'] = datetime.date(2022, 8, 22) result = release.update(existing_release.release_id, extended_new_props, 'oa:101') assert result.status == 200