"""Tests for the PDP authorization module.""" from http import HTTPStatus from unittest.mock import call, MagicMock, patch from uuid import uuid4 from owsrequest.context import RequestContext from pytest_mock import MockerFixture import application from payment.constants.error import ( ERROR_NO_VALID_IDENTITY_IN_CONTEXT, ERROR_NOT_PERMITTED_IDENTITY, ) from payment.utils import authorization from payment.utils.authorization import check_jwt_identity @patch('payment.utils.authorization.base') @patch('payment.utils.authorization.config') def test_authorize_resource( config_mock: MagicMock, resource_getters_base_mock: MagicMock, ) -> None: """Test the authorize_resource function.""" config_mock.authorization_backend.is_authorized.return_value = True result = authorization.authorize_resource(1, 'test_resource') assert result is True config_mock.authorization_backend.is_authorized.assert_called_once_with( action='view', resource_id=1, resource_type='test_resource', resource_getter=resource_getters_base_mock.ForwardKwargsGetter.return_value, ) @patch('payment.utils.authorization.g', spec=['log']) @patch('payment.utils.authorization.config') def test_authorize_resource_unauthorized( config_mock: MagicMock, g_mock: MagicMock, ) -> None: """Test the authorize_resource function.""" g_mock.request_context = MagicMock(jwt_identity_id='identity-id') config_mock.authorization_backend.is_authorized.return_value = False result = authorization.authorize_resource(1, 'test_resource') assert result is False g_mock.log.warning.assert_called_once_with( 'authorization_error', resources={ 'identity_id': 'identity-id', 'resource_id': 1, 'resource_type': 'test_resource', 'auth_response': False, }, ) @patch('payment.utils.authorization.base') @patch('payment.utils.authorization.config') def test_authorize_resource_uses_kwargs( config_mock: MagicMock, resource_getters_base_mock: MagicMock, ) -> None: """Test the authorize_resource uses kwargs.""" config_mock.authorization_backend.is_authorized.return_value = True result = authorization.authorize_resource( 1, 'test_resource', 'use', some='kwarg', ) assert result is True config_mock.authorization_backend.is_authorized.assert_called_once_with( action='use', resource_id=1, resource_type='test_resource', resource_getter=resource_getters_base_mock.ForwardKwargsGetter.return_value, some='kwarg', ) @patch('payment.utils.authorization.flask_request') def test_check_access_decorator_authorized(mock_flask_request: MagicMock) -> None: """Test check_access decorator when access is granted.""" mock_flask_request.verify_rules_access_standalone.return_value = True @authorization.check_access def test_func(): return 'success' result = test_func() assert result == 'success' mock_flask_request.verify_rules_access_standalone.assert_called_once() @patch('payment.utils.authorization.flaskify') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_decorator_unauthorized( mock_flask_request: MagicMock, mock_response: MagicMock, mock_flaskify: MagicMock, ) -> None: """Test check_access decorator when access is denied.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_error_response = MagicMock() mock_response.create_error_response.return_value = mock_error_response mock_flaskify.return_value = 'flaskified_response' @authorization.check_access def test_func(): return 'should not reach here' result = test_func() assert result == 'flaskified_response' mock_flask_request.verify_rules_access_standalone.assert_called_once() mock_response.create_error_response.assert_called_once_with( code='authorization_error', message='Unauthorized', status=403, ) mock_flaskify.assert_called_once_with(mock_error_response) @patch('payment.utils.authorization.flask_request') def test_check_access_decorator_with_args_kwargs( mock_flask_request: MagicMock, ) -> None: """Test check_access decorator preserves function args and kwargs.""" mock_flask_request.verify_rules_access_standalone.return_value = True @authorization.check_access def test_func(arg1, arg2, kwarg1=None, kwarg2=None): return f'{arg1}-{arg2}-{kwarg1}-{kwarg2}' result = test_func('a', 'b', kwarg1='c', kwarg2='d') assert result == 'a-b-c-d' mock_flask_request.verify_rules_access_standalone.assert_called_once() @patch('payment.utils.authorization.flask_request') def test_check_access_decorator_preserves_function_metadata( mock_flask_request: MagicMock, ) -> None: """Test check_access decorator preserves original function metadata.""" mock_flask_request.verify_rules_access_standalone.return_value = True @authorization.check_access def test_func_with_docstring(): """This is a test docstring.""" return 'success' assert test_func_with_docstring.__name__ == 'test_func_with_docstring' assert test_func_with_docstring.__doc__ == 'This is a test docstring.' @patch('payment.utils.authorization.authorize_resource') @patch('payment.utils.authorization.flaskify') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_configurable_resource_id( mock_flask_request: MagicMock, mock_response: MagicMock, mock_flaskify: MagicMock, mock_authorize_resource: MagicMock, ) -> None: """Test configurable decorator with resource_id provided.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_authorize_resource.return_value = True @authorization.check_access('test_type', 'view', 42) def test_func(): return 'success' result = test_func() assert result == 'success' mock_authorize_resource.assert_called_once_with( resource_id=42, resource_type='test_type', action='view', ) @patch('payment.utils.authorization.authorize_resource') @patch('payment.utils.authorization.flaskify') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_configurable_resource_id_param( mock_flask_request: MagicMock, mock_response: MagicMock, mock_flaskify: MagicMock, mock_authorize_resource: MagicMock, ) -> None: """Test configurable decorator with resource_id_param from kwargs.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_authorize_resource.return_value = True @authorization.check_access('test_type', 'edit', None, 'resource_id') def test_func(resource_id=None): return 'edited' result = test_func(resource_id=99) assert result == 'edited' mock_authorize_resource.assert_called_once_with( resource_id=99, resource_type='test_type', action='edit', ) @patch('payment.utils.authorization.flaskify') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_configurable_missing_resource_id( mock_flask_request: MagicMock, mock_response: MagicMock, mock_flaskify: MagicMock, ) -> None: """Test configurable decorator with missing resource_id and resource_id_param.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_error_response = MagicMock() mock_response.create_error_response.return_value = mock_error_response mock_flaskify.return_value = 'flaskified_response' @authorization.check_access('test_type', 'view', None, 'resource_id') def test_func(other_param='value'): return 'should not reach here' result = test_func() assert result == 'flaskified_response' mock_response.create_error_response.assert_called_once_with( code='authorization_error', message='Resource ID not provided', status=403, ) mock_flaskify.assert_called_once_with(mock_error_response) @patch('payment.utils.authorization.authorize_resource') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_configurable_default_resource_id( mock_flask_request: MagicMock, mock_response: MagicMock, mock_authorize_resource: MagicMock, ) -> None: """Test configurable decorator with missing resource_id and resource_id_param.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_error_response = MagicMock() mock_response.create_error_response.return_value = mock_error_response @authorization.check_access('test_type', 'view', None, None) def test_func(): return 'view' result = test_func() assert result == 'view' assert not mock_response.create_error_response.called mock_authorize_resource.assert_called_once_with( resource_id='0', resource_type='test_type', action='view', ) @patch('payment.utils.authorization.authorize_resource') @patch('payment.utils.authorization.flaskify') @patch('payment.utils.authorization.response') @patch('payment.utils.authorization.flask_request') def test_check_access_configurable_unauthorized_resource( mock_flask_request: MagicMock, mock_response: MagicMock, mock_flaskify: MagicMock, mock_authorize_resource: MagicMock, ) -> None: """Test configurable decorator when authorize_resource returns False.""" mock_flask_request.verify_rules_access_standalone.return_value = False mock_authorize_resource.return_value = False mock_error_response = MagicMock() mock_response.create_error_response.return_value = mock_error_response mock_flaskify.return_value = 'flaskified_response' @authorization.check_access('test_type', 'view', 123) def test_func(): return 'should not reach here' result = test_func() assert result == 'flaskified_response' mock_authorize_resource.assert_called_once_with( resource_id=123, resource_type='test_type', action='view', ) mock_response.create_error_response.assert_called_once_with( code='authorization_error', message='Unauthorized', status=403, ) mock_flaskify.assert_called_once_with(mock_error_response) def test_check_jwt_identity_success(mocker: MockerFixture, faker) -> None: """ Tests that if the request context has the correct identity, we return the decorated function result. """ test_identity = str(uuid4()) test_message = faker.pystr() mock_context = mocker.MagicMock(spec=RequestContext) mock_context.jwt_identity_id = test_identity @check_jwt_identity(test_identity) def to_be_decorated(obscure_pii=True): return test_message, HTTPStatus.OK mock_g = mocker.patch('payment.utils.authorization.g') mock_g.request_context = mock_context with application.app.test_request_context(): result = to_be_decorated() assert result == (test_message, 200) def test_check_jwt_identity_multiple_success(mocker: MockerFixture, faker) -> None: """ Tests that if the request context has the correct identity, we return the decorated function result. """ test_identity1 = str(uuid4()) test_identity2 = str(uuid4()) mock_context = mocker.MagicMock(spec=RequestContext) for test_identity in (test_identity1, test_identity2): mock_context.jwt_identity_id = test_identity @check_jwt_identity([test_identity1, test_identity2]) def to_be_decorated(test_param): return test_param, HTTPStatus.OK mock_g = mocker.patch('payment.utils.authorization.g') mock_g.request_context = mock_context with application.app.test_request_context(): result = to_be_decorated(True) assert result == (True, 200) def test_check_jwt_identity_failure_no_identity(mocker: MockerFixture, faker) -> None: """ Tests that if the request context does not have the correct identity, we return 403. """ test_identity = str(uuid4()) test_message = faker.pystr() mock_context = mocker.MagicMock(spec=RequestContext) mock_context.jwt_identity_id = None @check_jwt_identity(test_identity) def to_be_decorated(): return test_message, HTTPStatus.OK mock_g = mocker.patch('payment.utils.authorization.g') mock_g.request_context = mock_context with application.app.test_request_context(): result = to_be_decorated() assert result == ( {'error': ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, 401, ) def test_check_jwt_identity_failure_wrong_identity( mocker: MockerFixture, faker ) -> None: """ Tests that if the request context does not have the correct identity, we return 403. """ test_identity = str(uuid4()) wrong_identity = str(uuid4()) test_message = faker.pystr() mock_context = mocker.MagicMock(spec=RequestContext) mock_context.jwt_identity_id = wrong_identity @check_jwt_identity(test_identity) def to_be_decorated(): return test_message, HTTPStatus.OK mock_g = mocker.patch('payment.utils.authorization.g') mock_g.request_context = mock_context with application.app.test_request_context(): result = to_be_decorated() assert result == ( {'error': ERROR_NOT_PERMITTED_IDENTITY.format(jwt_identity=wrong_identity)}, HTTPStatus.UNAUTHORIZED, )