"""Test error formatters.""" from mock import Mock, patch import pytest from switchboard_consumer.constants.error import ( ERROR_CODE_TRACK_PROCESSING, ERROR_MESSAGE_TRACK_INSERT_FAILED, ERROR_MESSAGE_TRACK_INSERT_FAILED_PARSING, ERROR_MESSAGE_TRACK_INSERT_RAW_ERROR, UNKNOWN_ERROR_CODE, UNKNOWN_ERROR_MESSAGE ) from switchboard_consumer.constants.system import SONY from switchboard_consumer.formatters.errors import ( format_create_track_errors, format_exception, format_graphql_errors, format_partial_track_error, parse_graphql_error) @pytest.mark.parametrize('errors, formatted_errors', [ # Barebones error with default fallback fields ( [{ 'extensions': { 'code': 'some_code', }, 'message': 'An error message', }], [{ 'code': 'some_code', 'message': 'An error message' }], ), # Error with a body object and fields ( [ { 'message': '400: BAD REQUEST', 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'body': { 'code': 'not_found', 'message': 'Invalid project id' } } } } ], [ {'code': 'not_found', 'message': 'Invalid project id'} ], ), # Error with body object and no valid fields ( [ { 'message': '400: BAD REQUEST', 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'body': { 'random_field': 'hello', } } } } ], [ {'code': 'INTERNAL_SERVER_ERROR', 'message': '400: BAD REQUEST'} ], ), # Error with some valid and invalid fields ( [{ 'random_field': 'hello', 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'body': { 'random_field': 'hello', } } } }], [{ 'code': 'INTERNAL_SERVER_ERROR', 'message': UNKNOWN_ERROR_MESSAGE }], ), # Error with no valid parsable fields ( [ { 'random_field': 'hello' } ], [ { 'code': UNKNOWN_ERROR_CODE, 'message': UNKNOWN_ERROR_MESSAGE } ], ), # Error where message is a dict ( [ { 'message': {'a_key': 'and_a_value'}, 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'body': { 'random_field': 'hello', } } } } ], [ {'code': 'INTERNAL_SERVER_ERROR', 'message': "{'a_key': 'and_a_value'}"} ], ), # https://sme-apps.atlassian.net/browse/SOI-476 ( [{ 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'exception': { 'stacktrace': [ "'Error: 503: Service Temporarily Unavailable'", ] }, 'response': { 'body': '503 Service Unavailable', 'status': 503, 'statusText': 'Service Temporarily Unavailable', 'url': 'https://qa-ows-artist.theorchard.io/artist/2000118' } }, 'message': '503: Service Temporarily Unavailable', }], [ { 'code': 'INTERNAL_SERVER_ERROR', 'message': '503 Service Temporarily Unavailable ' 'https://qa-ows-artist.theorchard.io/artist/2000118' } ] ), ]) def test_error_parsing(errors, formatted_errors): """Test whether errors are formatted correctly.""" output = format_graphql_errors(errors) assert output == formatted_errors def test_exception_error_parsing(): """Test formatting of error array when passed exception.""" with pytest.raises(Exception) as err: raise Exception('Exception message') output = format_exception(err) assert output == { 'code': 'daemon_exception', 'message': '{}: {}'.format(type(err).__name__, err) } def test_format_create_track_errors_bulk_errors(create_product_message): """Test bulk error formatter is called correctly.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message errors = [ { 'path': [ 'saveTracks' ], 'extensions': { 'code': 'create_tracks_error_code', 'response': { 'body': { 'message': { 'tracks': { '2': { 'isrc': [ 'Not valid' ], 'writers': { '0': { 'type': [ 'Not a valid choice.' ] } } } } } } } }, 'message': '400: BAD REQUEST', } ] mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } with patch(('switchboard_consumer.' 'formatters.' 'errors.' 'format_bulk_create_track_error') ) as mock_bulk_create_track_error: format_create_track_errors( tracks, kafka_message, errors, mapping, Mock()) mock_bulk_create_track_error.assert_called_once() def test_format_bulk_create_track_error(create_product_message): """Test bulk error formatter handles a valid parsable error.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message errors = [ { 'path': [ 'saveTracks' ], 'extensions': { 'code': 'create_tracks_error_code', 'response': { 'body': { 'message': { 'tracks': { '2': { 'isrc': [ 'Not valid' ], 'writers': { '0': { 'type': [ 'Not a valid choice.' ] } } } } } } } }, 'message': '400: BAD REQUEST', } ] mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } processing_errors = format_create_track_errors( tracks, kafka_message, errors, mapping, Mock()) # 3 tracks with 3 errors assert len(processing_errors) == 3 # The first and second tracks will have failed with generic message assert processing_errors[0]['errors'] == [ { 'code': ERROR_CODE_TRACK_PROCESSING, 'message': ERROR_MESSAGE_TRACK_INSERT_FAILED } ] assert processing_errors[0]['ids'] == [ { 'businessKey': 'GBBKS1000335', 'businessKeyType': 'ISRC', 'system': SONY, 'localId': '20001', } ] assert processing_errors[1]['errors'] == [ { 'code': ERROR_CODE_TRACK_PROCESSING, 'message': ERROR_MESSAGE_TRACK_INSERT_FAILED } ] assert processing_errors[1]['ids'] == [ { 'businessKey': 'GBBKS1000336', 'businessKeyType': 'ISRC', 'system': SONY, 'localId': '20002', } ] # third track has the parsable bulk error assert processing_errors[2]['errors'] == [ { 'code': 'processing', 'message': 'Track failed to insert: isrc_0 - Not valid' }, { 'code': 'processing', 'message': 'Track failed to insert: writers_0_type_0 - Not a valid choice.' } ] assert processing_errors[2]['ids'] == [ { 'businessKey': 'GBBKS1000337', 'businessKeyType': 'ISRC', 'system': SONY, 'localId': '20003', } ] def test_format_bulk_create_track_error_with_unparsable_error( create_product_message): """Test bulk error formatter handles a unparsable error.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message errors = [ { 'path': [ 'saveTracks' ], 'extensions': { 'code': 'create_tracks_error_code', 'response': { 'body': { 'message': { 'wrong': 'wrong' } } } }, 'message': '400: BAD REQUEST', } ] mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } processing_errors = format_create_track_errors( tracks, kafka_message, errors, mapping, Mock()) # 3 tracks with 3 errors assert len(processing_errors) == 3 # Each track should have the same error as we cannot parse # anything specific for error in processing_errors: assert error['errors'] == [ { 'code': ERROR_CODE_TRACK_PROCESSING, 'message': ERROR_MESSAGE_TRACK_INSERT_RAW_ERROR.format( ERROR_MESSAGE_TRACK_INSERT_FAILED_PARSING) } ] def test_format_create_track_errors_partial_errors(create_product_message): """Test partial error formatter is called correctly.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message errors = [ { 'path': [ 'saveTracks', 1 ], 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'url': 'https://qa-ows-track.theorchard.io', 'status': 400, 'statusText': 'BAD REQUEST', 'body': { 'code': 'validation_error', 'message': 'An error' } }, }, 'message': '400: BAD REQUEST', }, { 'path': [ 'saveTracks', 2 ], 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'url': 'https://qa-ows-track.theorchard.io', 'status': 400, 'statusText': 'BAD REQUEST', 'body': { 'code': 'validation_error', 'message': 'An error' } }, }, 'message': '400: BAD REQUEST', }, ] mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } with patch(('switchboard_consumer.' 'formatters.' 'errors.' 'format_partial_track_error') ) as mock_format_partial_track_error: format_create_track_errors( tracks, kafka_message, errors, mapping, Mock()) assert mock_format_partial_track_error.call_count == 2 def test_format_partial_track_error(create_product_message): """Test that a partial track error is handled correctly.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message errors = [ { 'path': [ 'saveTracks', 1 ], 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'url': 'https://qa-ows-track.theorchard.io', 'status': 400, 'statusText': 'BAD REQUEST', 'body': { 'code': 'validation_error', 'message': 'An error' } }, }, 'message': '400: BAD REQUEST', }, { 'path': [ 'saveTracks', 2 ], 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'url': 'https://qa-ows-track.theorchard.io', 'status': 400, 'statusText': 'BAD REQUEST', 'body': { 'code': 'validation_error', 'message': 'An error' } }, }, 'message': '400: BAD REQUEST', }, ] mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } processing_error = format_partial_track_error( tracks, kafka_message, errors[0], mapping, Mock()) # Failed tracks should include their errors assert processing_error['errors'] == [ { 'code': ERROR_CODE_TRACK_PROCESSING, 'message': 'Track failed to insert: An error' } ] # Failed track should include source mapping assert processing_error['ids'] == [ mapping.get('GBBKS1000336') ] def test_format_partial_track_unparsable_error(create_product_message): """Test that a partial track error is handled correctly.""" tracks = [ { 'isrc': 'GBBKS1000335' }, { 'isrc': 'GBBKS1000336' }, { 'isrc': 'GBBKS1000337' }, ] kafka_message = create_product_message error = { 'path': [ 'saveTracks', 1 ], 'extensions': None } mapping = { 'GBBKS1000335': { 'system': SONY, 'localId': '20001', }, 'GBBKS1000336': { 'system': SONY, 'localId': '20002' }, 'GBBKS1000337': { 'system': SONY, 'localId': '20003' } } processing_error = format_partial_track_error( tracks, kafka_message, error, mapping, Mock()) # Failed tracks should include their errors assert processing_error['errors'] == [ { 'code': ERROR_CODE_TRACK_PROCESSING, 'message': ERROR_MESSAGE_TRACK_INSERT_RAW_ERROR.format( UNKNOWN_ERROR_MESSAGE) } ] # Failed track should include source mapping assert processing_error['ids'] == [ mapping.get('GBBKS1000336') ] def test_parse_graphql_error_with_body_as_string(): """Test GQL error parsing with body as a string.""" error = { 'extensions': { 'response': { 'body': 'unexpected string' } } } parsed_error = parse_graphql_error(error, 'some_key') assert parsed_error == 'unexpected string' def test_parse_graphql_error_with_bad_body_format(): """Test GQL error parsing with bad body format.""" error = { 'extensions': { 'response': { 'body': [] } } } parsed_error = parse_graphql_error(error, 'some_key') assert parsed_error == UNKNOWN_ERROR_MESSAGE def test_parse_graphql_error_with_escaped_json(): """Test GQL error parsing with escaped JSON.""" error = { 'extensions': { 'code': 'INTERNAL_SERVER_ERROR', 'response': { 'body': { "'code'": "'duplicate'", "'message'": "'Duplicate data found for field `publishers`'" }, } } } parsed_error = parse_graphql_error(error, 'tracks') assert parsed_error == UNKNOWN_ERROR_MESSAGE