"""Tests for the tenant logic module.""" import uuid from unittest import mock from unittest.mock import MagicMock, patch import flask import pytest from owsresponse import response from sqlalchemy.orm import exc as orm_exceptions from permissions.constants import constants from permissions.exceptions.incomplete_result_error import IncompleteResultError from permissions.logic import tenant from permissions.models import ( profile as profile_model, tenant as tenant_model, ) from permissions.types import ( AccessibleTenant, AdminableTenant, AdminableTenantDataloader, IdentityTenantsWithV2Roles, ProfileInfo, Tenant, TenantType, TenantWithV2Roles, ) from tests.unit import conftest @pytest.mark.parametrize( ('tenants', 'result'), [ # Don't remove the audience profile role! ( [ AdminableTenant( tenant=Tenant(tenant_uuid='aaa', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='AudienceProfile', profile_id=123, roles=['audience'], uuid='uuid-123', ), ProfileInfo( profile_type='InsightsProfile', profile_id=445, roles=['analytics'], uuid='uuid-abc', ), ], ) ], [ TenantWithV2Roles( tenant_uuid='aaa', tenant_type=TenantType.ACCOUNT, roles=['fansifter_can_view_fan_data', 'INSIGHTS_BASE_ROLE'], ) ], ), # Keep only the valid insights profile role ( [ AdminableTenant( tenant=Tenant(tenant_uuid='bbb', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='InsightsProfile', profile_id=123, roles=['analytics', 'inciting'], uuid='uuid-123', ), ], ) ], [ TenantWithV2Roles( tenant_uuid='bbb', tenant_type=TenantType.ACCOUNT, roles=['INSIGHTS_BASE_ROLE'] ) ], ), ], ) def test_unsupported_profiles_and_roles_removed( tenants: list[AdminableTenant], result: list[TenantWithV2Roles] ) -> None: """Test that unsupported profiles and roles are removed.""" assert tenant.map_to_v2_roles(tenants) == result @pytest.mark.parametrize( ('tenants', 'expected_result'), [ pytest.param( [ AdminableTenant( tenant=Tenant(tenant_uuid='aaa', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ) ], [ TenantWithV2Roles( tenant_uuid='aaa', tenant_type=TenantType.SUBACCOUNT, roles=['SETTINGS_BASE_ROLE'], ) ], id='Convert settings profile despite lack of explicit roles', ), pytest.param( [ AdminableTenant( tenant=Tenant(tenant_uuid='bbb', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='LabelProfile', profile_id=123, roles=['administrator', 'accounting'], uuid='uuid-123', ) ], ) ], [ TenantWithV2Roles( tenant_uuid='bbb', tenant_type=TenantType.SUBACCOUNT, roles=[ 'WORKSTATION_ADMIN_ROLE', ], ) ], id='Convert label profile admin role', ), pytest.param( [ AdminableTenant( tenant=Tenant(tenant_uuid='ccc', tenant_type=TenantType.COLLABORATOR), profiles=[ ProfileInfo( profile_type='LabelProfile', profile_id=123, roles=['accounting', 'analytics'], uuid='uuid-123', ), ProfileInfo( profile_type='MoneyhubProfile', profile_id=456, roles=['accounting'], uuid='uuid-abc', ), ], ) ], [ tenant.TenantWithV2Roles( tenant_uuid='ccc', tenant_type=TenantType.COLLABORATOR, roles=[ 'WORKSTATION_ANALYTICS_BASE_ROLE', 'CUSTOMER_ACCOUNTING_BASE_ROLE', ], ) ], id='Convert label profile roles to workstation roles', ), pytest.param( [ AdminableTenant( tenant=Tenant(tenant_uuid='ddd', tenant_type=TenantType.LABEL_PARTICIPANT), profiles=[ ProfileInfo( profile_type='InsightsProfile', profile_id=123, roles=['analytics'], uuid='uuid-123', ) ], ) ], [ tenant.TenantWithV2Roles( tenant_uuid='ddd', tenant_type=TenantType.LABEL_PARTICIPANT, roles=['INSIGHTS_BASE_ROLE'], ) ], id='Convert tenant type to snakecase', ), ], ) @mock.patch('permissions.logic.tenant.vend_contact_role') def test_profiles_to_v2_roles( vend_contact_role_mock: MagicMock, tenants: list[AdminableTenant], expected_result: list[TenantWithV2Roles], ) -> None: """Test that neo4j profiles and roles get mapped to corresponding v2 roles.""" # Needed for label profile administrator role case vend_contact_role_mock.VendContactRole.get_role_ids_by_vend_contact.return_value = [4] assert tenant.map_to_v2_roles(tenants) == expected_result @pytest.mark.parametrize( 'role_ids', [ pytest.param([4, 5], id='administrator and accounting roles'), pytest.param([5], id='just accounting role'), ], ) @mock.patch('permissions.logic.tenant.g') @mock.patch('permissions.logic.tenant.vend_contact_role') @mock.patch('permissions.logic.tenant.mysql.db_session') def test_profiles_to_v2_roles_label_profile_admin( db_session_mock: MagicMock, vend_contact_role_mock: MagicMock, g_mock: MagicMock, role_ids: list[int], app_context: flask.ctx.AppContext, ) -> None: """Test that label profile administrator role gets mapped to workstation admin role.""" session_mock = conftest.get_session_mock() db_session_mock.return_value = session_mock vend_contact_role_mock.VendContactRole.get_role_ids_by_vend_contact.return_value = role_ids tenants = [ AdminableTenant( tenant=Tenant(tenant_uuid='bbb', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='LabelProfile', profile_id=123, roles=['administrator', 'accounting'], uuid='uuid-123', ) ], ) ] expected_result = [ TenantWithV2Roles( tenant_uuid='bbb', tenant_type=TenantType.SUBACCOUNT, roles=[ 'WORKSTATION_ADMIN_ROLE', ], ) ] assert tenant.map_to_v2_roles(tenants) == expected_result vend_contact_role_mock.VendContactRole.get_role_ids_by_vend_contact.assert_called_once_with( tx=session_mock.__enter__.return_value, vend_contact_id=123 ) if 4 not in role_ids: g_mock.log.warn.assert_called_with( 'User does not have a vend contact role for administrator role', resources={'vend_contact_id': 123}, ) def test_get_my_adminable_tenant_types(): """Test get_my_adminable_tenant_types.""" return_value = [ { 'tenant_type': TenantType.ACCOUNT, 'tenant_count': 2, }, { 'tenant_type': 'label_participant', 'tenant_count': 2, }, { 'tenant_type': 'collaborator', 'tenant_count': 1, }, ] with patch.object( tenant_model, 'get_admin_tenant_type_count', return_value=response.Response(message=return_value), ), patch.object( profile_model, 'check_vendor_star_access', return_value=response.create_not_found_response( 'Identity does not have access to Vendor *.' ), ): actual = tenant.get_my_adminable_tenant_types( 'admin-uuid', ProfileInfo(profile_type='SettingsProfile', profile_id=12345, roles=[], uuid='abc-123'), ) assert actual.status == 200 assert actual.message == return_value assert profile_model.check_vendor_star_access.call_count == 1 assert tenant_model.get_admin_tenant_type_count.call_count == 1 @mock.patch('permissions.connectors.neo4j.db_session') @mock.patch('permissions.models.profile._check_vendor_star_access_settings') @mock.patch('permissions.models.tenant.check_admin_access_to_tenants') def test_check_admin_access_to_tenants_with_vendor_star_access( check_admin_access_to_tenants_mock, check_vendor_star_access_settings_mock, db_session_mock, test_logic_tenants, test_settings_profile, ): """Test check_admin_access_to_tenants with Vendor * access.""" session_mock = conftest.get_session_mock() db_session_mock.return_value = session_mock check_vendor_star_access_settings_mock.return_value = True result = tenant.check_admin_access_to_tenants( 'user-123', test_logic_tenants, test_settings_profile ) # Assert assert result == [ AccessibleTenant(tenant_type=t.tenant_type, tenant_uuid=t.tenant_uuid, access=True) for t in test_logic_tenants ] check_vendor_star_access_settings_mock.assert_called_once_with( tx=session_mock.__enter__.return_value, identity_id='user-123', profile_id=test_settings_profile.profile_id, ) check_admin_access_to_tenants_mock.assert_not_called() @mock.patch('permissions.connectors.neo4j.db_session') @mock.patch('permissions.models.profile._check_vendor_star_access_settings') @mock.patch('permissions.models.tenant.check_admin_access_to_tenants') def test_check_admin_access_to_tenants_without_vendor_star_access( check_admin_access_to_tenants_mock, check_vendor_star_access_settings_mock, db_session_mock, test_logic_tenants, test_settings_profile, ): """Test check_admin_access_to_tenants without Vendor * access.""" session_mock = conftest.get_session_mock() db_session_mock.return_value = session_mock check_vendor_star_access_settings_mock.side_effect = IncompleteResultError('not found') check_admin_access_to_tenants_mock.return_value = [ AccessibleTenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT, access=True), AccessibleTenant(tenant_uuid='def-456', tenant_type=TenantType.SUBACCOUNT, access=False), AccessibleTenant(tenant_uuid='ghi-789', tenant_type=TenantType.COLLABORATOR, access=False), AccessibleTenant( tenant_uuid='jkl-012', tenant_type=TenantType.LABEL_PARTICIPANT, access=True ), AccessibleTenant(tenant_uuid='mno-345', tenant_type=TenantType.ACCOUNT, access=False), AccessibleTenant(tenant_uuid='pqr-678', tenant_type=TenantType.SUBACCOUNT, access=True), AccessibleTenant(tenant_uuid='stu-901', tenant_type=TenantType.COLLABORATOR, access=True), AccessibleTenant( tenant_uuid='vwx-234', tenant_type=TenantType.LABEL_PARTICIPANT, access=False ), ] result = tenant.check_admin_access_to_tenants( 'user123', test_logic_tenants, test_settings_profile ) assert result == [ AccessibleTenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT, access=True), AccessibleTenant(tenant_uuid='def-456', tenant_type=TenantType.SUBACCOUNT, access=False), AccessibleTenant(tenant_uuid='ghi-789', tenant_type=TenantType.COLLABORATOR, access=False), AccessibleTenant( tenant_uuid='jkl-012', tenant_type=TenantType.LABEL_PARTICIPANT, access=True ), AccessibleTenant(tenant_uuid='mno-345', tenant_type=TenantType.ACCOUNT, access=False), AccessibleTenant(tenant_uuid='pqr-678', tenant_type=TenantType.SUBACCOUNT, access=True), AccessibleTenant(tenant_uuid='stu-901', tenant_type=TenantType.COLLABORATOR, access=True), AccessibleTenant( tenant_uuid='vwx-234', tenant_type=TenantType.LABEL_PARTICIPANT, access=False ), ] check_vendor_star_access_settings_mock.assert_called_once_with( tx=session_mock.__enter__.return_value, identity_id='user123', profile_id=test_settings_profile.profile_id, ) check_admin_access_to_tenants_mock.assert_called_once_with( session=session_mock.__enter__.return_value, tenants=test_logic_tenants, settings_profile=test_settings_profile, ) @pytest.mark.parametrize( ['check_result', 'return_value'], [ pytest.param([mock.Mock(access=True)], True, id='has access'), pytest.param([], False, id='doesnt have access'), pytest.param( [mock.Mock(access=True), mock.Mock(access=True)], False, id='too many items in result' ), ], ) @mock.patch('permissions.logic.tenant.check_admin_access_to_tenants') def test_check_admin_access_to_tenant( check_tenants_mock: mock.MagicMock, check_result: list[mock.Mock], return_value: bool, ): """Test check_admin_access_to_tenant.""" check_tenants_mock.return_value = check_result identity_id = '1️⃣-2️⃣-3️⃣-4️⃣' mock_tenant = mock.Mock() settings_profile = mock.Mock() result = tenant.check_admin_access_to_tenant( identity_uuid=identity_id, tenant=mock_tenant, settings_profile=settings_profile, ) assert result == return_value check_tenants_mock.assert_called_with( identity_uuid=identity_id, tenants=[mock_tenant], settings_profile=settings_profile, ) @mock.patch('permissions.connectors.neo4j.db_session') @mock.patch('permissions.models.tenant.get_parent_company_brand_for_tenant') def test_get_parent_company_brand_for_tenant( get_parent_company_brand_for_tenant_mock: MagicMock, db_session_mock: MagicMock ) -> None: """Test get_parent_company_brand_for_tenant.""" session_mock = conftest.get_session_mock() db_session_mock.return_value = session_mock get_parent_company_brand_for_tenant_mock.return_value = 'awal' result = tenant.get_parent_company_brand_for_tenant( Tenant(tenant_type=TenantType.ACCOUNT, tenant_uuid='nbea8e71-e2c9-4804-93d7-ae471a974534') ) assert result == 'awal' get_parent_company_brand_for_tenant_mock.assert_called_once() @pytest.mark.parametrize( ('tenant_to_add', 'roles_to_check', 'available_apps', 'available_roles', 'expected_result'), [ [ Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), ['WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE'], [ { 'application_id': 'WORKSTATION_ANALYTICS_APP', 'roles': ['WORKSTATION_ANALYTICS_BASE_ROLE'], 'url': 'https://workstation.theorchard.com/analytics', }, { 'application_id': 'SETTINGS_APP', 'roles': ['SETTINGS_BASE_ROLE'], 'url': 'https://settings.theorchard.com', }, ], ['WORKSTATION_ANALYTICS_APP', 'SETTINGS_BASE_ROLE'], True, ], [ Tenant(tenant_uuid='byn-293', tenant_type=TenantType.ACCOUNT), ['INSIGHTS_BASE_ROLE'], [ { 'application_id': 'WORKSTATION_ANALYTICS_APP', 'roles': ['WORKSTATION_ANALYTICS_BASE_ROLE'], 'url': 'https://workstation.theorchard.com/analytics', }, { 'application_id': 'SETTINGS_APP', 'roles': ['SETTINGS_BASE_ROLE'], 'url': 'https://settings.theorchard.com', }, ], [ 'WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE', ], False, ], [ Tenant(tenant_uuid='qsa-777', tenant_type=TenantType.LABEL_PARTICIPANT), ['SETTINGS_BASE_ROLE'], [], [], False, ], ], ) @patch('permissions.logic.tenant.g') @patch('permissions.models.ows_account.get_enabled_tenant_applications') def test_check_compatibility_with_tenant_configuration( mock_get_enabled_tenant_applications: MagicMock, g_mock: MagicMock, tenant_to_add: Tenant, roles_to_check: list[str], available_apps: list[dict], available_roles: list[str], expected_result: bool, app_context, ) -> None: """Test check_compatibility_with_tenant_configuration.""" mock_get_enabled_tenant_applications.return_value = available_apps result = tenant.check_compatibility_with_tenant_configuration(tenant_to_add, roles_to_check) assert result == expected_result if result is False: unavailable_roles = [role for role in roles_to_check if role not in available_roles] if available_roles and unavailable_roles: g_mock.log.info.assert_called_once_with( 'Unavailable roles requested', resources={ 'roles_to_check': roles_to_check, 'unavailable_roles': unavailable_roles, 'tenant_uuid': tenant_to_add.tenant_uuid, }, ) @pytest.mark.parametrize( ( 'tenant_to_modify', 'roles_to_check', 'available_apps', 'current_roles', 'accessible_tenant', 'expected_result', ), [ [ Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), ['WORKSTATION_CATALOG_ROLE'], [ { 'application_id': 'WORKSTATION_CATALOG_APP', 'roles': ['WORKSTATION_CATALOG_ROLE'], 'url': 'https://workstation.theorchard.com/catalog', }, { 'application_id': 'SETTINGS_APP', 'roles': ['SETTINGS_BASE_ROLE'], 'url': 'https://settings.theorchard.com', }, ], ['WORKSTATION_CATALOG_APP', 'SETTINGS_BASE_ROLE'], Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), False, ], [ Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), [ 'WORKSTATION_CATALOG_ROLE', 'WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE', ], [ { 'application_id': 'WORKSTATION_CATALOG_APP', 'roles': ['WORKSTATION_CATALOG_ROLE'], 'url': 'https://workstation.theorchard.com/catalog', }, { 'application_id': 'WORKSTATION_ANALYTICS_APP', 'roles': ['WORKSTATION_ANALYTICS_BASE_ROLE'], 'url': 'https://workstation.theorchard.com/analytics', }, { 'application_id': 'SETTINGS_APP', 'roles': ['SETTINGS_BASE_ROLE'], 'url': 'https://settings.theorchard.com', }, ], [ 'WORKSTATION_CATALOG_ROLE', 'WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE', ], Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), True, ], [ Tenant(tenant_uuid='byn-293', tenant_type=TenantType.ACCOUNT), [ 'WORKSTATION_CATALOG_ROLE', 'WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE', ], [ { 'application_id': 'WORKSTATION_CATALOG_APP', 'roles': ['WORKSTATION_CATALOG_ROLE'], 'url': 'https://workstation.theorchard.com/catalog', }, { 'application_id': 'WORKSTATION_ANALYTICS_APP', 'roles': ['WORKSTATION_ANALYTICS_BASE_ROLE'], 'url': 'https://workstation.theorchard.com/analytics', }, { 'application_id': 'SETTINGS_APP', 'roles': ['SETTINGS_BASE_ROLE'], 'url': 'https://settings.theorchard.com', }, ], [ 'WORKSTATION_CATALOG_ROLE', 'WORKSTATION_ANALYTICS_BASE_ROLE', 'SETTINGS_BASE_ROLE', ], Tenant(tenant_uuid='byn-293', tenant_type=TenantType.ACCOUNT), True, ], ], ) @patch('permissions.logic.tenant.g') @patch('permissions.logic.tenant.get_adminable_tenants_for_identity') @patch('permissions.models.tenant.get_identity_tenant_count') @patch('permissions.models.ows_account.get_enabled_tenant_applications') def test_is_update_removing_last_tenant( mock_get_enabled_tenant_applications: MagicMock, mock_get_identity_tenant_count, mock_get_adminable_tenants_for_identity, g_mock: MagicMock, tenant_to_modify: Tenant, roles_to_check: list[str], available_apps: list[dict], current_roles: list[str], accessible_tenant: Tenant, expected_result: bool, app_context, ) -> None: """Test is_update_removing_last_tenant.""" mock_get_enabled_tenant_applications.return_value = available_apps mock_get_identity_tenant_count.return_value = 1 mock_get_adminable_tenants_for_identity.return_value = accessible_tenant result = tenant._is_detaching_all_roles(tenant_to_modify, current_roles, roles_to_check) assert result == expected_result if result: g_mock.log.info.assert_called_once_with( 'All roles attempted to detach from the last tenant.', resources={ 'roles_to_detach': roles_to_check, 'current_roles': current_roles, 'tenant_uuid': tenant_to_modify.tenant_uuid, }, ) @pytest.mark.parametrize( ('adminable_tenants', 'expected_adminable_tenants'), [ # case: normal list including vendor star pytest.param( [ AdminableTenant( tenant=Tenant( tenant_uuid=constants.VENDOR_STAR_UUID, tenant_type=TenantType.ACCOUNT ), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ), AdminableTenant( tenant=Tenant(tenant_uuid='keep-this', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=456, roles=[], uuid='uuid-456', ) ], ), ], [ TenantWithV2Roles( tenant_type=TenantType.SUBACCOUNT, tenant_uuid='keep-this', roles=['SETTINGS_BASE_ROLE'], ) ], id='normal-list-with-vendor-star', ), # case: no tenants pytest.param([], [], id='empty-list'), # case: no vendor star present pytest.param( [ AdminableTenant( tenant=Tenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ), AdminableTenant( tenant=Tenant(tenant_uuid='def-456', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=456, roles=[], uuid='uuid-456', ) ], ), ], [ TenantWithV2Roles( tenant_type=TenantType.ACCOUNT, tenant_uuid='abc-123', roles=['SETTINGS_BASE_ROLE'], ), TenantWithV2Roles( tenant_type=TenantType.SUBACCOUNT, tenant_uuid='def-456', roles=['SETTINGS_BASE_ROLE'], ), ], id='no-vendor-star', ), ], ) @patch('permissions.models.tenant.get_adminable_tenants_for_identity') def test_get_adminable_tenants_for_identity_filters_vendor_star( mock_get_adminable_tenants: MagicMock, adminable_tenants: list[AdminableTenant], expected_adminable_tenants: list[TenantWithV2Roles], ) -> None: """Test get_adminable_tenants_for_identity filters out vendor star and maps v2 roles.""" mock_get_adminable_tenants.return_value = adminable_tenants result = tenant.get_adminable_tenants_for_identity( admin_context={'user': 'admin'}, identity_id='identity-uuid' ) assert result == expected_adminable_tenants mock_get_adminable_tenants.assert_called_once_with( admin_context={'user': 'admin'}, identity_id='identity-uuid', limit=99999, offset=constants.DEFAULT_OFFSET, ) @pytest.mark.parametrize( ('identity_uuids', 'model_results', 'expected_result'), [ # case: multiple identities with tenants pytest.param( [ uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086'), uuid.UUID('97fba315-fa3d-4133-bc49-fc01fddf5671'), ], [ AdminableTenantDataloader( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ AdminableTenant( tenant=Tenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ), ], ), AdminableTenantDataloader( identity_uuid='97fba315-fa3d-4133-bc49-fc01fddf5671', tenants=[ AdminableTenant( tenant=Tenant(tenant_uuid='def-456', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='InsightsProfile', profile_id=456, roles=['analytics'], uuid='uuid-456', ) ], ), ], ), ], [ IdentityTenantsWithV2Roles( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ TenantWithV2Roles( tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT, roles=['SETTINGS_BASE_ROLE'], ) ], ), IdentityTenantsWithV2Roles( identity_uuid='97fba315-fa3d-4133-bc49-fc01fddf5671', tenants=[ TenantWithV2Roles( tenant_uuid='def-456', tenant_type=TenantType.SUBACCOUNT, roles=['INSIGHTS_BASE_ROLE'], ) ], ), ], id='Multiple identities with tenants', ), # case: filters out vendor star tenant pytest.param( [uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086')], [ AdminableTenantDataloader( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ AdminableTenant( tenant=Tenant( tenant_uuid=constants.VENDOR_STAR_UUID, tenant_type=TenantType.ACCOUNT, ), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ), AdminableTenant( tenant=Tenant(tenant_uuid='keep-this', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=456, roles=[], uuid='uuid-456', ) ], ), ], ), ], [ IdentityTenantsWithV2Roles( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ TenantWithV2Roles( tenant_uuid='keep-this', tenant_type=TenantType.ACCOUNT, roles=['SETTINGS_BASE_ROLE'], ) ], ), ], id='Filters out vendor * tenant', ), # case: identity with no tenants returns None pytest.param( [ uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086'), uuid.UUID('97fba315-fa3d-4133-bc49-fc01fddf5671'), ], [ AdminableTenantDataloader( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ AdminableTenant( tenant=Tenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='SettingsProfile', profile_id=123, roles=[], uuid='uuid-123', ) ], ), ], ), ], [ IdentityTenantsWithV2Roles( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ TenantWithV2Roles( tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT, roles=['SETTINGS_BASE_ROLE'], ) ], ), None, ], id='Missing identity tenants returns None', ), # case: empty results pytest.param( [ uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086'), uuid.UUID('97fba315-fa3d-4133-bc49-fc01fddf5671'), ], [], [None, None], id='empty-results', ), # case: multiple profiles converted to v2 roles pytest.param( [uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086')], [ AdminableTenantDataloader( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ AdminableTenant( tenant=Tenant(tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT), profiles=[ ProfileInfo( profile_type='LabelProfile', profile_id=123, roles=['administrator', 'accounting'], uuid='uuid-123', ), ProfileInfo( profile_type='MoneyhubProfile', profile_id=456, roles=['accounting'], uuid='uuid-456', ), ], ), ], ), ], [ IdentityTenantsWithV2Roles( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ TenantWithV2Roles( tenant_uuid='abc-123', tenant_type=TenantType.ACCOUNT, roles=[ 'WORKSTATION_ADMIN_ROLE', 'CUSTOMER_ACCOUNTING_BASE_ROLE', ], ) ], ), ], id='Multiple profiles converted to v2 roles', ), # case: ignores deprecated roles pytest.param( [uuid.UUID('fd7b4385-6f42-4205-add6-4c90cc6ec086')], [ AdminableTenantDataloader( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ AdminableTenant( tenant=Tenant(tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT), profiles=[ ProfileInfo( profile_type='LabelProfile', profile_id=123, roles=['catalog', 'manage_rights'], uuid='uuid-123', ) ], ), ], ), ], [ IdentityTenantsWithV2Roles( identity_uuid='fd7b4385-6f42-4205-add6-4c90cc6ec086', tenants=[ TenantWithV2Roles( tenant_uuid='abc-123', tenant_type=TenantType.SUBACCOUNT, roles=['WORKSTATION_CATALOG_ROLE'], ) ], ), ], id='Exclude deprecated roles', ), ], ) @patch('permissions.models.profile.check_vendor_star_access', return_value=False) @patch('permissions.logic.tenant.vend_contact_role') @patch('permissions.models.tenant.get_adminable_tenants_for_identities') def test_get_adminable_tenants_for_identities_dataloader( mock_get_adminable_tenants: MagicMock, vend_contact_role_mock: MagicMock, mock_check_vendor_star: MagicMock, identity_uuids: list[uuid.UUID], model_results: list[AdminableTenantDataloader], expected_result: list[list[TenantWithV2Roles] | None], ) -> None: """Test get_adminable_tenants_for_identities_dataloader.""" # Mock the model function mock_get_adminable_tenants.return_value = model_results # Needed for label profile administrator role case vend_contact_role_mock.VendContactRole.get_role_ids_by_vend_contact.return_value = [4] admin_context = { 'identity_id': 'admin-uuid', 'profile_id': 99999, } result = tenant.get_adminable_tenants_for_identities_dataloader( identity_uuids=identity_uuids, admin_context=admin_context, is_seater=False, ) assert result == expected_result mock_check_vendor_star.assert_called_once_with( identity_id='admin-uuid', profile_type=constants.SETTINGSPROFILE, profile_id=99999, ) mock_get_adminable_tenants.assert_called_once_with( identity_uuids=identity_uuids, admin_context=admin_context, has_full_catalog_access=False, ) @patch('permissions.models.profile.check_vendor_star_access', return_value=False) @patch('permissions.logic.tenant.vend_contact_role') @patch('permissions.models.tenant.get_adminable_tenants_for_identities') @patch('permissions.models.tenant.get_seater_adminable_tenants_for_identities') def test_get_adminable_tenants_for_identities_dataloader_is_seater( mock_get_seater_adminable_tenants: MagicMock, mock_get_adminable_tenants: MagicMock, vend_contact_role_mock: MagicMock, mock_check_vendor_star: MagicMock, ) -> None: """Test is_seater=True calls seater model function and skips vendor star check.""" mock_get_seater_adminable_tenants.return_value = [] vend_contact_role_mock.VendContactRole.get_role_ids_by_vend_contact.return_value = [4] admin_context = { 'identity_id': 'admin-uuid', 'profile_id': None, } identity_uuids = [uuid.uuid4()] tenant.get_adminable_tenants_for_identities_dataloader( identity_uuids=identity_uuids, admin_context=admin_context, is_seater=True, ) mock_check_vendor_star.assert_not_called() mock_get_adminable_tenants.assert_not_called() mock_get_seater_adminable_tenants.assert_called_once_with( identity_uuids=identity_uuids, admin_context=admin_context, ) @mock.patch('permissions.logic.tenant.map_to_v2_roles') @mock.patch('permissions.logic.tenant.tenant_model.seat_get_tenant_by_uuid') def test_seat_is_update_removing_last_role_tenant_not_found( mock_seat_get_tenant: MagicMock, mock_map_to_v2_roles: MagicMock, ) -> None: """Test seat_is_update_removing_last_role raises ValueError when tenant not found.""" mock_seat_get_tenant.return_value = None with pytest.raises(ValueError, match='Tenant not found for identity.'): tenant.seat_is_update_removing_last_role( identity_id='test-identity-id', tenant_uuid='test-tenant-uuid', roles_to_detach=['INSIGHTS_BASE_ROLE'], ) mock_seat_get_tenant.assert_called_once_with( identity_id='test-identity-id', tenant_uuid='test-tenant-uuid', ) mock_map_to_v2_roles.assert_not_called() @mock.patch('permissions.logic.tenant.g', spec=['log']) @mock.patch('permissions.logic.tenant.map_to_v2_roles') @mock.patch('permissions.logic.tenant.tenant_model.seat_get_tenant_by_uuid') def test_seat_is_update_removing_last_role_returns_true( mock_seat_get_tenant: MagicMock, mock_map_to_v2_roles: MagicMock, _g_mock: MagicMock, ) -> None: """Test seat_is_update_removing_last_role returns True when detaching all roles.""" tenant_obj = Tenant(tenant_uuid='tenant-123', tenant_type=TenantType.PARENT_COMPANY) adminable_tenant = AdminableTenant( tenant=tenant_obj, profiles=[ ProfileInfo( profile_id=2, profile_type='InsightsProfile', roles=['analytics'], uuid='profile-uuid-2', ), ], ) mock_seat_get_tenant.return_value = adminable_tenant mock_map_to_v2_roles.return_value = [ TenantWithV2Roles( tenant_uuid='tenant-123', tenant_type=TenantType.PARENT_COMPANY, roles=['INSIGHTS_BASE_ROLE'], ) ] result = tenant.seat_is_update_removing_last_role( identity_id='test-identity-id', tenant_uuid='tenant-123', roles_to_detach=['INSIGHTS_BASE_ROLE'], ) assert result is True mock_seat_get_tenant.assert_called_once_with( identity_id='test-identity-id', tenant_uuid='tenant-123', ) mock_map_to_v2_roles.assert_called_once_with([adminable_tenant]) @mock.patch('permissions.logic.tenant.map_to_v2_roles') @mock.patch('permissions.logic.tenant.tenant_model.seat_get_tenant_by_uuid') def test_seat_is_update_removing_last_role_returns_false( mock_seat_get_tenant: MagicMock, mock_map_to_v2_roles: MagicMock, ) -> None: """Test seat_is_update_removing_last_role returns False when not detaching all roles.""" tenant_obj = Tenant(tenant_uuid='tenant-456', tenant_type=TenantType.PARENT_COMPANY) adminable_tenant = AdminableTenant( tenant=tenant_obj, profiles=[ ProfileInfo( profile_id=3, profile_type='SettingsProfile', roles=[], uuid='profile-uuid-3', ), ProfileInfo( profile_id=4, profile_type='InsightsProfile', roles=['analytics'], uuid='profile-uuid-4', ), ], ) mock_seat_get_tenant.return_value = adminable_tenant mock_map_to_v2_roles.return_value = [ TenantWithV2Roles( tenant_uuid='tenant-456', tenant_type=TenantType.PARENT_COMPANY, roles=['SETTINGS_BASE_ROLE', 'INSIGHTS_BASE_ROLE'], ) ] result = tenant.seat_is_update_removing_last_role( identity_id='test-identity-id', tenant_uuid='tenant-456', roles_to_detach=['INSIGHTS_BASE_ROLE'], ) assert result is False mock_seat_get_tenant.assert_called_once_with( identity_id='test-identity-id', tenant_uuid='tenant-456', ) mock_map_to_v2_roles.assert_called_once_with([adminable_tenant]) @pytest.mark.parametrize( ('mock_return_value', 'expected_result'), [ pytest.param( MagicMock(), True, id='Account exists', ), pytest.param( None, False, id='Account not found', ), ], ) @patch('permissions.logic.tenant.vendor_model') @patch('permissions.logic.tenant.mysql') def test_does_account_exist_in_art_relations( mock_mysql: MagicMock, mock_vendor_model: MagicMock, mock_return_value: MagicMock | None, expected_result: bool, ) -> None: """Test does_tenant_exist_in_art_relations for accounts.""" mock_session = MagicMock() mock_mysql.db_session.return_value.__enter__.return_value = mock_session tenant_uuid = 'test-account-uuid' test_tenant = Tenant(tenant_type=constants.ACCOUNT_TENANT_TYPE, tenant_uuid=tenant_uuid) if mock_return_value: mock_vendor_model.Vendor.get_by_uuid.return_value = mock_return_value else: mock_vendor_model.Vendor.get_by_uuid.side_effect = orm_exceptions.NoResultFound() result = tenant.does_tenant_exist_in_art_relations(test_tenant) assert result == expected_result mock_vendor_model.Vendor.get_by_uuid.assert_called_once_with( session=mock_session, uuid=tenant_uuid, ) @pytest.mark.parametrize( ('mock_return_value', 'expected_result'), [ pytest.param( MagicMock(), True, id='Subaccount exists', ), pytest.param( None, False, id='Subaccount not found', ), ], ) @patch('permissions.logic.tenant.subaccount_model') @patch('permissions.logic.tenant.mysql') def test_does_subaccount_exist_in_art_relations( mock_mysql: MagicMock, mock_subaccount_model: MagicMock, mock_return_value: MagicMock | None, expected_result: bool, ) -> None: """Test does_tenant_exist_in_art_relations for subaccounts.""" mock_session = MagicMock() mock_mysql.db_session.return_value.__enter__.return_value = mock_session tenant_uuid = 'test-subaccount-uuid' test_tenant = Tenant(tenant_type=constants.SUBACCOUNT_TENANT_TYPE, tenant_uuid=tenant_uuid) if mock_return_value: mock_subaccount_model.get_subaccount_by_uuid.return_value = mock_return_value else: mock_subaccount_model.get_subaccount_by_uuid.side_effect = orm_exceptions.NoResultFound() result = tenant.does_tenant_exist_in_art_relations(test_tenant) assert result == expected_result mock_subaccount_model.get_subaccount_by_uuid.assert_called_once_with( session=mock_session, uuid=tenant_uuid, ) @pytest.mark.parametrize( ('mock_return_value', 'expected_result'), [ pytest.param( MagicMock(), True, id='Parent company exists', ), pytest.param( None, False, id='Parent company not found', ), ], ) @patch('permissions.logic.tenant.parent_company') @patch('permissions.logic.tenant.mysql') def test_does_parent_company_exist_in_art_relations( mock_mysql: MagicMock, mock_parent_company: MagicMock, mock_return_value: MagicMock | None, expected_result: bool, ) -> None: """Test does_tenant_exist_in_art_relations for parent companies.""" mock_session = MagicMock() mock_mysql.db_session.return_value.__enter__.return_value = mock_session tenant_uuid = 'test-parent-company-uuid' test_tenant = Tenant(tenant_type=constants.PARENT_COMPANY_TENANT_TYPE, tenant_uuid=tenant_uuid) if mock_return_value: mock_parent_company.ParentCompany.get_by_uuid.return_value = mock_return_value else: mock_parent_company.ParentCompany.get_by_uuid.side_effect = orm_exceptions.NoResultFound() result = tenant.does_tenant_exist_in_art_relations(test_tenant) assert result == expected_result mock_parent_company.ParentCompany.get_by_uuid.assert_called_once_with( session=mock_session, uuid=tenant_uuid, ) @pytest.mark.parametrize( ('mock_return_value', 'expected_result'), [ pytest.param( MagicMock(), True, id='Company brand exists', ), pytest.param( None, False, id='Company brand not found', ), ], ) @patch('permissions.logic.tenant.company_brand_model') @patch('permissions.logic.tenant.mysql') def test_does_company_brand_exist_in_art_relations( mock_mysql: MagicMock, mock_company_brand_model: MagicMock, mock_return_value: MagicMock | None, expected_result: bool, ) -> None: """Test does_tenant_exist_in_art_relations for company brands.""" mock_session = MagicMock() mock_mysql.db_session.return_value.__enter__.return_value = mock_session tenant_uuid = 'test-company-brand-uuid' test_tenant = Tenant(tenant_type=constants.COMPANY_BRAND_TENANT_TYPE, tenant_uuid=tenant_uuid) if mock_return_value: mock_company_brand_model.CompanyBrand.get_by_uuid.return_value = mock_return_value else: mock_company_brand_model.CompanyBrand.get_by_uuid.side_effect = ( orm_exceptions.NoResultFound() ) result = tenant.does_tenant_exist_in_art_relations(test_tenant) assert result == expected_result mock_company_brand_model.CompanyBrand.get_by_uuid.assert_called_once_with( session=mock_session, uuid=tenant_uuid, ) @pytest.mark.parametrize( ( 'tenant_type', 'neo4j_exists', 'art_relations_exists', 'expected_result', ), [ pytest.param( constants.ACCOUNT_TENANT_TYPE, True, True, True, id='Account exists in both neo4j and art_relations', ), pytest.param( constants.ACCOUNT_TENANT_TYPE, True, False, False, id='Account exists in neo4j but not in art_relations', ), pytest.param( constants.ACCOUNT_TENANT_TYPE, False, None, False, id='Account does not exist in neo4j', ), pytest.param( constants.SUBACCOUNT_TENANT_TYPE, True, True, True, id='Subaccount exists in both neo4j and art_relations', ), pytest.param( constants.SUBACCOUNT_TENANT_TYPE, True, False, False, id='Subaccount exists in neo4j but not in art_relations', ), pytest.param( constants.SUBACCOUNT_TENANT_TYPE, False, None, False, id='Subaccount does not exist in neo4j', ), pytest.param( constants.PARENT_COMPANY_TENANT_TYPE, True, True, True, id='Parent company exists in both neo4j and art_relations', ), pytest.param( constants.PARENT_COMPANY_TENANT_TYPE, True, False, False, id='Parent company exists in neo4j but not in art_relations', ), pytest.param( constants.PARENT_COMPANY_TENANT_TYPE, False, None, False, id='Parent company does not exist in neo4j', ), pytest.param( constants.COMPANY_BRAND_TENANT_TYPE, True, True, True, id='Company brand exists in both neo4j and art_relations', ), pytest.param( constants.COMPANY_BRAND_TENANT_TYPE, True, False, False, id='Company brand exists in neo4j but not in art_relations', ), pytest.param( constants.COMPANY_BRAND_TENANT_TYPE, False, None, False, id='Company brand does not exist in neo4j', ), ], ) @patch('permissions.logic.tenant.does_tenant_exist_in_art_relations') @patch('permissions.logic.tenant.tenant_model') def test_does_tenant_exist_for_art_relations_types( mock_tenant_model: MagicMock, mock_does_tenant_exist_in_art_relations: MagicMock, tenant_type: str, neo4j_exists: bool, art_relations_exists: bool | None, expected_result: bool, ) -> None: """Test does_tenant_exist for all art_relations tenant types.""" tenant_uuid = 'test-tenant-uuid' test_tenant = Tenant(tenant_type=tenant_type, tenant_uuid=tenant_uuid) if neo4j_exists: mock_tenant_model.get_tenant_by_uuid_and_type.return_value = test_tenant else: mock_tenant_model.get_tenant_by_uuid_and_type.side_effect = IncompleteResultError( 'Tenant not found' ) mock_does_tenant_exist_in_art_relations.return_value = art_relations_exists result = tenant.does_tenant_exist(test_tenant) assert result == expected_result mock_tenant_model.get_tenant_by_uuid_and_type.assert_called_once_with( tenant_type=tenant_type, tenant_uuid=tenant_uuid, ) if neo4j_exists: mock_does_tenant_exist_in_art_relations.assert_called_once_with(test_tenant) else: mock_does_tenant_exist_in_art_relations.assert_not_called() @pytest.mark.parametrize( ('neo4j_exists', 'expected_result'), [ pytest.param( True, True, id='Tenant exists', ), pytest.param( False, False, id='Tenant does not exist', ), ], ) @patch('permissions.logic.tenant.does_tenant_exist_in_art_relations') @patch('permissions.logic.tenant.tenant_model') def test_does_tenant_exist_for_non_art_relations_types( mock_tenant_model: MagicMock, mock_does_tenant_exist_in_art_relations: MagicMock, neo4j_exists: bool, expected_result: bool, ) -> None: """Test does_tenant_exist for account and user tenant types.""" tenant_uuid = 'test-tenant-uuid' tenant_type = TenantType.LABEL_PARTICIPANT test_tenant = Tenant(tenant_type=tenant_type, tenant_uuid=tenant_uuid) if neo4j_exists: mock_tenant_model.get_tenant_by_uuid_and_type.return_value = test_tenant else: mock_tenant_model.get_tenant_by_uuid_and_type.side_effect = IncompleteResultError( 'Tenant not found' ) result = tenant.does_tenant_exist(test_tenant) assert result == expected_result mock_tenant_model.get_tenant_by_uuid_and_type.assert_called_once_with( tenant_type=tenant_type, tenant_uuid=tenant_uuid, ) mock_does_tenant_exist_in_art_relations.assert_not_called()