"""Tests for auth0 invite logic.""" from unittest import mock import pytest from permissions.constants import application from permissions.logic import auth0_invite from permissions.types import TenantType @pytest.mark.parametrize( ('roles', 'auth0_application_name'), [ # Single role cases that map to specific applications pytest.param([application.INSIGHTS_BASE_ROLE], 'insights-login', id='insights_role_only'), pytest.param( [application.BANKING_TAX_BASE_ROLE], 'documents-login', id='banking_tax_role_only' ), pytest.param( [application.COLLABORATORS_BASE_ROLE], 'collaborators-login', id='collaborators_role_only', ), # Default application cases pytest.param( [application.WORKSTATION_CATALOG_ROLE], 'settings-login', id='workstation_role_defaults_to_settings', ), pytest.param( [application.WORKSTATION_ADMIN_ROLE], 'settings-login', id='workstation_admin_role_defaults_to_settings', ), pytest.param(['unknown_role'], 'settings-login', id='unknown_role_defaults_to_settings'), pytest.param([], 'settings-login', id='empty_roles_defaults_to_settings'), # Multiple roles cases without custom handling default to settings-login pytest.param( [application.INSIGHTS_BASE_ROLE, application.SETTINGS_APP], 'settings-login', id='multiple_apps_defaults_to_settings', ), pytest.param( [application.WORKSTATION_CATALOG_APP, application.FANSIFTER_BASE_ROLE], 'settings-login', id='multiple_apps_defaults_to_settings_2', ), # 2-role cases with custom handling pytest.param( [application.BANKING_TAX_BASE_ROLE, application.CUSTOMER_ACCOUNTING_BASE_ROLE], 'documents-login', id='multiple_apps_custom_join_banking_accounting', ), pytest.param( [application.BANKING_TAX_BASE_ROLE, application.COLLABORATORS_BASE_ROLE], 'documents-login', id='multiple_apps_custom_join_banking_collaborators', ), pytest.param( [application.CUSTOMER_ACCOUNTING_BASE_ROLE, application.COLLABORATORS_BASE_ROLE], 'moneyhub-login', id='multiple_apps_custom_join_accounting_collaborators', ), # 3-role cases with custom handling pytest.param( [ application.CUSTOMER_ACCOUNTING_BASE_ROLE, application.COLLABORATORS_BASE_ROLE, application.BANKING_TAX_BASE_ROLE, ], 'documents-login', id='multiple_apps_custom_join_accounting_collaborators', ), ], ) def test_get_invite_application_name(roles: list[str], auth0_application_name: str): """Test getting the auth0 application name for the invitation link.""" assert auth0_invite.get_invite_application_name(roles) == auth0_application_name @pytest.mark.parametrize( ['vend_contact_present'], [ pytest.param(True, id='with_vend_contact'), pytest.param(False, id='without_vend_contact'), ], ) @mock.patch('permissions.logic.auth0_invite.g') @mock.patch('permissions.logic.auth0_invite.owsusers') @mock.patch('permissions.logic.auth0_invite.tenant') @mock.patch('permissions.logic.auth0_invite.identity_model') @mock.patch('permissions.logic.auth0_invite.get_invite_application_name') def test_send_auth0_invite_parametrized( get_app_name_mock, identity_model_mock, tenant_mock, owsusers_mock, g_mock, vend_contact_present, app_context, ): """Test send_auth0_invite with feature flag enabled/disabled and vend_contact scenarios.""" # Ensure g_mock.log.info is a regular Mock, not AsyncMock g_mock.log.info = mock.Mock() # Setup test data admin_identity = mock.Mock() assignee_identity = mock.Mock(email='test@example.com', id='identity-uuid', name='Test User') user_auth0_id = 'auth0|1234' tenant_type = 'account' tenant_uuid = 'tenant-uuid' roles_to_attach = ['role1', 'role2'] brand = 'theorchard' vend_contact = mock.Mock(id=42) if vend_contact_present else None expected_vend_contact_id = 42 if vend_contact_present else None expected_user_types = ['label'] expected_app_name = 'settings-login' expected_invite_tracking = 'trackstr' expected_tenant_name = 'TenantName' # Setup tenant and tenant_roles_input tenant = mock.Mock() tenant.tenant_type = tenant_type tenant.tenant_uuid = tenant_uuid tenant_roles_input = mock.Mock() tenant_roles_input.tenant = tenant tenant_roles_input.roles_to_attach = roles_to_attach # Setup mocks identity_model_mock.user_types_from_tenant_type.return_value = expected_user_types get_app_name_mock.return_value = expected_app_name owsusers_mock.create_invite_tracking_string.return_value = expected_invite_tracking expected_tenant_obj = mock.Mock( tenant_name=expected_tenant_name, tenant_type=TenantType.ACCOUNT ) tenant_mock.get_tenant_by_uuid_and_type.return_value = expected_tenant_obj # For 'account' type tenants, no inviter lookup should happen # get_tenant_parent_of_tenant won't be called for ACCOUNT type user_metadata_instance = mock.Mock() owsusers_mock.UserMetadata.return_value = user_metadata_instance # Call function auth0_invite.send_auth0_invite( admin_identity=admin_identity, assignee_identity=assignee_identity, user_auth0_id=user_auth0_id, tenant_roles_input=tenant_roles_input, brand=brand, vend_contact=vend_contact, ) expected_invitation_data = {'tenant_name': expected_tenant_name} tenant_mock.get_tenant_by_uuid_and_type.assert_called_with( tenant_uuid=tenant_roles_input.tenant.tenant_uuid, tenant_type=tenant_roles_input.tenant.tenant_type, ) # Verify UserMetadata called with correct parameters owsusers_mock.UserMetadata.assert_called_with( orchardIdentityId=assignee_identity.id, user_types=expected_user_types, username=assignee_identity.name, vend_contact_id=expected_vend_contact_id, type='alw' if vend_contact_present else None, inviteTracking=expected_invite_tracking, auth0_user_id=user_auth0_id, invitation_data=expected_invitation_data, ) # Verify other function calls owsusers_mock.create_auth0_org_invitation.assert_called_with( email=assignee_identity.email, brand=brand, admin=admin_identity, auth0_application_name=expected_app_name, user_metadata=user_metadata_instance, ) g_mock.log.info.assert_any_call( 'Sending org invite', resources={'email': assignee_identity.email, 'brand': brand} ) g_mock.log.info.assert_any_call( 'Successfully sent org invite', resources={'email': assignee_identity.email, 'brand': brand} ) @mock.patch('permissions.models.profile.check_vendor_star_access') @mock.patch('permissions.logic.auth0_invite.tenant') @mock.patch('permissions.logic.auth0_invite.owsusers') @mock.patch('permissions.logic.auth0_invite.get_invite_application_name') @mock.patch('permissions.logic.auth0_invite.identity_model') @mock.patch('permissions.logic.auth0_invite.g') def test_send_auth0_invite_brand_aware_inviter_tenant( g_mock, identity_model_mock, get_app_name_mock, owsusers_mock, tenant_mock, check_vendor_star_access_mock, app_context, ): """Test that inviter tenant lookup uses brand parameter for non-account invitations.""" from permissions.logic.auth0_invite import send_auth0_invite from permissions.types import AdminIdentity, Tenant, TenantRolesInput, TenantType # Setup mocks g_mock.log.info = mock.Mock() g_mock.log.warn = mock.Mock() # Mock check_vendor_star_access to return False (regular user, not Vendor Star) check_vendor_star_access_mock.return_value = False # Mock the tenant parent lookup from permissions.types import TenantWithName tenant_mock.get_tenant_parent_of_tenant.return_value = TenantWithName( tenant_name='Sony Admin Account', tenant_uuid='sony-admin-uuid', tenant_type=TenantType.ACCOUNT, ) # Admin identity with settings profile admin_identity = AdminIdentity( id='admin-id', first_name='Admin', last_name='User', name='Admin User', email='admin@example.com', auth0_user_id='auth0|admin123', active='true', user_types=['admin'], default_brand='theorchard', settings_profile=mock.Mock(profile_id=123), ) assignee_identity = mock.Mock( id='assignee-id', email='assignee@example.com', name='Assignee User' ) # Non-account tenant (should trigger inviter lookup) tenant_roles_input = TenantRolesInput( tenant=Tenant(tenant_type=TenantType.SUBACCOUNT, tenant_uuid='subaccount-uuid'), roles_to_attach=['role1'], roles_to_detach=[], ) brand = 'sonymusic' # Mock tenant.get_tenant_by_uuid_and_type for the target tenant target_tenant_node = mock.Mock( tenant_name='Target Subaccount', tenant_type=TenantType.SUBACCOUNT, tenant_uuid='subaccount-uuid', # Set this explicitly since we're now reusing it ) tenant_mock.get_tenant_by_uuid_and_type.return_value = target_tenant_node identity_model_mock.user_types_from_tenant_type.return_value = ['subaccount'] get_app_name_mock.return_value = 'settings-login' owsusers_mock.create_invite_tracking_string.return_value = 'tracking123' user_metadata_instance = mock.Mock() user_metadata_instance.invitation_data = {} owsusers_mock.UserMetadata.return_value = user_metadata_instance # Call the function send_auth0_invite( admin_identity=admin_identity, assignee_identity=assignee_identity, user_auth0_id=None, tenant_roles_input=tenant_roles_input, brand=brand, vend_contact=None, ) # Verify the tenant parent lookup was called tenant_mock.get_tenant_parent_of_tenant.assert_called_once_with(target_tenant_node) # Verify inviter tenant name was included in invitation_data owsusers_mock.create_auth0_org_invitation.assert_called_once() owsusers_mock.UserMetadata.assert_called_once() call_args = owsusers_mock.UserMetadata.call_args invitation_data = call_args.kwargs['invitation_data'] assert invitation_data['tenant_name'] == 'Target Subaccount' assert invitation_data['inviter_tenant_name'] == 'Sony Admin Account' @mock.patch('permissions.models.profile.check_vendor_star_access') @mock.patch('permissions.logic.auth0_invite.tenant') @mock.patch('permissions.logic.auth0_invite.owsusers') @mock.patch('permissions.logic.auth0_invite.get_invite_application_name') @mock.patch('permissions.logic.auth0_invite.identity_model') @mock.patch('permissions.logic.auth0_invite.g') def test_send_auth0_invite_vendor_star_no_brand_filter( g_mock, identity_model_mock, get_app_name_mock, owsusers_mock, tenant_mock, check_vendor_star_access_mock, app_context, ): """Test that Vendor Star users bypass brand filtering for inviter tenant lookup.""" from permissions.logic.auth0_invite import send_auth0_invite from permissions.types import AdminIdentity, Tenant, TenantRolesInput, TenantType # Setup mocks g_mock.log.info = mock.Mock() g_mock.log.warn = mock.Mock() # Mock check_vendor_star_access to return True (Vendor Star user) check_vendor_star_access_mock.return_value = True # Admin identity with settings profile (Vendor Star user) admin_identity = AdminIdentity( id='vendor-star-admin-id', first_name='Vendor', last_name='Star', name='Vendor Star Admin', email='vendorstar@example.com', auth0_user_id='auth0|vendorstar123', active='true', user_types=['admin'], default_brand='theorchard', settings_profile=mock.Mock(profile_id=456), ) assignee_identity = mock.Mock( id='assignee-id', email='assignee@altafonte.com', name='Assignee User' ) # Non-account tenant (should trigger inviter lookup) tenant_roles_input = TenantRolesInput( tenant=Tenant(tenant_type=TenantType.SUBACCOUNT, tenant_uuid='altafonte-subaccount-uuid'), roles_to_attach=['role1'], roles_to_detach=[], ) brand = 'altafonte' # Mock tenant.get_tenant_by_uuid_and_type for the target tenant target_tenant_node = mock.Mock( tenant_name='Altafonte Subaccount', tenant_type=TenantType.SUBACCOUNT, tenant_uuid='altafonte-subaccount-uuid', ) tenant_mock.get_tenant_by_uuid_and_type.return_value = target_tenant_node # Mock tenant parent lookup to raise exception (no parent found) tenant_mock.get_tenant_parent_of_tenant.side_effect = Exception('Parent not found') identity_model_mock.user_types_from_tenant_type.return_value = ['subaccount'] get_app_name_mock.return_value = 'settings-login' owsusers_mock.create_invite_tracking_string.return_value = 'tracking456' user_metadata_instance = mock.Mock() user_metadata_instance.invitation_data = {} owsusers_mock.UserMetadata.return_value = user_metadata_instance # Call the function send_auth0_invite( admin_identity=admin_identity, assignee_identity=assignee_identity, user_auth0_id=None, tenant_roles_input=tenant_roles_input, brand=brand, vend_contact=None, ) # Verify tenant parent lookup was attempted tenant_mock.get_tenant_parent_of_tenant.assert_called_once_with(target_tenant_node) # Verify invitation_data has tenant name but NO inviter_tenant_name (brand invites) owsusers_mock.create_auth0_org_invitation.assert_called_once() owsusers_mock.UserMetadata.assert_called_once() call_args = owsusers_mock.UserMetadata.call_args invitation_data = call_args.kwargs['invitation_data'] assert invitation_data['tenant_name'] == 'Altafonte Subaccount' assert 'inviter_tenant_name' not in invitation_data # Key: no specific inviter for Vendor Star @mock.patch('permissions.models.profile.check_vendor_star_access') @mock.patch('permissions.logic.auth0_invite.tenant') @mock.patch('permissions.logic.auth0_invite.owsusers') @mock.patch('permissions.logic.auth0_invite.get_invite_application_name') @mock.patch('permissions.logic.auth0_invite.identity_model') @mock.patch('permissions.logic.auth0_invite.g') def test_send_auth0_invite_vendor_star_with_direct_admin_access( g_mock, identity_model_mock, get_app_name_mock, owsusers_mock, tenant_mock, check_vendor_star_access_mock, app_context, ): """Test that Vendor Star users with direct admin access get personal inviter name.""" from permissions.logic.auth0_invite import send_auth0_invite from permissions.types import ( AdminIdentity, Tenant, TenantRolesInput, TenantType, TenantWithName, ) # Setup mocks g_mock.log.info = mock.Mock() g_mock.log.warn = mock.Mock() # Mock check_vendor_star_access to return True (Vendor Star user) check_vendor_star_access_mock.return_value = True # Admin identity with settings profile (Vendor Star user with direct admin access) admin_identity = AdminIdentity( id='vendor-star-admin-id', first_name='VendorStar', last_name='WithAccess', name='VendorStar WithAccess', email='vendorstar-admin@example.com', auth0_user_id='auth0|vendorstar-admin123', active='true', user_types=['admin'], default_brand='theorchard', settings_profile=mock.Mock(profile_id=789), ) assignee_identity = mock.Mock( id='assignee-id', email='assignee@altafonte.com', name='Assignee User' ) tenant_roles_input = TenantRolesInput( tenant=Tenant(tenant_type=TenantType.SUBACCOUNT, tenant_uuid='altafonte-sub-uuid'), roles_to_attach=['role1'], roles_to_detach=[], ) brand = 'altafonte' # Mock tenant.get_tenant_by_uuid_and_type for the target tenant target_tenant_node = mock.Mock( tenant_name='Altafonte Artist Subaccount', tenant_type=TenantType.SUBACCOUNT, tenant_uuid='altafonte-sub-uuid', ) tenant_mock.get_tenant_by_uuid_and_type.return_value = target_tenant_node # Mock tenant parent lookup to return parent (found parent relationship) tenant_mock.get_tenant_parent_of_tenant.return_value = TenantWithName( tenant_name='Altafonte Admin Account', tenant_uuid='altafonte-admin-uuid', tenant_type=TenantType.ACCOUNT, ) identity_model_mock.user_types_from_tenant_type.return_value = ['subaccount'] get_app_name_mock.return_value = 'settings-login' owsusers_mock.create_invite_tracking_string.return_value = 'tracking789' user_metadata_instance = mock.Mock() user_metadata_instance.invitation_data = {} owsusers_mock.UserMetadata.return_value = user_metadata_instance # Call the function send_auth0_invite( admin_identity=admin_identity, assignee_identity=assignee_identity, user_auth0_id=None, tenant_roles_input=tenant_roles_input, brand=brand, vend_contact=None, ) # Verify tenant parent lookup was called tenant_mock.get_tenant_parent_of_tenant.assert_called_once_with(target_tenant_node) # Verify invitation_data has BOTH tenant name AND specific inviter name (not generic) owsusers_mock.create_auth0_org_invitation.assert_called_once() owsusers_mock.UserMetadata.assert_called_once() call_args = owsusers_mock.UserMetadata.call_args invitation_data = call_args.kwargs['invitation_data'] assert invitation_data['tenant_name'] == 'Altafonte Artist Subaccount' assert invitation_data['inviter_tenant_name'] == ( 'Altafonte Admin Account' # Personal, not generic! ) @mock.patch('permissions.logic.auth0_invite.tenant') @mock.patch('permissions.logic.auth0_invite.owsusers') @mock.patch('permissions.logic.auth0_invite.get_invite_application_name') @mock.patch('permissions.logic.auth0_invite.identity_model') @mock.patch('permissions.logic.auth0_invite.g') def test_send_auth0_invite_account_invitation_no_inviter( g_mock, identity_model_mock, get_app_name_mock, owsusers_mock, tenant_mock, app_context, ): """Test account-to-account invitations don't include inviter tenant name.""" from permissions.logic.auth0_invite import send_auth0_invite from permissions.types import AdminIdentity, Tenant, TenantRolesInput, TenantType # Setup mocks g_mock.log.info = mock.Mock() g_mock.log.warn = mock.Mock() admin_identity = AdminIdentity( id='admin-id', first_name='Admin', last_name='User', name='Admin User', email='admin@example.com', auth0_user_id='auth0|admin123', active='true', user_types=['admin'], default_brand='theorchard', settings_profile=mock.Mock(profile_id=123), ) assignee_identity = mock.Mock( id='assignee-id', email='assignee@example.com', name='Assignee User' ) # Account tenant (should NOT trigger inviter lookup) tenant_roles_input = TenantRolesInput( tenant=Tenant(tenant_type=TenantType.ACCOUNT, tenant_uuid='account-uuid'), roles_to_attach=['role1'], roles_to_detach=[], ) brand = 'sonymusic' # Mock tenant.get_tenant_by_uuid_and_type for the target tenant only target_tenant_node = mock.Mock(tenant_name='Target Account', tenant_type=TenantType.ACCOUNT) tenant_mock.get_tenant_by_uuid_and_type.return_value = target_tenant_node # For ACCOUNT type tenants, no inviter lookup should happen # get_tenant_parent_of_tenant won't be called for ACCOUNT type identity_model_mock.user_types_from_tenant_type.return_value = ['account'] get_app_name_mock.return_value = 'settings-login' owsusers_mock.create_invite_tracking_string.return_value = 'tracking123' user_metadata_instance = mock.Mock() user_metadata_instance.invitation_data = {} owsusers_mock.UserMetadata.return_value = user_metadata_instance # Call the function send_auth0_invite( admin_identity=admin_identity, assignee_identity=assignee_identity, user_auth0_id=None, tenant_roles_input=tenant_roles_input, brand=brand, vend_contact=None, ) # Verify tenant parent lookup was NOT called (since it's account type) tenant_mock.get_tenant_parent_of_tenant.assert_not_called() # Verify only tenant_name is in invitation_data, no inviter_tenant_name owsusers_mock.create_auth0_org_invitation.assert_called_once() owsusers_mock.UserMetadata.assert_called_once() call_args = owsusers_mock.UserMetadata.call_args invitation_data = call_args.kwargs['invitation_data'] assert invitation_data['tenant_name'] == 'Target Account' assert 'inviter_tenant_name' not in invitation_data