"""Tests for request utility.""" from unittest.mock import MagicMock from unittest.mock import patch from fastapi import HTTPException from owsrequest.rules import EndpointRulesValidator import pytest from moneyhub.constants.constants import Header from moneyhub.constants.constants import LAMBDA_PROFILE_ID_QA from moneyhub.constants.constants import TOKEN from moneyhub.constants.error import MISSING_JSON_BODY from moneyhub.schemas.statement_attachment import StatementAttachmentPutSchema from moneyhub.utils import request from moneyhub.utils.request import extract_token def test_extract_json_body(): """Test extract_json_body method.""" request_body = StatementAttachmentPutSchema(file_location='s3://testurl') response = request.extract_json_body(request_body) assert response == {'file_location': 's3://testurl'} def test_extract_json_body_for_empty_req_body(): """Test extract_json_body method for empty request body.""" request_body = StatementAttachmentPutSchema() with pytest.raises(HTTPException): response = request.extract_json_body(request_body) assert response.json() == {'detail': MISSING_JSON_BODY} @patch('moneyhub.utils.request.get_resources_for_profile') @pytest.mark.parametrize('account_id, subaccount_id, expected', [ (24601, None, True), (12345, None, True), (123, None, False), (99999, None, False), (99999, 10003, True), ]) def test_profile_has_access_to_resource( mock_get_resources_for_profile, account_id, subaccount_id, expected): """Test checking if a profile has access to a resource.""" profile_type = 'MoneyhubProfile' profile_id = 327621 mock_get_resources_for_profile.return_value = [ { 'type': 'Vendor', 'vendorId': 24601, }, { 'type': 'Vendor', 'vendorId': 12345, }, { 'type': 'Subaccount', 'vendor_id': 99999, 'id': 10003, } ] result = request.profile_has_access_to_resource( profile_type, profile_id, account_id, subaccount_id) assert result == expected mock_get_resources_for_profile.assert_called_with(profile_type, profile_id) @patch('moneyhub.utils.request.get_resources_for_profile') @pytest.mark.parametrize('account_id, subaccount_id, expected', [ (24601, None, True), (12345, None, True), (99999, 10003, True), ]) def test_profile_has_access_to_resource_wildcard( mock_get_resources_for_profile, account_id, subaccount_id, expected): """Test checking if a profile has access to a resource when it has wildcard access.""" profile_type = 'MoneyhubProfile' profile_id = 327621 mock_get_resources_for_profile.return_value = [ { 'type': 'Vendor', 'vendorId': '*', }, ] result = request.profile_has_access_to_resource( profile_type, profile_id, account_id, subaccount_id) assert result == expected mock_get_resources_for_profile.assert_called_with(profile_type, profile_id) @patch('moneyhub.utils.request.g') @patch('moneyhub.utils.request.jwt_auth_enabled_for_env') @patch('moneyhub.utils.request.get_resources_for_profile') @patch('moneyhub.utils.request.Request') @pytest.mark.parametrize('profile_type, profile_role, log_only, should_error', [ ('MoneyhubProfile', 'accounting', False, False), ('MoneyhubProfile', 'malcontent', False, True), ('MessedUpProfile', 'accounting', False, True), ('MoneyhubProfile', 'malcontent', True, False), ]) def test_verify_rules_access_access_denied( mock_request, mock_resources_for_profile, mock_jwt_auth_enabled_for_env, mock_g, profile_type, profile_role, log_only, should_error ): """Test making a request with access rules failing validation.""" mock_jwt_auth_enabled_for_env.return_value = True profile_id = 100001 uuid = '91241f37-047d-476a-b65d-4d9b508b13b8' mock_request = MagicMock(headers={Header.AUTHORIZATION: 'Bearer fake-token'}) mock_request.method = 'GET' mock_g.return_value = MagicMock( token=__mock_token(profile_id, profile_type, profile_role, uuid)) mock_request.url.path = '/account/1234/statement-periods' mock_resources_for_profile.return_value = [ {'type': 'Vendor', 'vendorId': 1234}, {'type': 'Vendor', 'vendorId': '*'}, ] if should_error: with pytest.raises(HTTPException) as err: request.verify_rules_access( request=mock_request, validator=EndpointRulesValidator('moneyhub/access_rules.yml'), log_only=log_only) assert err.value.detail == 'Access denied' else: request.verify_rules_access( request=mock_request, validator=EndpointRulesValidator('moneyhub/access_rules.yml'), log_only=log_only) @patch('moneyhub.utils.request.g') @patch('moneyhub.utils.request.jwt_auth_enabled_for_env') @patch('moneyhub.utils.request.get_resources_for_profile') @patch('moneyhub.utils.request.Request') @pytest.mark.parametrize('subaccount_id, should_error', [ (10001, False), (20002, True), ]) def test_verify_rules_access_subaccount( mock_request, mock_resources_for_profile, mock_jwt_auth_enabled_for_env, mock_g, subaccount_id, should_error ): """Test making a request with a subaccount.""" mock_jwt_auth_enabled_for_env.return_value = True mock_request = MagicMock(headers={Header.AUTHORIZATION: 'Bearer fake-token'}) mock_request.method = 'GET' mock_g.return_value = MagicMock( token=__mock_token(24601, 'MoneyhubProfile', 'accounting', '4d9b508b13b8')) mock_request.url.path = '/account/1234/statement-periods' mock_request.query_params = { 'is_subaccount': 1, 'subaccount_id': subaccount_id, } mock_resources_for_profile.return_value = [ {'type': 'Subaccount', 'id': 10001}, ] if should_error: with pytest.raises(HTTPException) as err: request.verify_rules_access( request=mock_request, validator=EndpointRulesValidator('moneyhub/access_rules.yml'), log_only=False) assert err.value.detail == 'You do not have access to the requested URL.' else: request.verify_rules_access( request=mock_request, validator=EndpointRulesValidator('moneyhub/access_rules.yml'), log_only=False) @patch('moneyhub.utils.request.g') @patch('moneyhub.utils.request.jwt_auth_enabled_for_env') @patch('moneyhub.utils.request.Request') @patch('moneyhub.utils.request.get_resources_for_profile') def test_verify_rules_access_ownership_denied( mock_resources_for_profile, mock_request, mock_jwt_auth_enabled_for_env, mock_g ): """Test making a request with account ownership failing validation.""" profile_type = 'MoneyhubProfile' profile_id = 100001 mock_request.method = 'GET' mock_request.url.path = '/account/1234/statement-periods' mock_request.headers = {Header.AUTHORIZATION: 'Bearer fake-token'} mock_g.return_value = MagicMock( token=__mock_token(profile_id, profile_type, None, None)) mock_jwt_auth_enabled_for_env.return_value = True mock_validator = MagicMock() mock_validator.has_access.return_value = True mock_resources_for_profile.return_value = [ {'type': 'Vendor', 'vendorId': 12345}, {'type': 'Vendor', 'vendorId': 78910}, ] expected = 'You do not have access to the requested URL.' with pytest.raises(HTTPException) as err: request.verify_rules_access( request=mock_request, validator=mock_validator, log_only=False) assert err.value.detail == expected mock_resources_for_profile.assert_called_with(profile_type, profile_id) @patch('moneyhub.utils.request.g') @patch('moneyhub.utils.request.jwt_auth_enabled_for_env') @patch('moneyhub.utils.request.Request') def test_verify_rules_access_without_token(mock_request, mock_jwt_auth_enabled_for_env, mock_g): """Test making a request without a token failing validation.""" mock_g.return_value = MagicMock(token=None) mock_jwt_auth_enabled_for_env.return_value = True mock_request.method = 'GET' mock_request.scope = {} mock_request.url.path = '/account/1234/statement-periods' with pytest.raises(HTTPException) as err: request.verify_rules_access( request=mock_request, validator=EndpointRulesValidator('moneyhub/access_rules.yml'), log_only=True) assert err.value.detail == 'Access denied. Missing JWT token.' @patch('moneyhub.utils.request.Request') def test_extract_token_user(mock_request): """Test extracting a user token from a token string.""" profile_id = 123 profile_type = 'MoneyhubProfile' role = 'accounting' orchard_identity_id = 'fake-id' raw_token = __mock_request_token( profile_id=123, profile_type=profile_type, profile_role=role, orchard_identity_id=orchard_identity_id ) mock_request.scope = {TOKEN: raw_token} actual = extract_token(mock_request) assert actual == { 'profile_type': profile_type, 'profile_id': profile_id, 'roles': [role], 'orchardIdentityId': orchard_identity_id } @patch('moneyhub.utils.request.Request') def test_extract_token_user_multiple_profiles(mock_request): """Test extracting a user token from a token string with multiple profiles.""" profile_id = 123 profile_type = 'AbacusProfile' role = 'administrator' orchard_identity_id = 'fake-id-2' raw_token = { 'https://grass.theorchard.com/profiles': [ { 'profile_id': 9900104, 'profile_type': 'Account360Profile', 'roles': ['account360'], 'uuid': 'fake-id-1' }, { 'profile_id': profile_id, 'profile_type': profile_type, 'roles': [role], 'uuid': orchard_identity_id } ], 'https://grass.theorchard.com/user_metadata': { 'orchardIdentityId': orchard_identity_id } } mock_request.scope = {TOKEN: raw_token} actual = extract_token(mock_request) assert actual == { 'profile_type': profile_type, 'profile_id': profile_id, 'roles': [role], 'orchardIdentityId': orchard_identity_id } @patch('moneyhub.utils.request.Request') def test_extract_token_machine(mock_request): """Test extracting a machine token from a token string.""" raw_token = { 'iss': 'https://fake-url/', 'sub': 'OfwbefiuwebfkjsHhyMBBt5A89mGXTS3@clients', 'aud': 'https://fake-url', 'iat': 1665477021, 'exp': 1665513021, 'azp': 'OfwbefiuwebfkjsHhyMBBt5A89mGXTS3', 'gty': 'client-credentials' } mock_request.scope = {TOKEN: raw_token} actual = extract_token(mock_request) assert actual == { 'profile_type': 'AbacusProfile', 'profile_id': LAMBDA_PROFILE_ID_QA, 'roles': ['administrator'], 'orchardIdentityId': 'system_id' } @patch('moneyhub.utils.request.Request') def test_extract_token_no_token(mock_request): """Test extracting a machine token when no token is present.""" mock_request.scope = {} result = extract_token(mock_request) assert result is None def __mock_request_token( profile_id: int, profile_type: str, profile_role: str | None = None, profile_uuid: str | None = None, orchard_identity_id: str | None = None ) -> dict: """Create a raw JWT token taken from a request object used for Authorization.""" return { 'https://grass.theorchard.com/profiles': [ { 'profile_name': 'John Doe', 'first_name': 'John', 'last_name': 'Doe', 'created_at': '2023-05-15T11:37:46.425828Z', 'profile_type': profile_type, 'profile_id': profile_id, 'full_catalog_access': True, 'roles': [ profile_role ], 'uuid': profile_uuid, 'brand': 'orchard' }, ], 'https://grass.theorchard.com/user_metadata': { 'orchardIdentityId': orchard_identity_id } } def __mock_token( profile_id: int, profile_type: str = 'Moneyhub', profile_role: str | None = 'accounting', orchard_identity_id: str | None = 'fake-uuid' ) -> dict: """Create a new token dict.""" return { 'profile_type': profile_type, 'profile_id': profile_id, 'roles': [profile_role], 'orchardIdentityId': orchard_identity_id }