"""Tests for bulk remove ownership endpoint.""" from collections import OrderedDict import decimal import json from operator import itemgetter from unittest.mock import ANY from unittest.mock import Mock from unittest.mock import patch from moto import mock_aws import boto3 import pytest from masters_registry.constant import error from masters_registry.models import bulk_tasks as task_model from masters_registry.models.sql import track as track_sql from masters_registry.tasks import bulk from masters_registry.connectors.dynamodb import client as outside_dynamodb_client dynamodb_resource = boto3.resource("dynamodb") @pytest.fixture def test_payload(): """Valid payload for this endpoint.""" payload = { 'account_id': 1, 'account_type': 'vendor', 'isrcs': ['QA123'], 'territories': ['AF', 'AD'] } return payload @pytest.fixture def test_headers(): """Headers for testing this enpoint.""" headers = { 'Orchard-User-Id': 'oa:123', 'Correlation-Id': 'test correlation id', 'Content-Type': 'application/json' } return headers def masters_registry_response_with_internal_conflicts(**kwargs): """Response from dynamodb with multiple items.""" territories = { 'AF': [ {'tuid': decimal.Decimal('123')}, {'tuid': decimal.Decimal('789')} ], 'QA': [ {'tuid': decimal.Decimal('123')}, {'tuid': decimal.Decimal('789')} ], 'AD': [ {'tuid': decimal.Decimal('456')} ] } payload = { 'ResponseMetadata': { 'HTTPHeaders': {}, 'HTTPStatusCode': 200, 'RequestId': '', 'RetryAttempts': 0}, 'Responses': { 'test-masters_active': [ {'isrc': 'QA123', 'locked_territories': {}, 'territories': OrderedDict( sorted(territories.items(), key=itemgetter(0))), 'timestamp': decimal.Decimal('1489012800631.612'), 'updated_timestamp': decimal.Decimal('1514980909.436671')}] }, 'UnprocessedKeys': {} } return payload @pytest.mark.parametrize( 'required_key', ['account_id', 'account_type', 'isrcs', 'territories']) @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @mock_aws def test_bulk_remove_ownership_required_payload( client, feature_engine, test_payload, test_headers, required_key): """Expect error because required payload is missing.""" from moto.core import patch_client, patch_resource patch_client(outside_dynamodb_client) patch_resource(dynamodb_resource) del test_payload[required_key] result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert len(payload['message']) == 1 assert required_key in payload['message'] @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) def test_bulk_remove_ownership_account_type_invalid( client, feature_engine, test_payload, test_headers): """Expect error because account type is invalid.""" test_payload['account_type'] = 'invalid' result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert len(payload['message']) == 1 assert 'account_type' in payload['message'] @pytest.mark.parametrize('invalid_value', ['invalid', -1]) @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) def test_bulk_remove_ownership_account_id_invalid( client, feature_engine, test_payload, test_headers, invalid_value): """Expect error because account_id is invalid. Should handle case when account_id < 1. """ test_payload['account_id'] = invalid_value result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert len(payload['message']) == 1 assert 'account_id' in payload['message'] @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_isrc_does_not_exist_in_dynamo( client, feature_engine, test_payload, test_headers): """Expect error because ISRC doesn't exist in DynamoDB.""" test_payload['isrcs'] = ['MD3546'] result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 404 assert payload['code'] == error.ISRC_NOT_FOUND assert payload['message'] == ['MD3546'] @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_isrc_does_not_belong_to_vendor( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error becaouse ISRC doesn't belong to vendor.""" session_execute_mock = mock_mr_session_scope([]) result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 403 assert payload['code'] == error.ISRC_NOT_OWNED_BY_ACCOUNT assert payload['message'] == test_payload['isrcs'] expected_sql_args = [ track_sql.SELECT_TUID_BY_ISRC_AND_VENDOR_ID, { 'vendor_id': test_payload['account_id'], 'isrc_list': test_payload['isrcs'] } ] session_execute_mock.assert_called_with(*expected_sql_args) def masters_registry_response_without_internal_conflicts(**kwargs): """ Response from dynamodb with multiple items which have internal conflicts. """ payload = { 'ResponseMetadata': { 'HTTPHeaders': {}, 'HTTPStatusCode': 200, 'RequestId': '', 'RetryAttempts': 0}, 'Responses': { 'test-masters_active': [ {'isrc': 'QA123', 'locked_territories': {}, 'territories': { 'AF': [{'tuid': decimal.Decimal('789')}], 'AD': [{'tuid': decimal.Decimal('456')}]}, 'timestamp': decimal.Decimal('1489012800631.612'), 'updated_timestamp': decimal.Decimal('1514980909.436671')}, {'isrc': 'QA456', 'locked_territories': {}, 'territories': { 'CA': [{'tuid': decimal.Decimal('123')}], }, 'timestamp': decimal.Decimal('1489012800631.612'), 'updated_timestamp': decimal.Decimal('1514980909.436671')}] }, 'UnprocessedKeys': {} } return payload @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_without_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_isrc_without_conflicts( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error because ISRCs have no internal conflicts.""" tuid_isrc_pairs = [(123, 'QA123'), (789, 'QA456')] mock_mr_session_scope(tuid_isrc_pairs) test_payload['isrcs'] = ['QA123', 'QA456'] result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert payload['code'] == error.NO_INTERNAL_CONFLICTS assert payload['message'] == test_payload['isrcs'] @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_isrc_does_not_belong_to_subaccount( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error becaouse ISRC doesn't belong to subaccount.""" session_execute_mock = mock_mr_session_scope([]) test_payload['account_type'] = 'subaccount' result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 403 assert payload['code'] == error.ISRC_NOT_OWNED_BY_ACCOUNT assert payload['message'] == test_payload['isrcs'] expected_sql_args = [ track_sql.SELECT_TUID_BY_ISRC_AND_SUBACCOUNT_ID, { 'subaccount_id': test_payload['account_id'], 'isrc_list': test_payload['isrcs'] } ] session_execute_mock.assert_called_with(*expected_sql_args) @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) def test_bulk_remove_ownership_isrc_does_not_exist_in_db( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error because ISRC doesn't exist in DB.""" session_execute_mock = mock_mr_session_scope([]) result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) assert result.status_code == 404 assert payload['code'] == error.NOT_EXISTING_ISRCS_IN_TRACK assert payload['message'] == test_payload['isrcs'] expected_sql_args = [ track_sql.SELECT_ISRCS, {'isrcs': test_payload['isrcs']}] session_execute_mock.assert_called_with(*expected_sql_args) @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_not_tuid_on_given_territory_vendor( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error because vendor has no tuids on one of territories.""" tuid_isrc_pairs = [(456, 'QA123')] session_execute_mock = mock_mr_session_scope(tuid_isrc_pairs) result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) expected_error = error.NO_TUIDS_ON_GIVEN_TERRITORY.format( 'AF', 'QA123') assert result.status_code == 404 assert payload['message'] == expected_error expected_sql_args = [ track_sql.SELECT_TUID_BY_ISRC_AND_VENDOR_ID, {'vendor_id': 1, 'isrc_list': test_payload['isrcs']}] session_execute_mock.assert_called_with(*expected_sql_args) @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) def test_bulk_remove_ownership_not_tuid_on_given_territory_subaccount( client, feature_engine, test_payload, test_headers, mock_mr_session_scope): """Expect error because subaccount has no tuids on one of territories.""" tuid_isrc_pairs = [(456, 'QA123')] session_execute_mock = mock_mr_session_scope(tuid_isrc_pairs) test_payload['account_type'] = 'subaccount' result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) expected_error = error.NO_TUIDS_ON_GIVEN_TERRITORY.format( 'AF', 'QA123') assert result.status_code == 404 assert payload['message'] == expected_error expected_sql_args = [ track_sql.SELECT_TUID_BY_ISRC_AND_SUBACCOUNT_ID, {'subaccount_id': 1, 'isrc_list': test_payload['isrcs']}] session_execute_mock.assert_called_with(*expected_sql_args) def test_bulk_remove_ownership_orchard_user_id_is_required( client, feature_engine): """Expect error because Orchard-User-Id is missing.""" result = client.put( '/ownership/conflicts/resolve', headers={'Correlation-Id': 'test id'}) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert len(payload['message']) == 1 assert 'Orchard-User-Id' in payload['message'] def test_bulk_remove_ownership_correlation_id_is_required( client, feature_engine): """Expect error because Correlation-Id is missing.""" result = client.put( '/ownership/conflicts/resolve', headers={'Orchard-User-Id': 'test id'}) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert len(payload['message']) == 1 assert 'Correlation-Id' in payload['message'] def test_bulk_remove_ownership_empty_payload( client, feature_engine, test_headers): """Expect error when sending empty payload.""" result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps({})) payload = json.loads(result.data.decode()) assert result.status_code == 400 assert payload['message'] == error.EMPTY_PAYLOAD_MESSAGE @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) @patch( 'masters_registry.models.users.get_orchard_user_names', new=Mock(return_value=False)) @patch( 'masters_registry.tasks.bulk.bulk_resolve_conflicts.delay', new=Mock()) def test_bulk_remove_ownership_success( client, feature_engine, test_payload, test_headers, mock_mr_session_scope, setup_bulk_status_db): """Expect to create celery task with correct parameters.""" tuid_isrc_pairs = [(123, 'QA123')] mock_mr_session_scope(tuid_isrc_pairs) test_correlation_id = 'test correlation id' test_payload['territories'] = ['AF'] account_id = test_payload['account_id'] result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) expected_payload = {'task_id': 1} assert result.status_code == 200 assert payload == expected_payload expected_celery_call_args = [ {'QA123': [123]}, {'QA123': ['AF']}, '123', 1, test_correlation_id, account_id] bulk.bulk_resolve_conflicts.delay.assert_called_with( *expected_celery_call_args) created_task = task_model.get_task(1).message.as_dict() expected_task = { 'create_datetime': ANY, 'correlation_id': 'test correlation id', 'count': 1, 'user_name': '', 'finish_datetime': None, 'user_id': '123', 'result': None, 'status': 'PROCESSING', 'id': 1, 'type': 'BULK_RESOLVE_INTERNAL_CONFLICTS', 'account_type': None, 'account_id': None } assert created_task == expected_task @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) @patch( 'masters_registry.models.users.get_orchard_user_names', new=Mock(return_value=False)) @patch( 'masters_registry.tasks.bulk.bulk_resolve_conflicts.delay', new=Mock()) def test_bulk_remove_ownership_success_world_wide( client, feature_engine, test_payload, test_headers, mock_mr_session_scope, setup_bulk_status_db): """Expect to create celery task with correct parameters.""" tuid_isrc_pairs = [(123, 'QA123')] mock_mr_session_scope(tuid_isrc_pairs) test_correlation_id = 'test correlation id' test_payload['territories'] = ['WW'] account_id = test_payload['account_id'] result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(test_payload)) payload = json.loads(result.data.decode()) expected_payload = {'task_id': 1} assert result.status_code == 200 assert payload == expected_payload expected_celery_call_args = [ {'QA123': [123]}, {'QA123': ['AF', 'QA']}, '123', 1, test_correlation_id, account_id] bulk.bulk_resolve_conflicts.delay.assert_called_with( *expected_celery_call_args) created_task = task_model.get_task(1).message.as_dict() expected_task = { 'create_datetime': ANY, 'correlation_id': 'test correlation id', 'count': 1, 'user_name': '', 'finish_datetime': None, 'user_id': '123', 'result': None, 'status': 'PROCESSING', 'id': 1, 'type': 'BULK_RESOLVE_INTERNAL_CONFLICTS', 'account_type': None, 'account_id': None } assert created_task == expected_task @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) @patch( 'masters_registry.models.users.get_orchard_user_names', new=Mock(return_value=False)) @patch( 'masters_registry.tasks.bulk.bulk_resolve_conflicts.delay', new=Mock()) def test_bulk_remove_ownership_all_except_these_territories_case( client, feature_engine, test_headers, mock_mr_session_scope, setup_bulk_status_db): """ Should remove ownership when 'All except these territories' is selected. On UI there is an option to remove ownership from all except entered territories. In this case we get list of all possible territories except entered and we need to remove ONLY those territories that have conflict for given ISRC. """ tuid_isrc_pairs = [(123, 'QA123')] mock_mr_session_scope(tuid_isrc_pairs) request_payload = { 'account_id': 1, 'account_type': 'vendor', 'isrcs': ['QA123'], # Territories should be filtered, because ISRC has conflict only on AF 'territories': ['AF', 'AD'] } result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(request_payload)) response_payload = json.loads(result.data.decode()) expected_payload = {'task_id': 1} assert result.status_code == 200 assert response_payload == expected_payload expected_celery_call_args = [ {'QA123': [123]}, {'QA123': ['AF']}, '123', 1, test_headers['Correlation-Id'], request_payload['account_id']] bulk.bulk_resolve_conflicts.delay.assert_called_with( *expected_celery_call_args) created_task = task_model.get_task(1).message.as_dict() expected_task = { 'create_datetime': ANY, 'correlation_id': 'test correlation id', 'count': 1, 'user_name': '', 'finish_datetime': None, 'user_id': '123', 'result': None, 'status': 'PROCESSING', 'id': 1, 'type': 'BULK_RESOLVE_INTERNAL_CONFLICTS', 'account_type': None, 'account_id': None } assert created_task == expected_task def masters_registry_response_with_internal_conflicts_multiple_isrcs(**kwargs): """ Response from dynamodb with multiple items which have internal conflicts. """ payload = { 'ResponseMetadata': { 'HTTPHeaders': {}, 'HTTPStatusCode': 200, 'RequestId': '', 'RetryAttempts': 0}, 'Responses': { 'test-masters_active': [ {'isrc': 'QA123', 'locked_territories': {}, 'territories': { 'AF': [ {'tuid': decimal.Decimal('123')}, {'tuid': decimal.Decimal('789')}], 'AD': [{'tuid': decimal.Decimal('456')}]}, 'timestamp': decimal.Decimal('1489012800631.612'), 'updated_timestamp': decimal.Decimal('1514980909.436671')}, {'isrc': 'QA456', 'locked_territories': {}, 'territories': { 'CA': [ {'tuid': decimal.Decimal('123')}, {'tuid': decimal.Decimal('789')}] }, 'timestamp': decimal.Decimal('1489012800631.612'), 'updated_timestamp': decimal.Decimal('1514980909.436671')}] }, 'UnprocessedKeys': {} } return payload @patch( 'masters_registry.connectors.dynamodb.client.batch_get_item', new=masters_registry_response_with_internal_conflicts_multiple_isrcs) @patch( 'masters_registry.logic.ownership.get_isrcs_from_track_table', new=Mock(return_value=True)) @patch( 'masters_registry.models.users.get_orchard_user_names', new=Mock(return_value=False)) @patch( 'masters_registry.tasks.bulk.bulk_resolve_conflicts.delay', new=Mock()) def test_bulk_remove_ownership_all_except_these_territories_multiple_isrcs( client, feature_engine, test_headers, mock_mr_session_scope, setup_bulk_status_db): """Should handle case with multiple ISRCs.""" tuid_isrc_pairs = [(123, 'QA123'), (789, 'QA456')] mock_mr_session_scope(tuid_isrc_pairs) request_payload = { 'account_id': 1, 'account_type': 'vendor', 'isrcs': ['QA123', 'QA456'], 'territories': ['AF', 'AD', 'CA', 'US', 'UA'] } result = client.put( '/ownership/conflicts/resolve', headers=test_headers, data=json.dumps(request_payload)) response_payload = json.loads(result.data.decode()) expected_payload = {'task_id': 1} assert result.status_code == 200 assert response_payload == expected_payload expected_celery_call_args = [ {'QA123': [123], 'QA456': [789]}, {'QA123': ['AF'], 'QA456': ['CA']}, '123', 1, test_headers['Correlation-Id'], request_payload['account_id']] bulk.bulk_resolve_conflicts.delay.assert_called_with( *expected_celery_call_args) created_task = task_model.get_task(1).message.as_dict() expected_task = { 'create_datetime': ANY, 'correlation_id': 'test correlation id', 'count': 2, 'user_name': '', 'finish_datetime': None, 'user_id': '123', 'result': None, 'status': 'PROCESSING', 'id': 1, 'type': 'BULK_RESOLVE_INTERNAL_CONFLICTS', 'account_type': None, 'account_id': None } assert created_task == expected_task