"""Test for identity.""" import textwrap import uuid from unittest import mock from unittest.mock import MagicMock, patch import pytest from permissions.connectors import neo4j from permissions.constants import error from permissions.exceptions import incomplete_result_error from permissions.models import identity from permissions.types import Auth0Context, Identity, IdentityWithAuth0, ProfileInfo, TenantType from tests.unit.conftest import get_session_mock def test_get_settings_profile(): """Test get_identity_settings_profile.""" identity_uuid = 'uuid' profile_obj = { 'profileType': 'SettingsProfile', 'profileId': 182392521, 'roles': [], 'uuid': '778f7c28-146b-402f-91ea-7532cd0f0181', } result_node = mock.MagicMock() result_node.__getitem__.side_effect = lambda key, default=None: profile_obj.get(key, default) session_mock = get_session_mock([result_node]) with mock.patch.object(neo4j, 'db_session', return_value=session_mock): actual = identity.get_identity_settings_profile(identity_uuid) assert actual == ProfileInfo( profile_type='SettingsProfile', profile_id=182392521, roles=[], uuid='778f7c28-146b-402f-91ea-7532cd0f0181', ) session_mock.__enter__.return_value.run.assert_called_with( textwrap.dedent( """MATCH (i:Identity {id: $identity_id})-[:HAS_PROFILE]->""" """(p:Profile {profileType: "SettingsProfile"}) RETURN p.profileType as profileType, p.profileId as profileId, p.roles as roles, p.uuid as uuid""" ), {'identity_id': identity_uuid}, ) @pytest.mark.parametrize( ('tenant_type', 'expected_user_types'), [ (TenantType.ACCOUNT, ['label']), (TenantType.SUBACCOUNT, ['label']), (TenantType.COLLABORATOR, []), (TenantType.LABEL_PARTICIPANT, ['artist']), ], ) @mock.patch('permissions.models.owsusers.get_auth0_user_id_by_email') @mock.patch('permissions.models.identity.run_neo4j_identity_create_with_minimal_params') @mock.patch('permissions.connectors.neo4j.db_session') @mock.patch('permissions.models.identity.uuid') def test_create_identity_user_types( uuid_mock: mock.Mock, db_session_mock: mock.Mock, create_mock: mock.Mock, _: mock.Mock, tenant_type: TenantType, expected_user_types: list[str], ): """Test creating an identity with different user_types based on given tenant type.""" uuid_value = '6c69b65a-e5e7-43a6-a629-6307c7fc64e8' uuid_mock.uuid4.return_value = uuid_value session_mock = get_session_mock() db_session_mock.return_value = session_mock params = { 'session': session_mock.__enter__.return_value, 'audit_user_id': 'i-am-a-uuid', 'first_name': 'Skittles', 'last_name': 'Chung', 'email': 'skittles@emailfor.dogs', 'default_brand': 'theorchard', 'tenant_type': tenant_type, 'localization': 'es', } result = identity.create_identity(**params) assert result == create_mock.return_value create_mock.assert_called_with( user_types=expected_user_types, session=session_mock.__enter__.return_value, first_name=params['first_name'], last_name=params['last_name'], email=params['email'], identity_id=uuid_value, audit_user_id=params['audit_user_id'], default_brand=params['default_brand'], localization=params['localization'], is_employee=None, ) @pytest.fixture() def minimal_create_params(): """Return minimal params for creating a neo4j identity.""" return { 'identity_id': 'a-uuid', 'first_name': 'Kat', 'last_name': 'Dog', 'email': 'kat@dachshund.long', 'audit_user_id': 'different-uuid', 'user_types': ['label'], 'default_brand': 'theorchard', 'localization': 'es', } @pytest.mark.parametrize('is_employee', [True, None]) def test_run_neo4j_identity_create_with_minimal_params( is_employee: bool, minimal_create_params: dict ) -> None: """Test creating a neo4j identity, using defaults as necessary.""" session_mock = mock.Mock() identity_dict = { 'id': 'abc', 'firstName': 'Kat', 'lastName': 'Dog', 'name': 'Kat Dog', 'email': 'kat@dog.kat', 'auth0UserId': 'abc', 'userTypes': ['label'], 'defaultBrand': 'theorchard', } session_mock.run.return_value.single.return_value = {'identity': identity_dict} result = identity.run_neo4j_identity_create_with_minimal_params( **minimal_create_params, is_employee=is_employee, session=session_mock, ) # Test that keys in are in snakecase assert result == Identity( id=identity_dict['id'], first_name=identity_dict['firstName'], last_name=identity_dict['lastName'], name=identity_dict['name'], email=identity_dict['email'], auth0_user_id=identity_dict['auth0UserId'], user_types=identity_dict['userTypes'], active='Y', default_brand=identity_dict['defaultBrand'], ) session_mock.run.assert_called_with( textwrap.dedent(""" MERGE (i:Identity {email: $email}) ON CREATE SET i.id = $identityId, i.name = $name, i.auth0UserId = $auth0UserId, i.firstName = $firstName, i.lastName = $lastName, i.localization = $localization, i.numberFormat = $numberFormat, i.active = "Y", i.lastModifiedBy = $auditUser, i.lastModifiedAt = datetime(), i.userTypes = $userTypes, i.defaultBrand = $defaultBrand, i.auth0UserCreatedBy = $auth0UserCreatedBy, i.isEmployee = $isEmployee, i.createdAt = datetime(), i.createdBy = $auditUser RETURN i as identity """), identityId=minimal_create_params['identity_id'], firstName=minimal_create_params['first_name'], lastName=minimal_create_params['last_name'], name=minimal_create_params['first_name'] + ' ' + minimal_create_params['last_name'], email=minimal_create_params['email'], auth0UserId=minimal_create_params['identity_id'], auditUser=minimal_create_params['audit_user_id'], defaultBrand=minimal_create_params['default_brand'], userTypes=minimal_create_params['user_types'], localization='es', numberFormat='us', auth0UserCreatedBy='invitation', isEmployee=is_employee, ) def test_run_neo4j_identity_create_with_minimal_params_error(minimal_create_params): """Test run_neo4j_identity_create_with_minimal_params raises when it encounters an error.""" session_mock = mock.Mock() session_mock.run.return_value.single.return_value = None with pytest.raises(incomplete_result_error.IncompleteResultError): identity.run_neo4j_identity_create_with_minimal_params( **minimal_create_params, session=session_mock, ) create_params = { 'identity_id': 'a-uuid', 'name': 'Kat Dog', 'email': 'kat@dachshund.long', 'auth0_user_id': 'a-uuid', 'audit_user_id': 'different-uuid', 'localization': 'en', 'number_format': 'us', } @pytest.mark.parametrize( ('params', 'run_params'), [ ( # Default values used for all optional params create_params, { 'identityId': 'a-uuid', 'name': 'Kat Dog', 'email': 'kat@dachshund.long', 'auth0UserId': 'a-uuid', 'auditUser': 'different-uuid', 'localization': 'en', 'numberFormat': 'us', 'firstName': None, 'lastName': None, 'userTypes': [], 'defaultBrand': None, 'auth0UserCreatedBy': 'permissions', 'isEmployee': None, }, ), ( # All provided params passed through { **create_params, 'first_name': 'Kat', 'last_name': 'Dog', 'user_types': ['label'], 'default_brand': 'awal', 'auth0_user_created_by': 'invitation', 'is_employee': True, }, { 'identityId': 'a-uuid', 'name': 'Kat Dog', 'email': 'kat@dachshund.long', 'auth0UserId': 'a-uuid', 'auditUser': 'different-uuid', 'localization': 'en', 'numberFormat': 'us', 'firstName': 'Kat', 'lastName': 'Dog', 'userTypes': ['label'], 'defaultBrand': 'awal', 'auth0UserCreatedBy': 'invitation', 'isEmployee': True, }, ), ], ) def test_run_neo4j_identity_create(params: dict[str, str], run_params: dict[str, str]): """Test handling provided/not provided params to run_neo4j_identity_create.""" session_mock = mock.Mock() identity.run_neo4j_identity_create(**params, session=session_mock) session_mock.run.assert_called_with( textwrap.dedent(""" MERGE (i:Identity {email: $email}) ON CREATE SET i.id = $identityId, i.name = $name, i.auth0UserId = $auth0UserId, i.firstName = $firstName, i.lastName = $lastName, i.localization = $localization, i.numberFormat = $numberFormat, i.active = "Y", i.lastModifiedBy = $auditUser, i.lastModifiedAt = datetime(), i.userTypes = $userTypes, i.defaultBrand = $defaultBrand, i.auth0UserCreatedBy = $auth0UserCreatedBy, i.isEmployee = $isEmployee, i.createdAt = datetime(), i.createdBy = $auditUser RETURN i as identity """), **run_params, ) def test_run_neo4j_identity_create_error(): """Test run_neo4j_identity_create raises when it encounters an error.""" session_mock = mock.Mock() session_mock.run.return_value.single.return_value = None with pytest.raises(incomplete_result_error.IncompleteResultError): identity.run_neo4j_identity_create(**create_params, session=session_mock) def test_get_identity_by_email(make_graph_node): """Test get_identity_by_email method.""" email = 'test@test_email@sonymusic-pde.com' session_mock = mock.Mock() session_mock.run.return_value.single.return_value = { 'firstName': 'Test_1', 'lastName': 'User', 'name': 'Test_1 User', 'email': 'test@test_email@sonymusic-pde.com', 'id': 'a396fe3c-26c5-4176-b387-7276d66f6473', 'auth0UserId': 'auth-123', 'userTypes': ['label'], 'active': 'Y', 'defaultBrand': 'awal', } actual = identity.get_identity_by_email(email, session_mock) assert actual == Identity( first_name='Test_1', last_name='User', name='Test_1 User', email='test@test_email@sonymusic-pde.com', id='a396fe3c-26c5-4176-b387-7276d66f6473', auth0_user_id='auth-123', user_types=['label'], active='Y', default_brand='awal', ) @pytest.mark.parametrize( ('get_identity_by_email_result', 'get_auth0_user_organizations_result', 'expected_result'), [ pytest.param( Identity( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='auth0|99143f6bac619b006a2e3cb4', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', default_brand='awal', ), ['awal', 'orchard', 'knr'], IdentityWithAuth0( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='auth0|99143f6bac619b006a2e3cb4', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', auth0_context=Auth0Context( organizations=['awal', 'orchard', 'knr'], auth0_user_id='auth0|99143f6bac619b006a2e3cb4', ), default_brand='awal', ), id='existing identity', ), pytest.param(None, None, None, id='The identity does not exist'), pytest.param( Identity( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='85cd2abc-0443-4572-9df7-0536826f4b01', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', default_brand='awal', ), None, IdentityWithAuth0( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='85cd2abc-0443-4572-9df7-0536826f4b01', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', auth0_context=Auth0Context( organizations=[], auth0_user_id='85cd2abc-0443-4572-9df7-0536826f4b01' ), default_brand='awal', ), id='The identity being pending', ), pytest.param( Identity( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id=None, email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', default_brand='awal', ), None, IdentityWithAuth0( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id=None, email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', auth0_context=Auth0Context(organizations=[], auth0_user_id=None), default_brand='awal', ), id='The identity lacking an auth0UserId field value altogether', ), pytest.param( Identity( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='auth0|99143f6bac619b006a2e3cb4', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', default_brand='awal', ), None, IdentityWithAuth0( id='85cd2abc-0443-4572-9df7-0536826f4b01', auth0_user_id='auth0|99143f6bac619b006a2e3cb4', email='test@test_email@sonymusic-pde.com', first_name='Test', last_name='User', name='Test User', user_types=['label'], active='Y', auth0_context=Auth0Context( organizations=[], auth0_user_id='auth0|99143f6bac619b006a2e3cb4' ), default_brand='awal', ), id='The identity having an auth0 id but not being part of an organization', ), ], ) @patch('permissions.connectors.neo4j._get_neo4j_session') @patch('permissions.models.identity.get_identity_by_email') @patch('permissions.models.owsusers.get_auth0_user_organizations') @patch('permissions.models.identity.g') def test_get_identity_with_auth0( mock_g, get_auth0_user_organizations_mock, get_identity_by_email_mock, get_session_mock, get_identity_by_email_result, get_auth0_user_organizations_result, expected_result, app_context, ): """Test get_identity_with_auth0.""" email = 'test@test_email@sonymusic-pde.com' brand = 'awal' admin = MagicMock(id='555df6b6-661b-4d5b-bc0f-df83cf46eb75') get_auth0_user_organizations_mock.return_value = get_auth0_user_organizations_result get_identity_by_email_mock.return_value = get_identity_by_email_result get_session_mock.return_value = MagicMock() result = identity.get_identity_with_auth0(admin, email, brand) assert result == expected_result @patch('permissions.models.identity.g') def test_get_identity_with_auth0_error(mock_g, app_context): """Test get_identity_with_auth0 error occurring.""" fetch_mock = MagicMock() fetch_mock.fetchone.return_value = None session_mock = MagicMock() session_mock.execute.side_effect = [None, fetch_mock] email = 'test@test_email@sonymusic-pde.com' brand = 'awal' with patch.object(neo4j, 'db_session', return_value=session_mock): try: with pytest.raises( incomplete_result_error.IncompleteResultError, match=error.MESSAGE_GET_IDENTITIES ) as err: identity.get_identity_with_auth0(email, brand) raise err except Exception: assert err @pytest.fixture def mock_identity_record() -> dict: """Return a mock identity record.""" return { 'id': '123-456', 'firstName': 'Test', 'lastName': 'Tester', 'name': 'Test Tester', 'email': 'test@example.com', 'auth0UserId': 'auth0|123456', 'active': 'Y', 'userTypes': ['label'], 'defaultBrand': 'awal', } @pytest.fixture def mock_deactivated_identity_record() -> dict: """Return a mock deactivated identity record.""" return { 'id': '123-456', 'firstName': 'Test', 'lastName': 'Tester', 'name': 'Test Tester', 'email': 'test@example.com', 'auth0UserId': 'auth0|123456', 'active': 'N', 'userTypes': ['label'], 'defaultBrand': 'awal', } @mock.patch('permissions.connectors.neo4j.db_session') def test_get_identity_by_email_found( mock_neo4j_session: MagicMock, mock_identity_record: dict ) -> None: """Test get_identity_by_email.""" email = 'test@example.com' mock_neo4j_session.run.return_value.single.return_value = mock_identity_record result = identity.get_identity_by_email(email, mock_neo4j_session) assert isinstance(result, Identity) assert result.id == '123-456' assert result.first_name == 'Test' assert result.last_name == 'Tester' assert result.name == 'Test Tester' assert result.email == 'test@example.com' assert result.auth0_user_id == 'auth0|123456' assert result.active == 'Y' assert result.user_types == ['label'] mock_neo4j_session.run.assert_called_once() @mock.patch('permissions.connectors.neo4j.db_session') def test_get_identity_by_email_not_found(mock_neo4j_session: MagicMock) -> None: """Test get_identity_by_email when the identity is not found.""" email = 'nonexistent@example.com' mock_neo4j_session.run.return_value.single.return_value = None result = identity.get_identity_by_email(email, mock_neo4j_session) assert result is None mock_neo4j_session.run.assert_called_once() def test_get_identity_by_id_new(mock_identity_record: dict): """Test get_identity_by_id_new.""" identity_id = '123-456' session_mock = get_session_mock([mock_identity_record]) with patch.object(neo4j, 'db_session', return_value=session_mock): result = identity.get_identity_by_id_new(identity_id) assert isinstance(result, Identity) assert result.id == '123-456' assert result.first_name == 'Test' assert result.last_name == 'Tester' assert result.name == 'Test Tester' assert result.email == 'test@example.com' assert result.auth0_user_id == 'auth0|123456' assert result.active == 'Y' assert result.user_types == ['label'] def test_get_identity_by_id_new_not_found() -> None: """Test get_identity_by_id_new when the identity is not found.""" identity_id = 'nonexistent-id' session_mock = get_session_mock([None]) with patch.object(neo4j, 'db_session', return_value=session_mock): result = identity.get_identity_by_id_new(identity_id) assert result is None @mock.patch('permissions.connectors.neo4j.db_session') def test_update_identity_active_status_N( mock_neo4j_session: MagicMock, mock_deactivated_identity_record: dict ) -> None: """Test update_identity_active_status.""" identity_id = 'user-123-456' active = 'N' audit_user_id = 'admin-789-012' mock_neo4j_session.run.return_value.single.return_value = mock_deactivated_identity_record result = identity.update_identity_active_status( mock_neo4j_session, identity_id, active, audit_user_id ) assert isinstance(result, Identity) assert result.active == active mock_neo4j_session.run.assert_called_once() @mock.patch('permissions.connectors.neo4j.db_session') def test_update_identity_active_status_Y( mock_neo4j_session: MagicMock, mock_identity_record: dict ) -> None: """Test update_identity_active_status.""" identity_id = 'user-123-456' active = 'Y' audit_user_id = 'admin-789-012' mock_neo4j_session.run.return_value.single.return_value = mock_identity_record result = identity.update_identity_active_status( mock_neo4j_session, identity_id, active, audit_user_id ) assert isinstance(result, Identity) assert result.active == active mock_neo4j_session.run.assert_called_once_with( textwrap.dedent( """ MATCH (i:Identity {id: $identityId}) SET i.active = $active, i.lastModifiedBy = $auditUser, i.lastModifiedAt = datetime() , i.updatedOn = datetime(), i.auth0UserCreatedBy = 'invitation' RETURN i.id as id, i.firstName as firstName, i.lastName as lastName, i.name as name, i.email as email, i.auth0UserId as auth0UserId, i.active as active, i.defaultBrand as defaultBrand, coalesce(i.userTypes, []) as userTypes """ ), identityId=identity_id, active=active, auditUser=audit_user_id, ) @mock.patch('permissions.connectors.neo4j.db_session') def test_update_identity_default_brand( mock_neo4j_session: MagicMock, mock_identity_record: dict ) -> None: """Test update_identity_default_brand.""" identity_id = 'user-123-456' default_brand = 'awal' audit_user_id = 'admin-789-012' mock_neo4j_session.run.return_value.single.return_value = mock_identity_record result = identity.update_identity_default_brand( mock_neo4j_session, identity_id, default_brand, audit_user_id ) assert isinstance(result, Identity) assert result.id == mock_identity_record['id'] assert result.first_name == mock_identity_record['firstName'] assert result.last_name == mock_identity_record['lastName'] assert result.name == mock_identity_record['name'] assert result.email == mock_identity_record['email'] assert result.auth0_user_id == mock_identity_record['auth0UserId'] assert result.active == mock_identity_record['active'] assert result.user_types == mock_identity_record['userTypes'] mock_neo4j_session.run.assert_called_once() # Verify the cypher query and parameters call_args = mock_neo4j_session.run.call_args query = call_args[0][0] params = call_args[1] assert 'SET i.defaultBrand = $defaultBrand' in query assert 'i.lastModifiedBy = $adminIdentity' in query assert 'i.lastModifiedAt = datetime()' in query assert params['identityId'] == identity_id assert params['defaultBrand'] == default_brand assert params['adminIdentity'] == audit_user_id @mock.patch('permissions.connectors.neo4j.db_session') def test_update_identity_default_brand_error(mock_neo4j_session: MagicMock) -> None: """Test update_identity_default_brand raises error when identity not found.""" identity_id = 'nonexistent-id' default_brand = 'awal' audit_user_id = 'admin-789-012' mock_neo4j_session.run.return_value.single.return_value = None with pytest.raises(incomplete_result_error.IncompleteResultError) as exc_info: identity.update_identity_default_brand( mock_neo4j_session, identity_id, default_brand, audit_user_id ) assert f'Failed to update Identity {identity_id} default brand.' in str(exc_info.value) @pytest.mark.parametrize( 'mock_data, none_result', [ pytest.param( { 'profileId': 12345, 'profileType': 'MoneyhubProfile', 'roles': ['accounting'], 'uuid': 'profile-uuid-123', }, True, id='profile_found', ), pytest.param(None, False, id='profile_not_found'), ], ) @patch('permissions.models.identity.neo4j_connector.db_session') def test_get_profile_by_identity_id_and_profile_id_and_type( mock_db_session: MagicMock, mock_data: dict | None, none_result: bool, ) -> None: """Test get_profile_by_identity_id_and_profile_id_and_type.""" # set up test data identity_id = uuid.uuid4() profile_id = 12345 profile_type = 'MoneyhubProfile' # set up mock session mock_session = MagicMock() mock_db_session.return_value.__enter__.return_value = mock_session # configure the mock behavior based on the test case if mock_data is None: # mock the profile not found case mock_session.run.return_value.single.return_value = None else: # mock the record with profile data mock_record = MagicMock() mock_record.__getitem__.return_value = mock_data mock_session.run.return_value.single.return_value = mock_record result = identity.get_profile_by_identity_id_and_profile_id_and_type( identity_id=identity_id, profile_id=profile_id, profile_type=profile_type ) # assert result matches expected if none_result: assert result is not None assert isinstance(result, ProfileInfo) assert result.profile_id == mock_data['profileId'] assert result.profile_type == mock_data['profileType'] assert result.roles == mock_data['roles'] assert result.uuid == mock_data['uuid'] else: assert result is None # test session query was called mock_session.run.assert_called_once() # test the query was called with correct cypher call_args = mock_session.run.call_args[0] expected_query = """ MATCH (i:Identity {id: $identity_id})-[:HAS_PROFILE]-> (p:Profile {profileId: toInteger($profile_id), profileType: $profile_type}) RETURN p """.strip() actual_query = call_args[0].strip() assert actual_query == expected_query # test the query parameters actual_params = call_args[1] assert actual_params['identity_id'] == str(identity_id) assert actual_params['profile_id'] == profile_id assert actual_params['profile_type'] == profile_type def test_get_identities_employee_status(): """Test get_identities_employee_status returns correct mapping.""" uuid1 = 'uuid-1' uuid2 = 'uuid-2' record1 = MagicMock() record1.__getitem__ = lambda self, key: {'id': uuid1, 'is_employee': True}[key] record2 = MagicMock() record2.__getitem__ = lambda self, key: {'id': uuid2, 'is_employee': False}[key] session_mock = get_session_mock([record1, record2]) with mock.patch.object(neo4j, 'db_session', return_value=session_mock): result = identity.get_identities_employee_status([uuid1, uuid2]) assert result == {uuid1: True, uuid2: False} session_mock.__enter__.return_value.run.assert_called_once() call_args = session_mock.__enter__.return_value.run.call_args assert call_args[0][1] == {'identityUuids': [uuid1, uuid2]} def test_get_identities_employee_status_empty(): """Test get_identities_employee_status with empty list.""" session_mock = get_session_mock([]) with mock.patch.object(neo4j, 'db_session', return_value=session_mock): result = identity.get_identities_employee_status([]) assert result == {}