"""Tests for Handlers.""" import datetime import json import uuid from unittest.mock import MagicMock, call, patch import pytest from owsresponse import response, status from pythonfeatures import pythonfeatures from notifications import handlers from notifications.constants import header from notifications.constants.stream import DEFAULT_USER_FEED @patch('notifications.logic.stream.add_label_activities_to_user', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_add_activity(neo4j_exit, neo4j_enter, mock_add_activity, fixture_client): """Test add activity response.""" url = '/activity' data = { 'feed_name': 'label_spike_detector', 'feed_id': 'vendor_123', 'payload': { 'actor': 'The Orchard Activity Detector', 'verb': 'Placed', 'object': 'Track', 'target': 'Playlist', 'track_id': 123, }, } result = fixture_client.post(url, data=json.dumps(data)) data['payload']['original_feed'] = 'label_spike_detector:vendor_123' mock_add_activity.assert_called_with( activity_type='Vendor', activity_id=123, subscription_feed_type='labelSpikeDetector', payload=data['payload'], ) assert result.status_code == status.OK @pytest.mark.parametrize( 'data', [ {'feed_id': 'vendor_123', 'payload': {}}, {'feed_name': 'label_track_placement', 'payload': {}}, {'feed_name': 'label_track_placement', 'feed_id': 'vendor_123'}, ], ) @patch.object( pythonfeatures, 'get_single_feature_by_attributes', return_value=response.Response(message='control'), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_add_activity_bad_parameters(neo4j_exit, neo4j_enter, mock_features, fixture_client, data): """Test add activity response when called with bad parameters.""" url = '/activity' result = fixture_client.post(url, data=json.dumps(data)) assert result.status_code == status.BAD_REQUEST @patch( 'notifications.logic.stream.add_label_activities_to_user', return_value=response.Response(message={'user', 'profiles'}), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_add_activity_feature_on( neo4j_exit, neo4j_enter, mock_add_label_activities, fixture_client ): """Test add activity response.""" url = '/activity' data = { 'feed_name': 'label_video_product_rejection', 'feed_id': 'vendor_123', 'payload': { 'actor': 'The Orchard Activity Detector', 'verb': 'Placed', 'object': 'Track', 'target': 'Playlist', 'track_id': 123, }, } fixture_client.post(url, data=json.dumps(data)) data['payload']['original_feed'] = 'label_video_product_rejection:vendor_123' mock_add_label_activities.assert_called_with( activity_type='Vendor', activity_id=123, subscription_feed_type='videoProductRejection', payload=data['payload'], ) @patch( 'notifications.logic.subscriptions.subscription_for_ws_user', return_value=response.Response({'new': 'subscription'}), ) @patch( 'notifications.logic.stream.subscribe', return_value=response.Response({'old': 'subscription'}) ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe(neo4j_exit, neo4j_enter, mock_subscribe, mock_subscribe_ws, fixture_client): """Test subscribe response.""" url = '/subscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_TYPE: 'vendor', header.GRASS_ACCOUNT_ID: 123, } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_analytics_digest', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_subscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) assert result.status_code == status.OK @patch( 'notifications.logic.stream.subscribe', return_value=response.Response({'old': 'subscription'}) ) @patch( 'notifications.logic.subscriptions.subscription_for_ws_user', return_value=response.Response({'new': 'subscription'}), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_from_ws( neo4j_exit, neo4j_enter, mock_subscribe_ws, mock_subscribe, fixture_client ): """Test subscribe response.""" url = '/subscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_TYPE: 'vendor', header.GRASS_ACCOUNT_ID: 123, } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_release_approval', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_subscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) mock_subscribe_ws.assert_called_once() assert result.status_code == status.OK @patch('notifications.logic.stream.subscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_without_headers(neo4j_exit, neo4j_enter, mock_subscribe, fixture_client): """Test subscribe response.""" url = '/subscribe' headers = {header.ORCHARD_USER_ID: 'oa:123'} data = { 'user_feed_name': 'user_email_notifications', 'feed_name': 'label_analytics_digest', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_subscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) assert result.status_code == status.OK @patch('notifications.logic.stream.subscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_without_feed_id(neo4j_exit, neo4j_enter, mock_subscribe, fixture_client): """Test subscribe response when called without a feed id.""" url = '/subscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_subscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], 'vendor_123', None, ) assert result.status_code == status.OK @patch('notifications.logic.stream.subscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_orchard_feed_user_feed_id( neo4j_exit, neo4j_enter, mock_subscribe, fixture_client ): """Test subscribe when called for a unique orchard feed a user feed id.""" url = '/subscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_TYPE: 'vendor', header.GRASS_ACCOUNT_ID: 123, } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'orchard_trending_tracks_global', 'feed_id': 'orchard', 'user_feed_id': 'john-doe@gmail.com', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_subscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], data['user_feed_id'], ) assert result.status_code == status.OK @pytest.mark.parametrize( 'headers, data', [ ( {}, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', 'feed_id': 'vendor_123', }, ), ( { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', }, {'feed_name': 'label_track_placement', 'feed_id': 'vendor_123'}, ), ( { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', }, {'user_feed_name': 'user_workstation_notifications', 'feed_id': 'vendor_123'}, ), ( {header.ORCHARD_USER_ID: 'alw:123'}, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', }, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_bad_parameters(neo4j_exit, neo4j_enter, fixture_client, headers, data): """Test subscribe response when called with bad parameters.""" url = '/subscribe' result = fixture_client.post(url, headers=headers, data=json.dumps(data)) assert result.status_code == status.BAD_REQUEST @pytest.mark.parametrize( 'headers, data', [ ( { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', }, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', 'feed_id': 'vendor_456', }, ), ( { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'subaccount', }, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', 'feed_id': 'vendor_123', }, ), ( { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'subaccount', }, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', 'feed_id': 'subaccount_456', }, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_subscribe_unauthorized(neo4j_exit, neo4j_enter, fixture_client, headers, data): """Test subscribe response when the user is unauthorized.""" url = '/subscribe' result = fixture_client.post(url, headers=headers, data=json.dumps(data)) assert result.status_code == status.UNAUTHORIZED @patch('notifications.logic.stream.unsubscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe(neo4j_exit, neo4j_enter, mock_unsubscribe, fixture_client): """Test unsubscribe response.""" url = '/unsubscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'accounting_statement', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_unsubscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) assert result.status_code == status.OK @patch( 'notifications.logic.stream.unsubscribe', return_value=response.Response({'old': 'subscription'}), ) @patch( 'notifications.logic.subscriptions.subscription_for_ws_user', return_value=response.Response({'new': 'subscription'}), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe_from_ws( neo4j_exit, neo4j_enter, mock_subscribe_ws, mock_unsubscribe, fixture_client ): """Test unsubscribe response.""" url = '/unsubscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_release_approval', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_unsubscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) mock_subscribe_ws.assert_called_once() assert result.status_code == status.OK @patch('notifications.logic.stream.unsubscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe_with_orchard_header(neo4j_exit, neo4j_enter, mock_unsubscribe, fixture_client): """Test unsubscribe response.""" url = '/unsubscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', 'Referer': 'https://workstation.qaorch.com', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'accounting_statement', 'feed_id': 'vendor_123', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_unsubscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], None, ) assert result.status_code == status.OK @patch('notifications.logic.stream.unsubscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe_without_feed_id(neo4j_exit, neo4j_enter, mock_unsubscribe, fixture_client): """Test unsubscribe response when called without a feed id.""" url = '/unsubscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_unsubscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], 'vendor_123', None, ) assert result.status_code == status.OK @patch('notifications.logic.stream.unsubscribe', return_value=response.Response()) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe_with_user_feed_id(neo4j_exit, neo4j_enter, mock_unsubscribe, fixture_client): """Test unsubscribe response when called with a user feed id.""" url = '/unsubscribe' headers = { header.ORCHARD_USER_ID: 'alw:123', header.GRASS_ACCOUNT_ID: 123, header.GRASS_ACCOUNT_TYPE: 'vendor', } data = { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'accounting_statement', 'feed_id': 'vendor_123', 'user_feed_id': 'john-doe@gmail.com', } result = fixture_client.post(url, headers=headers, data=json.dumps(data)) mock_unsubscribe.assert_called_with( data['user_feed_name'], headers[header.ORCHARD_USER_ID], data['feed_name'], data['feed_id'], data['user_feed_id'], ) assert result.status_code == status.OK @pytest.mark.parametrize( 'headers, data', [ ( {}, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', 'feed_id': 'vendor_123', }, ), ( {header.ORCHARD_USER_ID: 'alw:123'}, {'feed_name': 'label_track_placement', 'feed_id': 'vendor_123'}, ), ( {header.ORCHARD_USER_ID: 'alw:123'}, {'user_feed_name': 'user_workstation_notifications', 'feed_id': 'vendor_123'}, ), ( {header.ORCHARD_USER_ID: 'alw:123'}, { 'user_feed_name': 'user_workstation_notifications', 'feed_name': 'label_track_placement', }, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_unsubscribe_bad_parameters(neo4j_exit, neo4j_enter, fixture_client, headers, data): """Test unsubscribe response when called with bad parameters.""" url = '/unsubscribe' result = fixture_client.post(url, headers=headers, data=json.dumps(data)) assert result.status_code == status.BAD_REQUEST @patch('notifications.logic.stream.get_user_notifications', return_value=response.Response()) def test_get_user_notifications(mock_get_user_notifications, fixture_client): """Test get user notifications response.""" url = '/user/notifications' headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_notifications.assert_called_with( headers[header.ORCHARD_USER_ID], DEFAULT_USER_FEED, None ) assert result.status_code == status.OK @patch('notifications.logic.stream.get_user_notifications', return_value=response.Response()) def test_get_user_notifications_with_user_feed(mock_get_user_notifications, fixture_client): """Test get user notifications response when called with a user feed.""" user_feed_name = 'user_workstation_notifications' url = '/user/notifications?user_feed_name={0}'.format(user_feed_name) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_notifications.assert_called_with( headers[header.ORCHARD_USER_ID], user_feed_name, None ) assert result.status_code == status.OK @patch('notifications.logic.stream.get_user_notifications', return_value=response.Response()) def test_get_user_notifications_with_user_feed_id(mock_get_user_notifications, fixture_client): """Test get user notifications when called with a user feed id.""" user_feed_id = 'john-doe@gmail.com' url = '/user/notifications?user_feed_id={0}'.format(user_feed_id) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_notifications.assert_called_with( headers[header.ORCHARD_USER_ID], DEFAULT_USER_FEED, user_feed_id ) assert result.status_code == status.OK def test_get_notifications_bad_parameters(fixture_client): """Test get notifications response when called with bad parameters.""" url = '/user/notifications' headers = {} result = fixture_client.get(url, headers=headers) assert result.status_code == status.BAD_REQUEST def test_get_notifications_invalid_user_feed(fixture_client): """Test get notifications response when called with invalid user feed.""" user_feed_name = 'INVALID' url = '/user/notifications?user_feed_name={0}'.format(user_feed_name) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) assert result.status_code == status.BAD_REQUEST @patch('notifications.logic.stream.get_user_subscriptions', return_value=response.Response()) def test_get_user_subscriptions(mock_get_user_subscriptions, fixture_client): """Test get user subscriptions response.""" url = '/user/subscriptions' headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_subscriptions.assert_called_with( headers[header.ORCHARD_USER_ID], DEFAULT_USER_FEED, None ) assert result.status_code == status.OK @patch('notifications.logic.stream.get_user_subscriptions', return_value=response.Response()) def test_get_user_subscriptions_with_user_feed(mock_get_user_subscriptions, fixture_client): """Test get user subscriptions response when called with a user feed.""" user_feed_name = 'user_workstation_notifications' url = '/user/subscriptions?user_feed_name={0}'.format(user_feed_name) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_subscriptions.assert_called_with( headers[header.ORCHARD_USER_ID], user_feed_name, None ) assert result.status_code == status.OK @patch('notifications.logic.stream.get_user_subscriptions', return_value=response.Response()) def test_get_user_subscriptions_with_user_feed_id(mock_get_user_subscriptions, fixture_client): """Test get user subscriptions when called with a user feed id.""" user_feed_id = 'john-doe@gmail.com' url = '/user/subscriptions?user_feed_id={0}'.format(user_feed_id) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) mock_get_user_subscriptions.assert_called_with( headers[header.ORCHARD_USER_ID], DEFAULT_USER_FEED, user_feed_id ) assert result.status_code == status.OK def test_get_user_subscriptions_bad_parameters(fixture_client): """Test get subscriptions response when called with bad parameters.""" url = '/user/subscriptions' headers = {} result = fixture_client.get(url, headers=headers) assert result.status_code == status.BAD_REQUEST def test_get_user_subscriptions_invalid_user_feed(fixture_client): """Test get subscriptions response when called with invalid user feed.""" user_feed_name = 'INVALID' url = '/user/subscriptions?user_feed_name={0}'.format(user_feed_name) headers = {header.ORCHARD_USER_ID: 'alw:123'} result = fixture_client.get(url, headers=headers) assert result.status_code == status.BAD_REQUEST @patch('notifications.logic.stream.get_feed_subscribers', return_value=response.Response()) def test_get_feed_subscribers(mock_get_feed_subscribers, fixture_client): """Test get feed subscribers response.""" feed_name = 'label_analytics_digest' feed_id = 'vendor_123' url = '/feed/subscribers?feed_name={0}&feed_id={1}'.format(feed_name, feed_id) result = fixture_client.get(url) mock_get_feed_subscribers.assert_called_with(feed_name, feed_id) assert result.status_code == status.OK def test_get_feed_subscribers_bad_parameters(fixture_client): """Test get feed subscribers response when called with bad params.""" url = '/feed/subscribers' result = fixture_client.get(url) assert result.status_code == status.BAD_REQUEST @patch('notifications.handlers.g') def test_exception_handler(mock_g, app_context): """Verify exception_Handler returns 500 status code and json payload.""" message = 'The server encountered an internal error and was unable to complete your request.' mock_error = MagicMock() server_response = handlers.exception_handler(mock_error) mock_g.log.exception.assert_called_with(mock_error) # assert status code is 500 assert server_response.status_code == 500 # assert json payload response_message = json.loads(server_response.data.decode()) assert response_message['message'] == message assert response_message['code'] == response.error.ERROR_CODE_INTERNAL_ERROR @pytest.mark.parametrize( 'entity_type, body,\ entity_node_type, automatic, expected_code', [ ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED', }, 'GlobalParticipant', False, 200, ), ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED', 'automatic': True, }, 'GlobalParticipant', True, 200, ), ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED', 'automatic': False, }, 'GlobalParticipant', False, 200, ), ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED', 'automatic': 'false', }, None, None, 400, ), ( 'participant', {'id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED'}, 'GlobalParticipant', False, 200, ), ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'HAS_FOLLOWED', }, 'GlobalParticipant', False, 400, ), ( 'sound_recording', { 'sound_recording_id': '327bf269-57d9-487c-b510-df273aafc2d9', 'relationship': 'HAS_FOLLOWED', }, 'GlobalSoundRecording', False, 200, ), ('vendor', {'vendor_id': 12345, 'relationship': 'HAS_FOLLOWED'}, 'Vendor', False, 200), ('vendor', {'vendor_id': '12345', 'relationship': 'HAS_FOLLOWED'}, 'Vendor', False, 200), ('vendor', {'vendor_id': 'abcde', 'relationship': 'HAS_FOLLOWED'}, None, None, 400), ( 'sub_account', {'sub_account_id': 12345, 'relationship': 'HAS_FOLLOWED'}, 'Subaccount', False, 200, ), ( 'sub_account', {'sub_account_id': '12345', 'relationship': 'HAS_FOLLOWED'}, 'Subaccount', False, 200, ), ( 'sub_account', {'sub_account_id': 'abcde', 'relationship': 'HAS_FOLLOWED'}, None, None, 400, ), ( 'participant', {'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', 'relationship': 'FOLLOWED'}, None, None, 400, ), ( 'participant', { 'participant_id': '52874287-cbf0-420f-a873-6f5b8ff9d767', }, None, None, 400, ), ('participant', {'relationship': 'HAS_FOLLOWED'}, None, None, 400), ( 'channel', {'channel_id': 'UCabCdef1JKlmno', 'relationship': 'HAS_FOLLOWED'}, 'Channel', False, 200, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.create_subscription', return_value=response.Response()) @patch('notifications.handlers._swap_entity_id', return_value=None) def test_create_entity_subscription( swap_entity_id, logic, neo4j_exit, neo4j_enter, fixture_client, entity_type, body, entity_node_type, automatic, expected_code, ): """Test entity subscription create inputs to handler.""" url = f'/subscription/{entity_type}' profile_id = 555 profile_type = 'LabelProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, headers=headers, json=body) response_code = result.status_code assert response_code == expected_code if response_code == 400: response_body = json.loads(result.data.decode('utf-8')) assert 'code' in response_body assert 'message' in response_body assert not logic.called elif response_code == 200: # handle backwards compatible id types in body entity_id = body.get(f'{entity_type}_id') or body.get('id') # parse into int if possible for IDs that are integers try: entity_id = int(entity_id) except ValueError: pass assert neo4j_enter.called assert neo4j_exit.called assert logic.called logic.assert_called_with( profile_type, profile_id, entity_node_type, entity_id, body['relationship'], automatic ) @pytest.mark.parametrize( 'entity_type, entity_id, relationship,\ entity_node_type, expected_code', [ ( 'participant', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'HAS_FOLLOWED', 'GlobalParticipant', 200, ), ( 'sound_recording', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'HAS_FOLLOWED', 'GlobalSoundRecording', 200, ), ('participant', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'FOLLOW', None, 400), ('channel', 'UCabCdef1JKlmno', 'HAS_FOLLOWED', 'Channel', 200), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.has_subscription', return_value=response.Response()) @patch('notifications.handlers._swap_entity_id', return_value=None) def test_get_entity_subscription( swap_entity_id, logic, neo4j_exit, neo4j_enter, fixture_client, entity_type, entity_id, relationship, entity_node_type, expected_code, ): """Test entity subscription get inputs to handler.""" url = f'/subscription/{entity_type}/{entity_id}/relationship/{relationship}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.get(url, headers=headers) response_code = result.status_code assert response_code == expected_code if response_code == 400: response_body = json.loads(result.data.decode('utf-8')) assert 'code' in response_body assert 'message' in response_body assert not neo4j_enter.called assert not neo4j_exit.called assert not logic.called elif response_code == 200: assert neo4j_enter.called assert neo4j_exit.called assert logic.called logic.assert_called_with( profile_type, profile_id, entity_node_type, entity_id, relationship ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.get_subscriptions', return_value=response.Response()) def test_list_entity_subscriptions_params(logic, neo4j_exit, neo4j_enter, fixture_client): """Test args that have non-default options.""" params = 'offset=10&limit=5&state=all&order_dir=asc&ids=a,b,c&sub_type=video' url = f'/subscription/participant/relationship/HAS_FOLLOWED?{params}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.get(url, headers=headers) assert logic.call_args_list == [ call( 'InsightsProfile', 555, 'GlobalParticipant', 'HAS_FOLLOWED', { 'state': 'all', 'order_by': 'last_modified', 'offset': 10, 'limit': 5, 'ids': ['a', 'b', 'c'], 'order_dir': 'asc', 'sub_type': 'video', }, ) ] response_code = result.status_code assert response_code == 200 @pytest.mark.parametrize( 'entity_type, relationship,\ entity_node_type, expected_code', [ ('participant', 'HAS_FOLLOWED', 'GlobalParticipant', 200), ('sound_recording', 'HAS_FOLLOWED', 'GlobalSoundRecording', 200), ('vendor', 'HAS_FOLLOWED', 'Vendor', 200), ('sub_account', 'HAS_FOLLOWED', 'Subaccount', 200), ('participant', 'FOLLOW', None, 400), ('artist', 'HAS_FOLLOWED', None, 400), ('channel', 'HAS_FOLLOWED', 'Channel', 200), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.get_subscriptions', return_value=response.Response()) def test_list_entity_subscriptions_no_params( logic, neo4j_exit, neo4j_enter, fixture_client, entity_type, relationship, entity_node_type, expected_code, ): """Test participant subscription list inputs to handler.""" url = f'/subscription/{entity_type}/relationship/{relationship}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.get(url, headers=headers) response_code = result.status_code assert response_code == expected_code if response_code == 400: response_body = json.loads(result.data.decode('utf-8')) assert 'code' in response_body assert 'message' in response_body assert not neo4j_enter.called assert not neo4j_exit.called assert not logic.called elif response_code == 200: assert neo4j_enter.called assert neo4j_exit.called assert logic.called logic.assert_called_with( profile_type, profile_id, entity_node_type, relationship, { 'ids': [], 'offset': 0, 'limit': None, 'order_by': 'last_modified', 'order_dir': 'desc', 'state': 'undeleted', 'sub_type': None, }, ) @pytest.mark.parametrize( 'entity_type, entity_node_type, expected_ids', [ ('participant', 'GlobalParticipant', ['1', '2', '3']), ('vendor', 'Vendor', [1, 2, 3]), ('sub_account', 'Subaccount', [1, 2, 3]), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.resolve_node_ids') @patch('notifications.logic.subscriptions.get_subscriptions', return_value=response.Response()) def test_list_entity_subscriptions_id_parse( logic, resolve_ids, neo4j_exit, neo4j_enter, fixture_client, entity_type, entity_node_type, expected_ids, ): """Test ids param for entities that do not need ids to be resolved.""" url = f'/subscription/{entity_type}/relationship/HAS_FOLLOWED?ids=1,2,3' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} fixture_client.get(url, headers=headers) assert not resolve_ids.called logic.assert_called_with( profile_type, profile_id, entity_node_type, 'HAS_FOLLOWED', { 'ids': expected_ids, 'offset': 0, 'limit': None, 'order_by': 'last_modified', 'order_dir': 'desc', 'state': 'undeleted', 'sub_type': None, }, ) @pytest.mark.parametrize( 'entity_type, entity_node_type, expected_ids', [ ('sound_recording', 'GlobalSoundRecording', ['a', 'b', 'c']), ('channel', 'Channel', ['a', 'b', 'c']), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch('notifications.logic.subscriptions.resolve_node_ids', return_value=['a', 'b', 'c']) @patch('notifications.logic.subscriptions.get_subscriptions', return_value=response.Response()) def test_list_entity_subscriptions_id_resolve( logic, resolve_ids, neo4j_exit, neo4j_enter, fixture_client, entity_type, entity_node_type, expected_ids, ): """Test ids param for entities that do need ids to be resolved.""" url = f'/subscription/{entity_type}/relationship/HAS_FOLLOWED?ids=1,2,3' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} fixture_client.get(url, headers=headers) logic.assert_called_with( profile_type, profile_id, entity_node_type, 'HAS_FOLLOWED', { 'ids': expected_ids, 'offset': 0, 'limit': None, 'order_by': 'last_modified', 'order_dir': 'desc', 'state': 'undeleted', 'sub_type': None, }, ) @pytest.mark.parametrize('entity_type, input_ids', [('vendor', 'a,b,c'), ('sub_account', 'a,b,c')]) def test_list_entity_subscriptions_bad_int_types(fixture_client, entity_type, input_ids): """Test id type casting to int for entity types that need it.""" url = f'/subscription/{entity_type}/relationship/HAS_FOLLOWED?ids={input_ids}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} response = fixture_client.get(url, headers=headers) assert response.status_code == 400 assert json.loads(response.data.decode('utf-8')) == { 'code': 'validation_error', 'message': "ids param error: invalid literal for int() with base 10: 'a'", } def test_list_entity_subscriptions_max_id_len(fixture_client): """Test maximum number of ids in param.""" ids = ','.join([str(x) for x in range(0, 51)]) url = f'/subscription/participant/relationship/HAS_FOLLOWED?ids={ids}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} response = fixture_client.get(url, headers=headers) assert response.status_code == 400 assert json.loads(response.data.decode('utf-8')) == { 'code': 'validation_error', 'message': 'maximum 50 ids per request', } @pytest.mark.parametrize( 'entity_type, entity_id, relationship,\ entity_node_type, expected_code', [ ( 'participant', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'HAS_FOLLOWED', 'GlobalParticipant', 200, ), ( 'sound_recording', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'HAS_FOLLOWED', 'GlobalSoundRecording', 200, ), ('participant', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'FOLLOW', None, 400), ('artist', '52874287-cbf0-420f-a873-6f5b8ff9d767', 'HAS_FOLLOWED', None, 400), ('channel', 'UCabCdef1JKlmno', 'HAS_FOLLOWED', 'Channel', 200), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.soft_delete_subscription', return_value=response.Response() ) @patch('notifications.handlers._swap_entity_id', return_value=None) def test_delete_entity_subscription( swap_entity_id, logic, neo4j_exit, neo4j_enter, fixture_client, entity_type, entity_id, relationship, entity_node_type, expected_code, ): """Test entity subscription delete inputs to handler.""" url = f'/subscription/{entity_type}/{entity_id}/relationship/{relationship}' profile_id = 555 profile_type = 'InsightsProfile' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.delete(url, headers=headers) response_code = result.status_code assert response_code == expected_code if response_code == 400: response_body = json.loads(result.data.decode('utf-8')) assert 'code' in response_body assert 'message' in response_body assert not neo4j_enter.called assert not neo4j_exit.called assert not logic.called elif response_code == 200: assert neo4j_enter.called assert neo4j_exit.called assert logic.called logic.assert_called_with( profile_type, profile_id, entity_node_type, entity_id, relationship ) @pytest.mark.parametrize( 'input_str, expected_output', [ ('vendor', ['Vendor']), ('sub_account', ['Subaccount']), ('participant', ['GlobalParticipant']), ('sound_recording', ['GlobalSoundRecording']), ('label', ['Vendor', 'Subaccount']), (None, [None]), ], ) def test_get_followed_entities(input_str, expected_output): """Test translating followed entity to node type(s).""" result = handlers._get_followed_entities(input_str) assert result == expected_output @pytest.mark.parametrize( 'request_body, expected_code', [ ({'feed_type': 'social_spike', 'notification_type': 'push_notifications'}, 204), ( { 'feed_type': 'social_spike', 'notification_type': 'push_notifications', 'followed_entity': 'participant', }, 204, ), ( { 'feed_type': 'social_spike', 'notification_type': 'push_notifications', 'followed_entity': 'global_participant', }, 400, ), ({'feed_type': 'social_spike', 'notification_type': 'pidgeon_carrier'}, 400), ( { 'feed_type': 'social_spike', }, 400, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.unsubscribe_by_notification_type', return_value=response.Response(status=204), ) def test_unsubscribe_by_notification_type( mock_unsubscribe, neo4j_exit, neo4j_enter, fixture_client, request_body, expected_code ): """Test unsubscribe_by_notification_type response.""" url = '/notifications/unsubscribe' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == expected_code if result.status_code == 204: entity_type = 'GlobalParticipant' if 'followed_entity' in request_body else None mock_unsubscribe.assert_called_with( profile_type, int(profile_id), request_body['notification_type'], request_body['feed_type'], entity_type, ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.unsubscribe_by_notification_type', return_value=response.Response(status=201), ) def test_unsubscribe_by_label(mock_unsubscribe, neo4j_exit, neo4j_enter, fixture_client): """Test special unsubscribe by label case.""" url = '/notifications/unsubscribe' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} request_body = { 'feed_type': 'trending_tracks', 'notification_type': 'push_notifications', 'followed_entity': 'label', } result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == 201 assert mock_unsubscribe.call_args_list == [ call('LabelProfile', 12345, 'push_notifications', 'trending_tracks', 'Vendor'), call('LabelProfile', 12345, 'push_notifications', 'trending_tracks', 'Subaccount'), ] @pytest.mark.parametrize( ('app_id', 'app_ids', 'notification_type', 'expected'), [ ( 'workstation', [], None, ['workstation'], ), ( None, 'workstation,insights', None, ['workstation', 'insights'], ), ( None, [], 'emailNotification', [], ), ( None, 'workstation', 'emailNotification', ['workstation'], ), ('workstation', 'insights', None, ['insights', 'workstation']), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.get_all_subscriptions', return_value=response.Response(status=200), ) def test_get_all_subscriptions( mock_logic, neo4j_exit, neo4j_enter, fixture_client, app_id, app_ids, notification_type, expected, ): """Test get_all_subscriptions.""" url = '/subscriptions/all' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} query_string = {'app_id': app_id, 'app_ids': app_ids, 'notification_type': notification_type} result = fixture_client.get(url, headers=headers, query_string=query_string) assert result.status_code == 200 mock_logic.assert_called_with(expected, notification_type) @pytest.mark.parametrize( 'identity_id, email, full_name, verification_url, brand, expected_status', ( # all good ( header.ORCHARD_SYST_IDENTITY_ID, 'test@example.com', 'test_user', 'https://example.com', 'awal', 202, ), # bad identity_id ('BAD_IDENTITY_ID', 'test@example.com', 'test_user', 'https://example.com', 'awal', 403), # bad email (header.ORCHARD_SYST_IDENTITY_ID, '', 'test_user', 'https://example.com', 'awal', 400), # bad full_name ( header.ORCHARD_SYST_IDENTITY_ID, 'test@example.com', '', 'https://example.com', 'awal', 400, ), # bad verification_url (header.ORCHARD_SYST_IDENTITY_ID, 'test@example.com', 'test_user', '', 'awal', 400), # bad brand ( header.ORCHARD_SYST_IDENTITY_ID, 'test@example.com', 'test_user', 'https://example.com', 'not_awal', 400, ), ), ) def test_verify_identity( fixture_client, mocker, identity_id, email, full_name, verification_url, brand, expected_status ): """Test get_all_subscriptions_for_identity.""" url = '/identity/verify' request_body = { 'email': email, 'full_name': full_name, 'verification_url': verification_url, 'brand': brand, } logic_mock = mocker.patch( 'notifications.logic.identity_verification.verify', return_value=response.Response(status=expected_status), ) result = fixture_client.post( url, headers={header.ORCHARD_IDENTITY_ID: identity_id}, json=request_body ) assert result.status_code == expected_status if expected_status == 202: logic_mock.assert_called_with( email=email, full_name=full_name, verification_url=verification_url, brand=brand ) else: logic_mock.assert_not_called() @pytest.mark.parametrize( ('app_id', 'app_ids', 'profile_types', 'expected'), [ (None, None, [], []), (None, 'workstation', [], ['workstation']), ('collaborators', 'workstation,insights', [], ['workstation', 'insights', 'collaborators']), (None, None, ['LabelProfile'], []), ('workstation', None, ['LabelProfile'], ['workstation']), (None, 'workstation', ['LabelProfile'], ['workstation']), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.get_all_subscriptions_for_identity', return_value=response.Response(status=200), ) @patch('notifications.models.identity.can_administer_profile', return_value=True) @patch( 'notifications.models.subscriptions.get_subscription_by_param', return_value=response.Response({'appId': 'collaborators'}), ) def test_get_all_subscriptions_for_identity( get_subscription, can_administer_profile, mock_logic, neo4j_exit, neo4j_enter, fixture_client, app_id, app_ids, profile_types, expected, ): """Test get_all_subscriptions_for_identity.""" identity_id = 'uuid' url = f'/identity/{identity_id}/subscriptions' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} query_string = {'app_id': app_id, 'app_ids': app_ids, 'profile_types': profile_types} result = fixture_client.get(url, headers=headers, query_string=query_string) assert result.status_code == 200 mock_logic.assert_called_with(identity_id, profile_types, expected) @pytest.mark.parametrize( ('data', 'expected'), [ ({'follow_all_resources': True, 'follow_resources': []}, 200), ( { 'follow_all_resources': False, }, 200, ), # validation errors ({}, 400), ( { 'follow_all_resources': 'foo', }, 400, ), ({'follow_all_resources': True, 'follow_resources': 'foo'}, 400), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.edit_subscription_for_identity', return_value=response.Response(status=200), ) @patch('notifications.models.identity.can_administer_profile', return_value=True) @patch( 'notifications.models.subscriptions.get_subscription_by_param', return_value=response.Response({'appId': 'collaborators'}), ) def test_edit_subscription_for_identity( get_subscription, can_administer_profile, mock_logic, neo4j_exit, neo4j_enter, fixture_client, data, expected, ): """Test edit_subscription_for_identity.""" identity_id = 'uuid' subscription_name = 'approval' url = f'/identity/{identity_id}/subscriptions/{subscription_name}' headers = {header.ORCHARD_PROFILE_ID: 'LabelProfile', header.ORCHARD_PROFILE_TYPE: '12345'} result = fixture_client.put(url, headers=headers, json=data) assert result.status_code == expected if expected == 200: mock_logic.assert_called_with( identity_id, subscription_name, data.get('follow_all_resources'), data.get('follow_resources'), False, False, ) @pytest.mark.parametrize( 'headers, profile_id, response_code', [ ({}, '123', 200), ({'Orchard-Profile-Type': 'LabelProfile', 'Orchard-Profile-Id': '123'}, '123', 200), ({'Orchard-Profile-Type': 'LabelProfile', 'Orchard-Profile-Id': '123'}, '456', 401), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.get_all_notifications_for_profile', return_value=[ { 'notification_type': 'email', 'feed_type': 'social_spike', 'followed_entity': 'GlobalParticipant', } ], ) @patch('notifications.logic.subscriptions.get_all_notifications_for_profile_gs') def test_get_all_notifications( mock_notifications_for_profile_gs, mock_notifications_for_profile, neo4j_exit, neo4j_enter, fixture_client, headers, profile_id, response_code, ): """Test get_all_notifications_for_profile response.""" profile_type = 'LabelProfile' url = f'/profile/profile_id/{profile_id}/profile_type/{profile_type}/notifications' result = fixture_client.get(url, headers=headers) assert result.status_code == response_code if response_code == 200: mock_notifications_for_profile.assert_called_with(profile_type, int(profile_id)) mock_notifications_for_profile_gs.assert_called_with(profile_type, int(profile_id)) @pytest.mark.parametrize( 'request_body, url_params, expected_undelete, expected_code', [ ({'feed_type': 'social_spike', 'notification_type': 'push_notifications'}, None, True, 201), ( { 'feed_type': 'social_spike', 'notification_type': 'push_notifications', 'followed_entity': 'participant', }, None, True, 201, ), ( {'feed_type': 'social_spike', 'notification_type': 'push_notifications'}, {'undelete': 'false'}, False, 201, ), ( {'feed_type': 'social_spike', 'notification_type': 'push_notifications'}, {'undelete': 'true'}, True, 201, ), ( {'feed_type': 'social_spike', 'notification_type': 'push_notifications'}, {'undelete': 'blah'}, None, 400, ), ({'feed_type': 'social_spike', 'notification_type': 'pidgeon_carrier'}, None, None, 400), ( { 'feed_type': 'social_spike', 'notification_type': 'push_notifications', 'followed_entity': 'global_participant', }, None, None, 400, ), ( { 'feed_type': 'social_spike', }, None, None, 400, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.subscribe_by_notification_type', return_value=response.Response(status=201), ) def test_subscribe_by_notification_type( mock_subscribe, neo4j_exit, neo4j_enter, fixture_client, request_body, url_params, expected_undelete, expected_code, ): """Test subscribe_by_notification_type response.""" url = '/notifications/subscribe' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, json=request_body, headers=headers, query_string=url_params) assert result.status_code == expected_code if result.status_code == 201: entity_type = 'GlobalParticipant' if 'followed_entity' in request_body else None mock_subscribe.assert_called_with( profile_type, int(profile_id), request_body['notification_type'], request_body['feed_type'], entity_type, expected_undelete, ) else: assert not mock_subscribe.called @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.subscriptions.subscribe_by_notification_type', return_value=response.Response(status=201), ) def test_subscribe_by_label(mock_subscribe, neo4j_exit, neo4j_enter, fixture_client): """Test special subscribe by label case.""" url = '/notifications/subscribe' profile_type = 'LabelProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} request_body = { 'feed_type': 'trending_tracks', 'notification_type': 'push_notifications', 'followed_entity': 'label', } result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == 201 assert mock_subscribe.call_args_list == [ call('LabelProfile', 12345, 'push_notifications', 'trending_tracks', 'Vendor', True), call('LabelProfile', 12345, 'push_notifications', 'trending_tracks', 'Subaccount', True), ] @pytest.mark.parametrize( 'payload, response_code', [ ( { 'application': {'name': 'orchard-go', 'version': '2.0.1'}, 'message': 'Cool cool cool', 'category': 'DATA_INACCURATE', 'participant': { 'id': '700cb3c2-fe8c-42eb-b1e0-dd376be6c15c', 'name': 'Phoebe Bridgers', 'chartmetric_id': '122237', }, 'referrer': 'PARTICIPANT_SCREEN', }, 202, ), ({'bad': 'payload'}, 400), ], ) @patch( 'notifications.logic.email.report_participant_data_issue', return_value=response.Response(status=202), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_report_participant_data( neo4j_exit, neo4j_enter, mock_report_participant_data_issue, fixture_client, payload, response_code, ): """Test report_participant_data_issue responses.""" identity_id = '03e83149-3c7f-4587-940b-ce83dec34bc8' result = fixture_client.post( f'/identity/{identity_id}/report/participant-data', data=json.dumps(payload), headers={'Content-Type': 'application/json'}, ) assert result.status_code == response_code @pytest.mark.parametrize( 'payload, response_code', [ ( { 'application': {'name': 'orchard-go', 'version': '2.0.1'}, 'message': 'Cool cool cool', 'category': 'DATA_INACCURATE', 'artist_name': 'Phoebe Bridgers', 'sound_recording': {'isrc': 'USJ5G2020003', 'name': 'Kyoto'}, 'referrer': 'PARTICIPANT_SCREEN', }, 202, ), ({'bad': 'payload'}, 400), ], ) @patch( 'notifications.logic.email.report_sound_recording_data_issue', return_value=response.Response(status=202), ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') def test_report_sound_recording_data( neo4j_exit, neo4j_enter, mock_report_sound_recording_data, fixture_client, payload, response_code, ): """Test report_sound_recording_data_issue responses.""" identity_id = '03e83149-3c7f-4587-940b-ce83dec34bc8' result = fixture_client.post( f'/identity/{identity_id}/report/sound-recording-data', data=json.dumps(payload), headers={'Content-Type': 'application/json'}, ) assert result.status_code == response_code @patch('notifications.logic.subscriptions.resolve_node_ids', return_value=['xyz']) def test_swap_entity_id(mock_logic): """Test id being overwritten.""" config = { 'entity_node_type': 'GlobalSoundRecording', 'entity_node_id_name': 'isrc', 'entity_id': 'abc', } result = handlers._swap_entity_id(config) assert mock_logic.called assert config['entity_id'] == 'xyz' assert result is None @patch('notifications.logic.subscriptions.resolve_node_ids', return_value=[]) def test_swap_entity_id_error(mock_logic): """Test node not being found.""" config = { 'entity_node_type': 'GlobalSoundRecording', 'entity_node_id_name': 'isrc', 'entity_id': 'abc', } result = handlers._swap_entity_id(config) assert mock_logic.called assert config['entity_id'] == 'abc' assert result is not None assert result.status_code == 404 @patch('notifications.logic.subscriptions.resolve_node_ids', return_value=['xyz', 'abc']) def test_swap_entity_id_dupe_error(mock_logic): """Test node not being found.""" config = { 'entity_node_type': 'GlobalSoundRecording', 'entity_node_id_name': 'isrc', 'entity_id': 'abc', } result = handlers._swap_entity_id(config) assert mock_logic.called assert config['entity_id'] == 'abc' assert result is not None assert result.status_code == 409 @patch('notifications.logic.subscriptions.resolve_node_ids', return_value=[]) def test_swap_entity_id_default(mock_logic): """Test id not being overwritten due to default id name.""" config = { 'entity_node_type': 'GlobalSoundRecording', 'entity_node_id_name': 'id', 'entity_id': 'abc', } result = handlers._swap_entity_id(config) assert not mock_logic.called assert config['entity_id'] == 'abc' assert result is None @pytest.mark.parametrize( 'request_body, expected_code', [ # sucessful request ( { 'date': '2019-06-15', 'network': 'twitter', 'new_followers': 100, 'chartmetric_id': 12345, }, 204, ), # missing date ({'network': 'twitter', 'new_followers': 100, 'chartmetric_id': 12345}, 400), # missing followers ({'date': '2019-06-15', 'network': 'twitter', 'chartmetric_id': 12345}, 400), # missing chartmetric id ( { 'date': '2019-06-15', 'network': 'twitter', 'new_followers': 100, }, 400, ), # bad social network ( { 'date': '2019-06-15', 'network': 'spotify', 'new_followers': 100, 'chartmetric_id': 12345, }, 400, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.stream.add_social_spike_activity', return_value=response.Response(status=204), ) def test_add_social_spike( mock_add_activity, neo4j_exit, neo4j_enter, fixture_client, request_body, expected_code ): """Test add participant social spike.""" url = '/activity/social_spike' profile_type = 'InsightsProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == expected_code if expected_code == 204: assert mock_add_activity.call_args_list == [ call( datetime.datetime.strptime(request_body['date'], '%Y-%m-%d'), request_body['network'], request_body['new_followers'], request_body['chartmetric_id'], ) ] assert neo4j_enter.called assert neo4j_exit.called else: assert not mock_add_activity.called assert not neo4j_enter.called assert not neo4j_exit.called @pytest.mark.parametrize( 'request_body, expected_code', [ # sucessful request with subaccount ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': { 'isrc': '12345', 'tracks': [{'id': 123, 'vendor_id': 987, 'subaccount_id': 654}], }, }, 204, ), # sucessful request without subaccount ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': { 'isrc': '12345', 'tracks': [{'id': 123, 'vendor_id': 987, 'subaccount_id': None}], }, }, 204, ), # vendor_id string ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': { 'isrc': '12345', 'tracks': [{'id': 123, 'vendor_id': '987', 'subaccount_id': 654}], }, }, 400, ), # subaccount_id string ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': { 'isrc': '12345', 'tracks': [{'id': 123, 'vendor_id': 987, 'subaccount_id': '654'}], }, }, 400, ), # playlist empty ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': {}, 'sound_recording': { 'isrc': '12345', 'tracks': [{'id': 123, 'vendor_id': 987, 'subaccount_id': 654}], }, }, 400, ), # sound recording empty ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': {}, }, 400, ), # subaccount id missing ( { 'timestamp': '2019-06-15 12:30:05', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': {'isrc': '12345', 'vendor_id': 987}, }, 400, ), # date instead of datetime ( { 'timestamp': '2019-06-15', 'playlist': { 'dsp': 'spotify', 'rank': 1, 'id': 'xyz', 'name': 'Best Songs of 2020', }, 'sound_recording': {'isrc': '12345', 'vendor_id': 987, 'subaccount_id': 654}, }, 400, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.stream.add_playlist_placement_activity', return_value=response.Response(status=204), ) def test_add_playlist_placement( mock_add_activity, neo4j_exit, neo4j_enter, fixture_client, request_body, expected_code ): """Test add playlist placement.""" url = '/activity/playlist_placement' profile_type = 'InsightsProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == expected_code if expected_code == 204: assert mock_add_activity.call_args_list == [ call( datetime.datetime.strptime(request_body['timestamp'], '%Y-%m-%d %H:%M:%S'), request_body['playlist'], request_body['sound_recording'], ) ] assert neo4j_enter.called assert neo4j_exit.called else: assert not neo4j_enter.called assert not neo4j_exit.called @pytest.mark.parametrize( 'request_body, expected_code', [ # sucessful request no subaccount id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': None}, }, 204, ), # sucessful request with subaccount id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': 9012}, }, 204, ), # string subaccount id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': '9012'}, }, 400, ), # missing subaccount id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678}, }, 400, ), # string track id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': '1234', 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': 9012}, }, 400, ), # string label id ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'spotify', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': '5678', 'subaccount_id': 9012}, }, 400, ), # bad dsp ( { 'date': '2019-06-15', 'region': 'USA', 'dsp': 'amazon', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': 9012}, }, 400, ), # timestamp not date ( { 'date': '2019-06-15 12:00:00', 'region': 'USA', 'dsp': 'amazon', 'percent_diff': 100, 'day_streams': 1000, 'track': {'id': 1234, 'isrc': 'xyz', 'vendor_id': 5678, 'subaccount_id': 9012}, }, 400, ), ], ) @patch('notifications.handlers.Neo4jSession.__enter__') @patch('notifications.handlers.Neo4jSession.__exit__') @patch( 'notifications.logic.stream.add_trending_track_activity', return_value=response.Response(status=204), ) def test_add_trending_track( mock_add_activity, neo4j_exit, neo4j_enter, fixture_client, request_body, expected_code ): """Test add sound recording trending track.""" url = '/activity/trending_track' profile_type = 'InsightsProfile' profile_id = '12345' headers = {header.ORCHARD_PROFILE_ID: profile_id, header.ORCHARD_PROFILE_TYPE: profile_type} result = fixture_client.post(url, json=request_body, headers=headers) assert result.status_code == expected_code if expected_code == 204: assert mock_add_activity.call_args_list == [ call( datetime.datetime.strptime(request_body['date'], '%Y-%m-%d'), request_body['dsp'], request_body['region'], request_body['percent_diff'], request_body['day_streams'], request_body['track'], ) ] else: assert not mock_add_activity.called assert not neo4j_enter.called assert not neo4j_exit.called @patch( 'notifications.logic.audience_email.shopify_store_sync_completed', return_value=response.Response(status=204), ) def test_audience_shopify_store_sync_completed(audience_email_mock, fixture_client): """Test audience_shopify_store_sync_completed endpoint.""" url = '/audience/shopify-store-sync-completed' identity_id = str(uuid.uuid4()) store_id = str(uuid.uuid4()) store_domain = 'mock-store.myshopify.com' result = fixture_client.post( url, json={ 'identity_id': identity_id, 'store_id': store_id, 'store_domain': store_domain, 'is_multiartist_store': True, }, ) assert audience_email_mock.called assert result.status_code == 204 @patch( 'notifications.logic.audience_email.audience_file_exported', return_value=response.Response(status=204), ) def test_audience_audience_file_exported(audience_email_mock, fixture_client): """Test POST /audience/audience-file-exported endpoint.""" url = '/audience/audience-file-exported' identity_id = str(uuid.uuid4()) audience_name = 'test audience' filename = 'zip/test.zip' password = 'secret' result = fixture_client.post( url, json={ 'identity_id': identity_id, 'audience_name': audience_name, 'filename': filename, 'password': password, }, ) assert audience_email_mock.called assert result.status_code == 204 @pytest.mark.parametrize( 'platform', [ ('META'), ('TIKTOK'), ('GOOGLE'), ], ) @patch( 'notifications.logic.audience_email.ad_reporting_data_sync_completed', return_value=response.Response(status=204), ) def test_audience_email_correct_ad_reporting_platform( audience_email_mock, fixture_client, platform, ): """Test audience_shopify_store_sync_completed endpoint.""" url = '/audience/ad-reporting-sync-completed' identity_id = str(uuid.uuid4()) ad_accounts = [str(uuid.uuid4())] result = fixture_client.post( url, json={ 'identity_id': identity_id, 'ad_reporting_platform': platform, 'ad_accounts': ad_accounts, }, ) assert audience_email_mock.called assert result.status_code == 204 @patch( 'notifications.logic.distribution_email.scheduled_update_processed', return_value=response.Response(status=202), ) def test_scheduled_update_processed(distribution_email_mock, fixture_client): """Test POST /distribution/scheduled_update_processed endpoint.""" url = '/distribution/scheduled_update_processed' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'product': {'id': 2958087, 'name': 'Vanishing Point', 'upc': '195497264421'}, 'recipients': ['bburton@theorchard.com', 'lseal@theorchard.com'], 'delivery_stores': [{'id': 1, 'name': 'iTunes/Apple'}, {'id': 286, 'name': 'Spotify'}], 'processed_at': '2023-05-09T00:00Z', 'update': {'sale_start_date': '2023-05-20'}, }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert distribution_email_mock.called assert result.status_code == 202 @patch( 'notifications.logic.distribution_email.scheduled_update_failed', return_value=response.Response(status=202), ) def test_scheduled_update_failed(distribution_email_mock, fixture_client): """Test POST /distribution/scheduled_update_failed endpoint.""" url = '/distribution/scheduled_update_failed' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'product': {'id': 2958087, 'name': 'Vanishing Point', 'upc': '195497264421'}, 'recipients': ['bburton@theorchard.com', 'lseal@theorchard.com'], 'delivery_stores': [{'id': 286, 'name': 'Spotify'}, {'id': 1, 'name': 'iTunes/Apple'}], 'update': {'sale_start_date': '2023-05-20'}, }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert distribution_email_mock.called assert result.status_code == 202 @patch( 'notifications.logic.distribution_email.scheduled_update_warning', return_value=response.Response(status=202), ) def test_scheduled_update_warning(distribution_email_mock, fixture_client): """Test POST /distribution/scheduled_update_warning endpoint.""" url = '/distribution/scheduled_update_warning' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'product': {'id': 2958087, 'name': 'Vanishing Point', 'upc': '195497264421'}, 'recipients': ['bburton@theorchard.com', 'lseal@theorchard.com'], 'process_at': '2023-05-20T12:00:01Z', }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert distribution_email_mock.called assert result.status_code == 202 @pytest.mark.parametrize( 'payload, response_code, expected_call_report_product_review_failure', [ ( { 'identity_id': str(uuid.uuid4()), 'upc': '697691883793', 'product_name': 'some release', }, 202, True, ), ({'bad': 'payload'}, 400, False), ], ) @patch( 'notifications.logic.content_review_email.report_product_review_failure', return_value=response.Response(status=202), ) def test_content_review_failure_notify( content_review_email_mock, fixture_client, payload, response_code, expected_call_report_product_review_failure, ): """Test POST /content-review/failure-notify endpoint.""" result = fixture_client.post( '/content-review/failure-notify', data=json.dumps(payload), headers={'Content-Type': 'application/json'}, ) assert content_review_email_mock.called == expected_call_report_product_review_failure assert result.status_code == response_code @pytest.mark.parametrize( 'payload, response_code, expected_call', [ ( { 'identity_id': str(uuid.uuid4()), 'upc': '697691883793', 'resolution': 'approve', 'notes': 'Approved after legal review.', 'escalation_type': 'Carve-outs/Exclusives', 'escalation_note': 'Product needs special handling.', }, 202, True, ), ( { 'identity_id': str(uuid.uuid4()), 'upc': '697691883793', 'resolution': 'reject', 'notes': None, 'escalation_type': None, 'escalation_note': None, }, 202, True, ), ( { 'identity_id': str(uuid.uuid4()), 'upc': '697691883793', 'resolution': 'approve', }, 202, True, ), ({'bad': 'payload'}, 400, False), ], ) @patch( 'notifications.logic.content_review_email.review_escalation_completed', return_value=response.Response(status=202), ) def test_content_review_notify_escalation_complete( content_review_email_mock, fixture_client, payload, response_code, expected_call, ): """Test POST /content-review/notify-escalation-complete endpoint.""" result = fixture_client.post( '/content-review/notify-escalation-complete', data=json.dumps(payload), headers={'Content-Type': 'application/json'}, ) assert content_review_email_mock.called == expected_call assert result.status_code == response_code if expected_call: content_review_email_mock.assert_called_once_with( upc=payload.get('upc'), identity_id=payload.get('identity_id'), resolution=payload.get('resolution'), notes=payload.get('notes'), escalation_type=payload.get('escalation_type'), escalation_note=payload.get('escalation_note'), ) @patch( 'notifications.logic.distribution_email.ws_scheduled_update_failed', return_value=response.Response(status=202), ) def test_ws_scheduled_update_failed(distribution_email_mock, fixture_client): """Test POST /distribution/ws_scheduled_update_failed endpoint.""" url = '/distribution/ws_scheduled_update_failed' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'product': {'id': 2958087, 'name': 'Vanishing Point', 'upc': '195497264421'}, 'recipients': ['bburton@theorchard.com', 'lseal@theorchard.com'], }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert distribution_email_mock.called assert result.status_code == 202 @patch( 'notifications.logic.distribution_email.ws_scheduled_update_processed', return_value=response.Response(status=202), ) def test_ws_scheduled_update_processed(distribution_email_mock, fixture_client): """Test POST /distribution/ws_scheduled_update_processed endpoint.""" url = '/distribution/ws_scheduled_update_processed' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'product': {'id': 2958087, 'name': 'Vanishing Point', 'upc': '195497264421'}, 'recipients': ['bburton@theorchard.com', 'lseal@theorchard.com'], }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert distribution_email_mock.called assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_started') def test_bulk_begin(bulk_ingestion_slack_mock, fixture_client): """Test POST /bulk/ingest/begin endpoint.""" url = '/bulk/ingest/begin' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 'vendor_id': 12345, 'vendor_name': 'Kitty Wizard Records, LLC', 'total_products': 10, } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 200 @patch( 'notifications.logic.bulk_ingestion_slack.bulk_ingest_started', side_effect=Exception('Slack API error'), ) def test_bulk_begin_slack_failure(bulk_ingestion_slack_mock, fixture_client): """Test POST /bulk/ingest/begin endpoint handles Slack failure gracefully.""" url = '/bulk/ingest/begin' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 'vendor_id': 12345, 'vendor_name': 'Kitty Wizard Records, LLC', 'total_products': 10, } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 200 @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_succeeded', return_value=response.Response(status=202), ) def test_bulk_success(bulk_ingestion_email_mock, fixture_client): """Test POST /bulk/ingest/success endpoint.""" url = '/bulk/ingest/success' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert bulk_ingestion_email_mock.called assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_succeeded') @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_succeeded', return_value=response.Response(status=202), ) def test_bulk_success_with_slack( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/success endpoint calls Slack when execution_arn is present.""" url = '/bulk/ingest/success' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_id': 32387, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 202 @patch( 'notifications.logic.bulk_ingestion_slack.bulk_ingest_succeeded', side_effect=Exception('Slack API error'), ) @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_succeeded', return_value=response.Response(status=202), ) def test_bulk_success_slack_failure( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/success endpoint handles Slack failure gracefully.""" url = '/bulk/ingest/success' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_id': 32387, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_succeeded') @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_succeeded', return_value=response.Response(status=202), ) def test_bulk_success_without_execution_arn_no_slack( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/success endpoint does not call Slack without execution_arn.""" url = '/bulk/ingest/success' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_not_called() assert result.status_code == 202 @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_failed', return_value=response.Response(status=202), ) def test_bulk_failure(bulk_ingestion_email_mock, fixture_client): """Test POST /bulk/ingest/failure endpoint.""" url = '/bulk/ingest/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'ingestion_report_link': 'https://super-cool.com/report.pdf', 'submissions_failed': False, }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert bulk_ingestion_email_mock.called assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_failed') @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_failed', return_value=response.Response(status=202), ) def test_bulk_failure_with_slack( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/failure endpoint calls Slack when execution_arn is present.""" url = '/bulk/ingest/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_id': 32387, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'ingestion_report_link': 'https://super-cool.com/report.pdf', 'submissions_failed': False, 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 202 @patch( 'notifications.logic.bulk_ingestion_slack.bulk_ingest_failed', side_effect=Exception('Slack API error'), ) @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_failed', return_value=response.Response(status=202), ) def test_bulk_failure_slack_failure( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/failure endpoint handles Slack failure gracefully.""" url = '/bulk/ingest/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_id': 32387, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'ingestion_report_link': 'https://super-cool.com/report.pdf', 'submissions_failed': False, 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_failed') @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_failed', return_value=response.Response(status=202), ) def test_bulk_failure_without_execution_arn_no_slack( bulk_ingestion_email_mock, bulk_ingestion_slack_mock, fixture_client ): """Test POST /bulk/ingest/failure endpoint does not call Slack without execution_arn.""" url = '/bulk/ingest/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'ingestion_report_link': 'https://super-cool.com/report.pdf', 'submissions_failed': False, } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) assert bulk_ingestion_email_mock.called bulk_ingestion_slack_mock.assert_not_called() assert result.status_code == 202 @patch( 'notifications.logic.bulk_ingestion_email.bulk_ingest_failed', return_value=response.Response(status=202), ) def test_bulk_failure_without_optional_fields(bulk_ingestion_email_mock, fixture_client): """Test POST /bulk/ingest/failure endpoint without optional fields.""" url = '/bulk/ingest/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID result = fixture_client.post( url, json={ 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'identity_id': identity_id, 'identity_name': 'Ben', 'total_products': 10, 'submitted_products_count': 5, 'vendor_name': 'Kitty Wizard Records, LLC', 'completed_on': '2023-10-01T13:00:00Z', 'metadata_file_link': 'https://super-cool.com/file.pdf', 'submit_products': True, 'assets_required': False, 'ingestion_report_link': None, 'submissions_failed': False, }, headers={header.ORCHARD_IDENTITY_ID: identity_id}, ) assert bulk_ingestion_email_mock.called assert result.status_code == 202 @patch('notifications.logic.bulk_ingestion_slack.bulk_ingest_product_failed') def test_bulk_product_failure(bulk_ingestion_slack_mock, fixture_client): """Test POST /bulk/ingest/product/failure endpoint.""" url = '/bulk/ingest/product/failure' identity_id = header.ORCHARD_SYST_IDENTITY_ID payload = { 'bulk_session_id': '113773f6-853c-4c83-a24b-b2a5a48fa194', 'execution_arn': 'arn:aws:states:us-east-1:123456789012:execution:my-state-machine:execution-id', # noqa: E501 'product_code': 'PROD-12345', } result = fixture_client.post( url, json=payload, headers={header.ORCHARD_IDENTITY_ID: identity_id} ) bulk_ingestion_slack_mock.assert_called_once_with(payload) assert result.status_code == 200