"""Tests for Handlers.""" import json from unittest.mock import MagicMock, patch import pytest from owsrequest import error_response from owsresponse import response from werkzeug.exceptions import InternalServerError from permissions import api from permissions.constants import error from permissions.constants.constants import DEFAULT_LIMIT from permissions.logic import profile @patch('permissions.api.tracer') @patch('permissions.api.g') def test_exception_handler(mock_g, mock_tracer, 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 = Exception('test error') server_response = api.exception_handler(mock_error) mock_g.log.exception.assert_called_with(mock_error) mock_tracer.current_root_span.return_value.set_exc_info.assert_called_once_with( type(mock_error), mock_error, mock_error.__traceback__ ) # 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 @patch('permissions.api.tracer') @patch('permissions.api.g') def test_exception_handler_uses_original_exception(mock_g, mock_tracer, app_context): """Verify exception_Handler reports the original exception for 500 wrappers.""" original_exception = TypeError('bad call signature') wrapped_exception = InternalServerError() wrapped_exception.original_exception = original_exception api.exception_handler(wrapped_exception) mock_g.log.exception.assert_called_with(original_exception) mock_tracer.current_root_span.return_value.set_exc_info.assert_called_once_with( type(original_exception), original_exception, original_exception.__traceback__, ) @patch('permissions.api.tracer') def test_set_span_error_from_response_sets_tags(mock_tracer): """Verify span error tags are set from direct 500 responses.""" root_span = MagicMock() root_span.get_tag.return_value = None mock_tracer.current_root_span.return_value = root_span server_response = api.app.response_class( response=json.dumps( { 'code': response.error.ERROR_CODE_INTERNAL_ERROR, 'message': 'Failed to delete relationship. Please check if relationship exists.', } ), status=500, mimetype='application/json', ) returned_response = api.set_span_error_from_response(server_response) assert returned_response is server_response root_span.set_tag.assert_any_call( 'error.message', 'Failed to delete relationship. Please check if relationship exists.', ) root_span.set_tag.assert_any_call('error.type', response.error.ERROR_CODE_INTERNAL_ERROR) @patch('permissions.api.tracer') def test_set_span_error_from_response_does_not_override_existing_message(mock_tracer): """Verify span error tags are not overwritten when error.message already exists.""" root_span = MagicMock() root_span.get_tag.return_value = 'existing error message' mock_tracer.current_root_span.return_value = root_span server_response = MagicMock() server_response.status_code = 500 returned_response = api.set_span_error_from_response(server_response) assert returned_response is server_response server_response.get_json.assert_not_called() root_span.set_tag.assert_not_called() @pytest.mark.parametrize( 'logic_response', [ response.Response(message={'foo': 'bar'}), response.create_not_found_response('Resource not found.'), response.create_fatal_response('Invalid Resource type'), ], ) def test_get_resources(logic_response, mocker, fixture_client): """Verify get_resources.""" mocker.patch('permissions.logic.resource.get_resources', return_value=logic_response) handler_response = fixture_client.get('/e2e/resource/ArtistInfo/1234') result = json.loads(handler_response.data.decode()) assert handler_response.status_code == logic_response.status if logic_response.status == 200: assert result == logic_response.message else: assert result == logic_response.errors @pytest.mark.parametrize( 'logic_response', [ response.Response(message={'foo': 'bar'}, status=201), response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), response.create_fatal_response('Neo4j error'), ], ) def test_create_resource(logic_response, mocker, fixture_client): """Verify create_resource.""" mocker.patch('permissions.logic.resource.create_resource', return_value=logic_response) data = {'name': 'test artist'} handler_response = fixture_client.post('/e2e/resource/ArtistInfo/1234', json=data) result = json.loads(handler_response.data.decode()) assert handler_response.status_code == logic_response.status if logic_response.status == 201: assert result == logic_response.message else: assert result == logic_response.errors @pytest.mark.parametrize( ('data', 'url', 'expected'), [ [ {'name': 'test artist'}, '/e2e/resource/dummy/1234', response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message={ 'resource_type': [ 'Must be one of: ArtistInfo, Vendor, Subaccount, Label, SubAccount, LabelParticipant, Collaborator.' ] }, # noqa ), ], [ # name is not mandatory field. {}, '/e2e/resource/ArtistInfo/123', response.Response(message={'foo': 'bar'}, status=201), ], ], ) def test_create_resource_schema_validation(data, url, expected, mocker, fixture_client): """Verify create_resource for schema validation errors.""" mocker.patch( 'permissions.logic.resource.create_resource', return_value=response.Response(message={'foo': 'bar'}, status=201), ) handler_response = fixture_client.post(url, json=data) result = json.loads(handler_response.data.decode()) assert handler_response.status_code == expected.status if expected.status == 201: assert result == expected.message else: assert result == expected.errors @pytest.mark.parametrize( 'logic_response', [ response.Response(message={'foo': 'bar'}, status=204), response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], ) def test_delete_resource(logic_response, mocker, fixture_client): """Verify delete_resource.""" mocker.patch('permissions.logic.resource.delete_resource', return_value=logic_response) handler_response = fixture_client.delete( '/e2e/resource/ArtistInfo/1234', json={}, headers={'Content-Type': 'application/json'} ) assert handler_response.status_code == logic_response.status @pytest.mark.parametrize( ('headers', 'logic_response'), [ [ # valid request with valid headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.Response(message={'foo': 'bar'}), ], [ { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # empty headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # partial headers. { 'Orchard-Identity-Id': 'admin-uuid', }, response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message='invalid combination of profile_type, profile_id, and identity_id', ), ], ], ) def test_deactivate_user(headers, logic_response, mocker, fixture_client): """Verify deactivate_user.""" mocker.patch( 'permissions.logic.resource.deactivate_resources_common_with_admin', return_value=logic_response, ) handler_response = fixture_client.delete('/deactivate/identity/uuid-1234', headers=headers) assert handler_response.status_code == logic_response.status result = json.loads(handler_response.data.decode()) if logic_response: assert result == logic_response.message else: assert result == logic_response.errors @pytest.mark.parametrize( ('headers', 'logic_response', 'expected_response'), [ ( # valid request with valid headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', 'Content-Type': 'application/json', }, response.Response(message={'total': 2, 'data': ['foo', 'bar']}), response.Response( { 'items': ['foo', 'bar'], 'pagination': { 'limit': DEFAULT_LIMIT, 'offset': 0, 'total_records': 2, 'type': 'standard', 'active': True, }, } ), ), ( # incomplete headers. {'Orchard-Identity-Id': 'admin-uuid', 'Content-Type': 'application/json'}, response.Response(message={'total': 2, 'data': ['foo', 'bar']}), error_response.create_error_incomplete_profile_headers(), ), ( # missing headers. {}, response.Response(message={'total': 2, 'data': ['foo', 'bar']}), error_response.create_error_incomplete_profile_headers(), ), ( # error from logic. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', 'Content-Type': 'application/json', }, response.create_error_response('foo', 'bar'), response.create_error_response('foo', 'bar'), ), ], ) def test_get_user_resources_for_admin( headers, logic_response, expected_response, mocker, fixture_client ): """Test get_user_resources_for_admin.""" mocker.patch( 'permissions.logic.resource.get_user_resources_for_admin', return_value=logic_response ) handler_response = fixture_client.get( '/identity/user-uuid/direct-access/resources/LabelParticipant', json={}, headers=headers ) assert handler_response.status_code == expected_response.status result = json.loads(handler_response.data.decode()) if expected_response: assert result == expected_response.message else: assert result == expected_response.errors @pytest.mark.parametrize( ('headers', 'logic_response'), [ [ # valid request with valid headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.Response(message={'foo': 'bar'}), ], [ { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # empty headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # partial headers. { 'Orchard-Identity-Id': 'admin-uuid', }, response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message='invalid combination of profile_type, profile_id, and identity_id', ), ], ], ) def test_activate_user(headers, logic_response, mocker, fixture_client): """Verify activate_user.""" mocker.patch('permissions.logic.identity.activate_user', return_value=logic_response) handler_response = fixture_client.patch('/activate/identity/uuid-1234', headers=headers) assert handler_response.status_code == logic_response.status result = json.loads(handler_response.data.decode()) if logic_response: assert result == logic_response.message else: assert result == logic_response.errors @pytest.mark.parametrize( ('headers', 'logic_response'), [ [ # valid request with valid headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.Response(message={'foo': 'bar'}), ], [ # valid request with CollaboratorsProfile. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'CollaboratorsProfile', }, response.Response(message={'foo': 'bar'}), ], [ { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # empty headers. { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', }, response.create_error_response( code=error.ERROR_CODE_VALIDATION_ERROR, message='validation error' ), ], [ # partial headers. { 'Orchard-Identity-Id': 'admin-uuid', }, error_response.create_error_forbidden_user(), ], ], ) def test_create_identity_profiles_resources(headers, logic_response, mocker, fixture_client): """Test create_identity_profiles_resources.""" mocker.patch( 'permissions.logic.profile.create_identity_profiles_resources', return_value=logic_response ) mocker.patch( 'permissions.models.owsusers.get_settings_profile_for_identity', return_value={'profile_type': 'InsightsProfile', 'profile_id': 122}, ) data = { 'identity': {'name': 'foo@bar.com', 'email': 'foo@bar.com'}, 'resource_access': [ {'resource_type': 'LabelParticipant', 'roles': ['analytics'], 'uuid': 'uuid-8869'}, ], 'brand': 'awal', } handler_response = fixture_client.post( '/identity/add-resources-profiles', headers=headers, json=data ) assert handler_response.status_code == logic_response.status result = json.loads(handler_response.data.decode()) if logic_response: assert result == logic_response.message admin_context = { 'identity_id': headers.get('Orchard-Identity-Id'), 'profile_type': 'InsightsProfile', 'profile_id': int(headers.get('Orchard-Profile-Id')), } profile.create_identity_profiles_resources.assert_called_once_with( admin_context, brand='awal', identity=data['identity'], resource_access=data['resource_access'], create_auth0_user=True, send_password_reset=True, set_email_verified=True, overwrite_existing_access=True, master_contact=False, user_metadata={}, ) else: assert result == logic_response.errors @pytest.mark.parametrize( ('data',), [ ( { 'identity': {'name': 'foo@bar.com', 'email': 'foo@bar.com'}, 'resource_access': [ { 'resource_type': 'LabelParticipant', 'roles': ['analytics'], 'uuid': 'uuid-8869', }, ], }, ), ( { 'identity': {'name': 'foo@bar.com', 'email': 'foo@bar.com'}, 'resource_access': [ {'resource_type': 'Vendor', 'roles': ['analytics'], 'uuid': 'uuid-8869'}, ], }, ), ( { 'identity': {'name': 'foo@bar.com', 'email': 'foo@bar.com'}, 'resource_access': [ {'resource_type': 'Vendor', 'roles': ['payee_management'], 'uuid': 'uuid-8869'}, ], }, ), ( { 'identity': {'name': 'foo@bar.com', 'email': 'foo@bar.com'}, 'resource_access': [ { 'resource_type': 'Vendor', 'roles': ['analytics', 'payee_management'], 'uuid': 'uuid-8869', }, { 'resource_type': 'Subaccount', 'roles': ['analytics', 'catalog'], 'uuid': 'uuid-2456', }, { 'resource_type': 'LabelParticipant', 'roles': ['analytics'], 'uuid': 'uuid-lp1234', }, ], }, ), ], ) def test_create_identity_profiles_resources_required_fields(data, mocker, fixture_client): """Test create_identity_profiles_resources.""" headers = { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'InsightsProfile', } mocker.patch( 'permissions.logic.profile.create_identity_profiles_resources', return_value=response.Response(message={'logic': 'success'}), ) handler_response = fixture_client.post( '/identity/add-resources-profiles', headers=headers, json=data ) assert handler_response admin_context = { 'identity_id': headers.get('Orchard-Identity-Id'), 'profile_type': headers.get('Orchard-Profile-Type'), 'profile_id': int(headers.get('Orchard-Profile-Id')), } profile.create_identity_profiles_resources.assert_called_once_with( admin_context, identity=data['identity'], resource_access=data['resource_access'], create_auth0_user=True, send_password_reset=True, set_email_verified=True, overwrite_existing_access=True, master_contact=False, user_metadata={}, ) @patch('permissions.handlers.handlers.redis') @patch('permissions.handlers.handlers.resource') def test_check_profile_access_handler(mock_resource, mock_redis, fixture_client): """Test check_profile_access.""" mock_redis.get.return_value = None mock_resource.check_label_access_for_profile.return_value = 403 result = fixture_client.head('/profile/uuid/abc/resource/label/id/1') assert result.status_code == 403 mock_redis.get.return_value = 123 result = fixture_client.head('/profile/uuid/abc/resource/label/id/1') assert result.status_code == 123 @pytest.fixture def mock_happy_case_admin_headers(): """Return headers to be used in the happy-case for admin endpoints.""" return { 'Orchard-Identity-Id': 'admin-uuid', 'Orchard-Profile-Id': 122, 'Orchard-Profile-Type': 'SettingsProfile', } def test_add_identity_profiles_will_bust_cache( mocker, fixture_client, mock_happy_case_admin_headers, ): """Test add_identity_profiles will bust_identity_cache.""" # This is the minimum response required to test bust_identity_cache is called create_identity_profiles_resources_response = response.Response( { 'identities_affected': [ {'id': 'uuid-abc'}, {'id': 'uuid-lmn'}, ] } ) mocker.patch( 'permissions.handlers.handlers.profile.create_identity_profiles_resources', return_value=create_identity_profiles_resources_response, ) mock_bust_by_identity = mocker.patch( 'permissions.handlers.handlers.bust_by_identity', return_value=None ) payload = { # This is the minimum data required to pass validation 'identity': { 'name': 'Skittles', 'email': 'skittles@skittlesdog.com', } } result = fixture_client.post( '/identity/add-resources-profiles', headers=mock_happy_case_admin_headers, json=payload, ) assert result.status_code == 200 mock_bust_by_identity.assert_called_once_with( ['uuid-abc', 'uuid-lmn'], resource_types=['all_admin'] ) def test_edit_identity_profiles_will_bust_cache( mocker, fixture_client, mock_happy_case_admin_headers, ): """Test edit_identity_profiles will bust_identity_cache.""" edit_identity_profiles_resources_response = response.Response( {'status': 'ok', 'message': 'unrealistic response but irrelevant to this unit test'} ) # noqa: E501 mocker.patch( 'permissions.handlers.handlers.profile.edit_identity_profiles_resources', return_value=edit_identity_profiles_resources_response, ) mock_bust_by_identity = mocker.patch( 'permissions.handlers.handlers.bust_by_identity', return_value=None ) identity_uuid = 'uuid-xyz' result = fixture_client.post( f'/identity/{identity_uuid}/edit-resources-profiles', headers=mock_happy_case_admin_headers, json={}, # Looks like nothing is required in the POST body ) assert result.status_code == 200 mock_bust_by_identity.assert_called_once_with([identity_uuid], resource_types=['all_admin']) def test_add_resource_to_profile_will_bust_cache( mocker, fixture_client, ): """Test add_resource_to_profile by profile type/id will bust_identity_cache.""" add_resource_to_profile_response = response.Response( {'status': 'ok', 'message': 'unrealistic response but irrelevant to this unit test'} ) # noqa: E501 mocker.patch( 'permissions.handlers.handlers.resource.add_resource_to_profile', return_value=add_resource_to_profile_response, ) expected_identity_uuids = ['uuid-abc', 'uuid-def', 'uuid-xyz'] mock_get_identities_by_profile = mocker.patch( 'permissions.handlers.handlers.profile.get_identities_by_profile', return_value=expected_identity_uuids, ) mock_bust_by_identity = mocker.patch( 'permissions.handlers.handlers.bust_by_identity', return_value=None ) profile_type = 'InsightsProfile' profile_id = 8987 result = fixture_client.post( f'/ows/profile/{profile_type}/{profile_id}/has-access-to/resource/Vendor/123', json={}, # Looks like nothing is required in the POST body # And no header requirements ) assert result.status_code == 200 mock_get_identities_by_profile.assert_called_once_with( profile_type, profile_id, None, ) mock_bust_by_identity.assert_called_once_with( expected_identity_uuids, resource_types=['all_admin'] ) def test_add_resource_to_profile_uuid_will_bust_cache( mocker, fixture_client, ): """Test add_resource_to_profile by profile uuid will bust_identity_cache.""" add_resource_to_profile_response = response.Response( {'status': 'ok', 'message': 'unrealistic response but irrelevant to this unit test'} ) # noqa: E501 mocker.patch( 'permissions.handlers.handlers.resource.add_resource_to_profile', return_value=add_resource_to_profile_response, ) expected_identity_uuids = ['uuid-abc', 'uuid-def', 'uuid-xyz'] mock_get_identities_by_profile = mocker.patch( 'permissions.handlers.handlers.profile.get_identities_by_profile', return_value=expected_identity_uuids, ) mock_bust_by_identity = mocker.patch( 'permissions.handlers.handlers.bust_by_identity', return_value=None ) profile_uuid = 'this-is-a-profile-uuid' result = fixture_client.post( f'/ows/profile/{profile_uuid}/has-access-to/resource/Vendor/123', json={}, # Looks like nothing is required in the POST body # And no header requirements ) assert result.status_code == 200 mock_get_identities_by_profile.assert_called_once_with( 'InsightsProfile', 0, profile_uuid, ) mock_bust_by_identity.assert_called_once_with( expected_identity_uuids, resource_types=['all_admin'] )