"""Unit tests for the create_project Lambda function.""" from unittest.mock import patch from common.schemas.state_machine_schema import StateMachineSchema import pytest from src import index from src.constants.queries import ( CREATE_PROJECT, GET_PRODUCT_BY_UPC, GET_PROJECT_BY_PROJECT_CODE, ) from src.exceptions import ( ArtistIdNotFoundException, CreateProjectException, DuplicateProjectCodeException, ) @patch('src.index.graphql_gateway') def test_handler_project_not_found_creates( mock_gql_gateway, context_event, get_product_by_upc_no_result, get_project_not_found, create_project_response, ): """Handler creates project when UPC unknown and no project exists.""" mock_gql_gateway.execute.side_effect = [ get_product_by_upc_no_result, # check_project_code_mismatch get_project_not_found, # check_for_project create_project_response, # create_project ] result = index.handler(context_event, None) assert mock_gql_gateway.execute.call_count == 3 assert result['project']['project_id'] == 9002 @patch('src.index.graphql_gateway') def test_handler_project_already_exists_skips_create( mock_gql_gateway, context_event, get_product_by_upc_matching_project, get_project_found, ): """Handler returns existing project_id without creating a new one.""" mock_gql_gateway.execute.side_effect = [ get_product_by_upc_matching_project, # check_project_code_mismatch get_project_found, # check_for_project ] result = index.handler(context_event, None) assert mock_gql_gateway.execute.call_count == 2 assert result['project']['project_id'] == 9001 @patch('src.index.graphql_gateway') def test_handler_project_code_mismatch_logs_and_continues( mock_gql_gateway, context_event, get_product_by_upc_mismatched_project, get_project_not_found, create_project_response, ): """Handler logs a warning on code mismatch but continues execution.""" mock_gql_gateway.execute.side_effect = [ get_product_by_upc_mismatched_project, # check_project_code_mismatch get_project_not_found, # check_for_project create_project_response, # create_project ] result = index.handler(context_event, None) assert mock_gql_gateway.execute.call_count == 3 assert result['project']['project_id'] == 9002 @patch('src.index.graphql_gateway') def test_handler_graphql_error_raises_create_project_exception( mock_gql_gateway, context_event, get_product_by_upc_no_result, get_project_not_found, ): """Handler wraps unexpected GraphQL errors in CreateProjectException.""" from lambdacommon.graphql.graphql import GraphQLError mock_gql_gateway.execute.side_effect = [ get_product_by_upc_no_result, get_project_not_found, GraphQLError([{'message': 'Something went wrong'}]), ] with pytest.raises(CreateProjectException): index.handler(context_event, None) @patch('src.index.graphql_gateway') def test_handler_returns_serialized_context( mock_gql_gateway, context_event, get_product_by_upc_no_result, get_project_not_found, create_project_response, ): """Handler returns a fully serialized state machine context dict.""" mock_gql_gateway.execute.side_effect = [ get_product_by_upc_no_result, get_project_not_found, create_project_response, ] result = index.handler(context_event, None) assert 'project' in result assert 'product' in result assert result['product']['upc'] == '886443197781' assert result['project']['project_id'] == 9002 # check_project_code_mismatch @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_no_upc(mock_gql_gateway, context_event): """Skips the check when the product has no UPC.""" sm_context = StateMachineSchema().load(context_event) sm_context.product.upc = None index.check_project_code_mismatch(sm_context) mock_gql_gateway.execute.assert_not_called() @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_no_existing_product( mock_gql_gateway, context_event, get_product_by_upc_no_result, ): """No exception raised when productByUpc returns null.""" mock_gql_gateway.execute.return_value = get_product_by_upc_no_result sm_context = StateMachineSchema().load(context_event) index.check_project_code_mismatch(sm_context) # should not raise call_args = mock_gql_gateway.execute.call_args assert call_args[0][0] == GET_PRODUCT_BY_UPC assert call_args[0][1] == {'upc': '886443197781'} @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_matching_code( mock_gql_gateway, context_event, get_product_by_upc_matching_project, ): """No exception when project codes match.""" mock_gql_gateway.execute.return_value = get_product_by_upc_matching_project sm_context = StateMachineSchema().load(context_event) index.check_project_code_mismatch(sm_context) # should not raise @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_different_code_logs_warning( mock_gql_gateway, context_event, get_product_by_upc_mismatched_project, ): """Logs a warning when codes differ but does not raise.""" mock_gql_gateway.execute.return_value = ( get_product_by_upc_mismatched_project ) sm_context = StateMachineSchema().load(context_event) index.check_project_code_mismatch(sm_context) # should not raise mock_gql_gateway.execute.assert_called_once() @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_normalises_sony_prefix( mock_gql_gateway, context_event, ): """SONY:id: prefix is stripped before comparison — no exception raised.""" context_event['project']['project_code'] = '1074641' mock_gql_gateway.execute.return_value = { 'data': { 'productByUpc': { 'vendorId': 34563, 'project': { 'projectCode': 'SONY:id:1074641', 'projectId': 9001, }, } } } sm_context = StateMachineSchema().load(context_event) index.check_project_code_mismatch(sm_context) # should not raise @patch('src.index.graphql_gateway') def test_check_project_code_mismatch_product_no_project( mock_gql_gateway, context_event, get_product_by_upc_no_project, ): """No exception when product exists but has no associated project.""" mock_gql_gateway.execute.return_value = get_product_by_upc_no_project sm_context = StateMachineSchema().load(context_event) index.check_project_code_mismatch(sm_context) # should not raise # check_for_project @patch('src.index.graphql_gateway') def test_check_for_project_found( mock_gql_gateway, context_event, get_project_found, ): """Returns project dict when project exists.""" mock_gql_gateway.execute.return_value = get_project_found sm_context = StateMachineSchema().load(context_event) result = index.check_for_project(sm_context) assert result['projectId'] == 9001 call_args = mock_gql_gateway.execute.call_args assert call_args[0][0] == GET_PROJECT_BY_PROJECT_CODE assert call_args[0][1] == { 'projectCode': '1074641', 'accountId': 34563, 'subaccountId': 57803, } @patch('src.index.graphql_gateway') def test_check_for_project_not_found( mock_gql_gateway, context_event, get_project_not_found, ): """Returns empty dict when project does not exist.""" mock_gql_gateway.execute.return_value = get_project_not_found sm_context = StateMachineSchema().load(context_event) result = index.check_for_project(sm_context) assert result == {} def test_check_for_project_no_project_code(context_event): """Skips GraphQL call when context has no project_code.""" context_event['project']['project_code'] = None sm_context = StateMachineSchema().load(context_event) result = index.check_for_project(sm_context) assert result == {} @patch('src.index.graphql_gateway') def test_check_for_project_null_subaccount_defaults_to_zero( mock_gql_gateway, context_event, get_project_not_found, ): """Null subaccount_id is sent as 0 in the GraphQL payload.""" context_event['product']['subaccount_id'] = None mock_gql_gateway.execute.return_value = get_project_not_found sm_context = StateMachineSchema().load(context_event) index.check_for_project(sm_context) payload = mock_gql_gateway.execute.call_args[0][1] assert payload['subaccountId'] == 0 # create_project @patch('src.index.graphql_gateway') def test_create_project_sends_correct_payload( mock_gql_gateway, context_event, create_project_response, ): """Sends the expected payload to GraphQL.""" mock_gql_gateway.execute.return_value = create_project_response sm_context = StateMachineSchema().load(context_event) result = index.create_project(sm_context) assert result['projectId'] == 9002 call_args = mock_gql_gateway.execute.call_args assert call_args[0][0] == CREATE_PROJECT data = call_args[0][1]['data'] assert data['projectCode'] == '1074641' assert data['name'] == 'Test Project' assert data['accountId'] == 34563 assert data['subaccountId'] == 57803 assert data['artistId'] == 11111 @patch('src.index.graphql_gateway') def test_create_project_artist_id_none_when_no_label_participants( mock_gql_gateway, context_event, create_project_response, ): """Payload has artistId as None when label_participants is null.""" context_event['label_participants'] = None mock_gql_gateway.execute.return_value = create_project_response sm_context = StateMachineSchema().load(context_event) index.create_project(sm_context) data = mock_gql_gateway.execute.call_args[0][1]['data'] assert data['artistId'] is None @patch('src.index.graphql_gateway') def test_create_project_duplicate_code_raises( mock_gql_gateway, context_event, ): """Raises DuplicateProjectCodeException on race-condition duplicate.""" from lambdacommon.graphql.graphql import GraphQLError mock_gql_gateway.execute.side_effect = GraphQLError( [{'message': "Project code '1074641' already exists"}] ) sm_context = StateMachineSchema().load(context_event) with pytest.raises(DuplicateProjectCodeException): index.create_project(sm_context) @patch('src.index.graphql_gateway') def test_create_project_other_graphql_error_reraises( mock_gql_gateway, context_event, ): """Non-duplicate GraphQLErrors are re-raised as-is.""" from lambdacommon.graphql.graphql import GraphQLError mock_gql_gateway.execute.side_effect = GraphQLError( [{'message': 'Unexpected error'}] ) sm_context = StateMachineSchema().load(context_event) with pytest.raises(GraphQLError): index.create_project(sm_context) # _retrieve_artist_id def test_retrieve_artist_id_found(context_event): """Returns artist_id when label participant name matches project artist.""" sm_context = StateMachineSchema().load(context_event) assert index._retrieve_artist_id(sm_context) == 11111 def test_retrieve_artist_id_no_match(context_event): """Raises ArtistIdNotFoundException when no participant name matches.""" context_event['label_participants'][0]['name'] = 'Someone Else' sm_context = StateMachineSchema().load(context_event) with pytest.raises(ArtistIdNotFoundException): index._retrieve_artist_id(sm_context) def test_retrieve_artist_id_no_label_participants(context_event): """Returns None when label_participants is null.""" context_event['label_participants'] = None sm_context = StateMachineSchema().load(context_event) assert index._retrieve_artist_id(sm_context) is None def test_retrieve_artist_id_no_project_artist(context_event): """Returns None when project has no artist set.""" context_event['project']['artist'] = None sm_context = StateMachineSchema().load(context_event) assert index._retrieve_artist_id(sm_context) is None