"""Test for model identities.""" import textwrap from unittest import mock from owsresponse import response import pytest from pythonfeatures import pythonfeatures from tests.unit.conftest import get_session_mock from users import constants from users.models import identities @pytest.mark.parametrize( ('node_details', 'expected_query', 'expected_params'), [ ( { 'node_id': 10, 'labels': ('Identiy',), 'data': { 'identity_id': '1234', 'email': 'test@theorchard.com', 'name': 'test@theorchard.com', }, }, ( 'MERGE (i: Identity {id: $identity_id})\n SET i += $identity_data\n RETURN i as identity', # noqa: E501, ), { 'identity_data': { 'email': 'test@theorchard.com', 'name': 'test@theorchard.com', 'isEmployee': True, }, 'identity_id': '1234', }, ), ( { 'node_id': 11, 'labels': ('Identiy',), 'data': { 'identity_id': '5678', 'email': 'test@theorchard.com', 'name': 'test@theorchard.com', 'google_user_id': '104137916463319005366', }, }, ( 'MERGE (i: Identity {id: $identity_id})\n SET i += $identity_data\n RETURN i as identity', # noqa: E501, ), { 'identity_data': { 'email': 'test@theorchard.com', 'name': 'test@theorchard.com', 'googleUserId': '104137916463319005366', 'isEmployee': True, }, 'identity_id': '5678', }, ), ( { 'node_id': 12, 'labels': ('Identiy',), 'data': { 'identity_id': '910', 'email': 'test@nottheorchard.com', 'name': 'test@nottheorchard.com', 'auth0_user_id': '5c067bc05f37972e07cb605f', }, }, ( 'MERGE (i: Identity {id: $identity_id})\n SET i += $identity_data\n RETURN i as identity', # noqa: E501, ), { 'identity_data': { 'email': 'test@nottheorchard.com', 'name': 'test@nottheorchard.com', 'auth0UserId': '5c067bc05f37972e07cb605f', 'isEmployee': False, }, 'identity_id': '910', }, ), ], ) def test_create_identity(make_graph_node, node_details, expected_query, expected_params, mocker): """Test create_identity.""" db_response = [ { 'identity': make_graph_node( node_id=node_details['node_id'], labels=node_details['labels'], data=node_details['data'], ) } ] session_mock = get_session_mock(db_response) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.create_identity(node_details['data']) assert result session_mock.run.assert_called_once() assert session_mock.run.call_args[0] == expected_query assert session_mock.run.call_args[1] == expected_params @pytest.mark.parametrize( ('node_details', 'expected_query', 'expected_params'), [ # identity found ( { 'node_id': 10, 'labels': ('Identity',), 'data': {'name': 'Test Person', 'email': 'test@test.com'}, }, ( 'MATCH (i:Identity)\n WHERE i.id = $identity_id\n RETURN i as identity', # noqa: E501, ), {'identity_id': 'auth0id'}, ), # identity not found ( None, ( 'MATCH (i:Identity)\n WHERE i.id = $identity_id\n RETURN i as identity', # noqa: E501, ), {'identity_id': 'auth0id'}, ), ], ) def test_get_identity(make_graph_node, node_details, expected_query, expected_params, mocker): """Test get_identity.""" identity_id = 'auth0id' db_response = [] if node_details: db_response = [ { 'identity': make_graph_node( node_id=node_details['node_id'], labels=node_details['labels'], data=node_details['data'], ) } ] session_mock = get_session_mock(db_response) with mocker.patch('users.models.identities.get_session', return_value=session_mock): identities.get_identity(identity_id) session_mock.run.assert_called_once() assert session_mock.run.call_args[0] == expected_query assert session_mock.run.call_args[1] == expected_params # @todo test that the result response codes are correct (200, 404) def test_get_identity_by_auth0_id(make_graph_node, mocker): """Test get_identity_by_auth0_id.""" auth0_id = 'auth0id' db_response = [{'identity': 'node'}] session_mock = get_session_mock(db_response) with mocker.patch('users.models.identities.get_session', return_value=session_mock): identities.get_identity_by_auth0_id(auth0_id) session_mock.run.assert_called_once() assert ( session_mock.run.call_args[0][0] == 'MATCH (i:Identity)\n WHERE i.auth0UserId = $identity_id\n RETURN i as identity' # noqa: E501, ) @pytest.mark.parametrize( ('session_data'), [ # everything is found ( { 'identity': {'foo': 'bar'}, 'company_brand': {'bar': 'baz'}, 'service_tier': {'raz': 'taz'}, } ), # service_tier not found ({'identity': {'foo': 'bar'}, 'company_brand': {'bar': 'baz'}, 'service_tier': None}), # company brand, identity, or profile missing (None), ], ) def test_get_account_session_by_identity(mocker, session_data): """Test get_account_session_by_identity.""" auth0_id = '124abcid' label_profile_id = 54321 session_mock = get_session_mock([session_data]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_account_session_by_identity(auth0_id, label_profile_id) if result: assert result.status == 200 session_mock.run.assert_called_once() args = session_mock.run.call_args assert ( "MATCH (p:Profile{profileType: 'LabelProfile', profileId: $label_profile_id})\n <-[:HAS_PROFILE]-(i:Identity{auth0UserId: $auth0_id})" # noqa: E501, in args[0][0] ) assert 'OPTIONAL MATCH (l)<-[:OWNS]-(v:Vendor)' in args[0][0] assert 'vendor: CASE WHEN v IS NOT NULL THEN v ELSE l END' in args[0][0] assert 'identity: path.identity,' in args[0][0] assert 'company_brand: cb,' in args[0][0] assert 'service_tier: CASE WHEN st IS NOT NULL THEN st ELSE NULL END' in args[0][0] else: assert result.status == 404 assert result.message == constants.ERROR_MESSAGE_IDENTITY_NOT_FOUND @pytest.mark.parametrize( ('update_data', 'expected_params'), [ ( {'identity_id': '123', 'email': 'test@theorchard.com', 'name': 'test'}, {'id': '123', 'update_identity_data': {'email': 'test@theorchard.com', 'name': 'test'}}, ), ( { 'identity_id': '123', 'email': 'test@theorchard.com', 'name': 'test', 'auth0UserId': '123', }, { 'id': '123', 'update_identity_data': { 'email': 'test@theorchard.com', 'name': 'test', 'auth0UserId': '123', }, }, ), ( { 'identity_id': '123', 'email': 'test@theorchard.com', 'name': 'test', 'googleUserId': 'acsdfr5345fsfd34', }, { 'id': '123', 'update_identity_data': { 'email': 'test@theorchard.com', 'name': 'test', 'googleUserId': 'acsdfr5345fsfd34', }, }, ), ], ) def test_update_identity(update_data, expected_params, make_graph_node, mocker): """Test update_identity.""" session_mock = get_session_mock([update_data]) identity_id = '123' with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.update_identity(identity_id, update_data) assert result.status == 200 args = session_mock.run.call_args assert 'MATCH (i:Identity)' in args[0][0] assert 'SET i += $update_identity_data' in args[0][0] assert args[1] == expected_params def test_delete_identity(make_graph_node, mocker): """Test delete_identity.""" identity_id = 'auth0id' session_mock = get_session_mock([]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.delete_identity(identity_id) assert result.status == 204 assert not result.message session_mock.run.assert_called_once() args = session_mock.run.call_args assert 'MATCH (i:Identity)' in args[0][0] assert 'WHERE i.id = $identity_id' in args[0][0] assert 'DETACH DELETE' in args[0][0] assert {'identity_id': 'auth0id'} == args[1] def test_get_identity_by_email(make_graph_node, mocker): """Test get_identity_by_email.""" email = 'test@theorchard.com' session_mock = get_session_mock([{'identity': {'foo': 'bar'}}]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_identity_by_email(email) assert result.status == 200 session_mock.run.assert_called_once() args = session_mock.run.call_args assert ( args[0][0] == 'MATCH (i:Identity)\n WHERE i.email = $email\n RETURN i as identity' # noqa: E501, ) assert args[1] == {'email': email} @mock.patch('users.models.identities.get_session') def test_get_identity_id_by_email_found(session_mock: mock.MagicMock): """Test get_identity_id_by_email when identity is found.""" email = 'test@theorchard.com' identity_id = 'test-identity-uuid' session_mock.return_value.run.return_value.peek.return_value.data.return_value = { 'identity_id': identity_id } result = identities.get_identity_id_by_email(email) assert result == identity_id session_mock.return_value.run.assert_called_once_with( 'MATCH (i:Identity)\n WHERE i.email = $email\n RETURN i.id as identity_id', email=email, ) @mock.patch('users.models.identities.get_session') def test_get_identity_id_by_email_not_found(session_mock: mock.MagicMock): """Test get_identity_id_by_email when identity is not found.""" email = 'notfound@theorchard.com' session_mock.return_value.run.return_value.peek.return_value = None result = identities.get_identity_id_by_email(email) assert result is None session_mock.return_value.run.assert_called_once() def test_update_identity_by_email(make_graph_node, mocker): """Test update_identity_by_email.""" update_data = {'id': 'newId', 'name': 'new name'} session_mock = get_session_mock([{'identity': update_data}]) email = 'foo@theorchard.com' with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.update_identity_by_email(email, update_data) assert result.status == 200 args = session_mock.run.call_args assert args[0] == ( 'MATCH (i:Identity)\n WHERE i.email = $email\n SET i += $update_identity_data\n RETURN i as identity', # noqa: E501, ) assert args[1] == {'email': 'foo@theorchard.com', 'update_identity_data': update_data} # Identity pending status + auth0-id resolution. def test_get_pending_status_true_when_auth0_user_id_equals_id(): """An identity whose auth0UserId equals its own id is pending (un-finished invite).""" node = {'id': 'identity-1', 'auth0UserId': 'identity-1', 'email': 'pending@example.com'} assert identities.get_pending_status(node)['pending'] is True def test_get_pending_status_false_when_linked_to_auth0_user(): """An identity linked to a real Auth0 user (auth0UserId != id) is active, not pending.""" node = {'id': 'identity-1', 'auth0UserId': 'auth0-user-1', 'email': 'active@example.com'} assert identities.get_pending_status(node)['pending'] is False def test_get_identity_by_auth0_id_resolves_by_auth0_user_id(make_graph_node, mocker): """Resolution keys on auth0UserId (never email), so an Auth0 user whose email later changes still resolves to its original identity.""" identity_node = make_graph_node( 'd1230771-6b14-4c11-a15c-d08ea6fc47f5', labels=('Identity',), data={'auth0UserId': 'auth0-user-1', 'email': 'user@example.com'}, ) session_mock = mock.MagicMock() session_mock.run.return_value.peek.return_value.data.return_value = {'identity': identity_node} mocker.patch('users.models.identities.get_session', return_value=session_mock) resolved = identities.get_identity_by_auth0_id('auth0-user-1') query = session_mock.run.call_args[0][0] assert 'i.auth0UserId = $identity_id' in query assert 'email' not in query assert resolved.message['id'] == 'd1230771-6b14-4c11-a15c-d08ea6fc47f5' assert resolved.message['email'] == 'user@example.com' def test_get_identity_for_profile(make_graph_node, mocker): """Test get_identity_for_profile.""" profile_id = 12345 profile_type = 'LabelProfile' session_mock = get_session_mock([{'identity': {'foo': 'bar'}}]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_identity_for_profile(profile_id, profile_type) assert result.status == 200 session_mock.run.assert_called_once() args = session_mock.run.call_args assert ( args[0][0] == 'MATCH (i:Identity)-[:HAS_PROFILE]->(p:Profile)\n WHERE p.profileId = $profile_id AND p.profileType = $profile_type\n RETURN i as identity' # noqa: E501, ) def test_get_identity_for_profile_uuid(make_graph_node, mocker): """Test get_identity_for_profile_uuid.""" profile_uuid = 'some-uuid' session_mock = get_session_mock([{'identity': {'foo': 'bar'}}]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_identity_for_profile_uuid(profile_uuid) assert result.status == 200 session_mock.run.assert_called_once() args = session_mock.run.call_args assert args[0] == ( 'MATCH (i:Identity)-[:HAS_PROFILE]->(p:Profile)\n WHERE p.uuid = $profile_uuid\n RETURN i as identity', # noqa: E501, ) @pytest.mark.parametrize( ( 'identity_id', 'email', 'relation', 'feature_enabled_response', 'expected_status', 'expected_cypher', ), [ ( 'uuid', None, 'HAS_ACCESS_TO', response.Response(message='disabled'), 200, textwrap.dedent( """ OPTIONAL MATCH(:Profile {profileType: 'SettingsProfile', profileId: $adminProfileId}) -[:HAS_ADMIN_ACCESS_TO]->(starVendor:Vendor {id: '*'}) CALL apoc.when( starVendor IS NOT NULL, ' MATCH (i:Identity) WHERE i.id = $identityId AND ( i.id = $adminIdentityId OR NOT (i)-[:HAS_PROFILE]->(:Profile {profileType: "SettingsProfile"})-[:HAS_ADMIN_ACCESS_TO]->(:Vendor {id: "*"}) ) RETURN i as node ', ' MATCH (ap:Profile)-[:HAS_ADMIN_ACCESS_TO]->(x) <-[:HAS_ACCESS_TO|DELETED_HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO|DELETED_HAS_ADMIN_ACCESS_TO]-(up:Profile) <-[:HAS_PROFILE]-(i:Identity) WHERE ap.profileType = $adminProfileType AND ap.profileId = $adminProfileId AND i.id = $identityId RETURN distinct(i) as node UNION MATCH (ap:Profile)-[:HAS_ADMIN_ACCESS_TO]->(x)-[:OWNS|HAS_LABEL_PARTICIPANT]->(s)<-[:HAS_ACCESS_TO]-(up:Profile)<-[:HAS_PROFILE]-(i:Identity) WHERE ap.profileType = $adminProfileType AND ap.profileId = $adminProfileId AND (s:Subaccount OR s:LabelParticipant OR s:Collaborator) AND i.id = $identityId RETURN distinct(i) as node ', { starVendor: starVendor, adminIdentityId: $adminIdentityId, adminProfileId: $adminProfileId, adminProfileType: $adminProfileType, identityId: $identityId, email: $email } ) YIELD value RETURN value.node as identity LIMIT 1""" # noqa ), ), ( None, 'foo@bar.com', 'HAS_ADMIN_ACCESS_TO', response.Response(message='enabled'), 200, textwrap.dedent( """ OPTIONAL MATCH(:Profile {profileType: 'SettingsProfile', profileId: $adminProfileId}) -[:HAS_ADMIN_ACCESS_TO]->(starVendor:Vendor {id: '*'}) CALL apoc.when( starVendor IS NOT NULL, ' MATCH (i:Identity) WHERE i.email = $email AND ( i.id = $adminIdentityId OR NOT (i)-[:HAS_PROFILE]->(:Profile {profileType: "SettingsProfile"})-[:HAS_ADMIN_ACCESS_TO]->(:Vendor {id: "*"}) ) RETURN i as node ', ' MATCH (ap:Profile)-[:HAS_ADMIN_ACCESS_TO]->(x) <-[:HAS_ACCESS_TO|DELETED_HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO|DELETED_HAS_ADMIN_ACCESS_TO]-(up:Profile) <-[:HAS_PROFILE]-(i:Identity) WHERE ap.profileType = $adminProfileType AND ap.profileId = $adminProfileId AND i.email = $email RETURN distinct(i) as node UNION MATCH (ap:Profile)-[:HAS_ADMIN_ACCESS_TO]->(x)-[:OWNS|HAS_LABEL_PARTICIPANT]->(s)<-[:HAS_ACCESS_TO]-(up:Profile)<-[:HAS_PROFILE]-(i:Identity) WHERE ap.profileType = $adminProfileType AND ap.profileId = $adminProfileId AND (s:Subaccount OR s:LabelParticipant OR s:Collaborator) AND i.email = $email RETURN distinct(i) as node ', { starVendor: starVendor, adminIdentityId: $adminIdentityId, adminProfileId: $adminProfileId, adminProfileType: $adminProfileType, identityId: $identityId, email: $email } ) YIELD value RETURN value.node as identity LIMIT 1""" # noqa ), ), (None, None, None, response.Response(message='disabled'), 500, None), ], ) def test_get_identity_for_admin( mocker, identity_id, email, relation, feature_enabled_response, expected_status, expected_cypher ): """Test get_identity_for_admin.""" admin_context = { 'identity_id': 'admin-uuid', 'profile_type': 'InsightsProfile', 'profile_id': 12, } session_mock = get_session_mock([{'identity': {'foo': 'bar', 'relation': relation}}]) mocker.patch.object( pythonfeatures, 'get_single_feature_by_attributes', return_value=feature_enabled_response ) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_identity_for_admin( admin_context, identity_id=identity_id, email=email ) assert result.status == expected_status if expected_status == 200: session_mock.run.assert_called_once() args = session_mock.run.call_args assert args[0][0] == expected_cypher else: assert session_mock.run.call_count == 0 def test_get_identity_vendor(make_graph_node, mocker): """Test get_identity_vendor.""" identity_id = '12345' label_profile_id = 54321 session_mock = get_session_mock([{'name': 'test'}]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): result = identities.get_identity_vendor(identity_id, label_profile_id) assert result.status == 200 session_mock.run.assert_called_once() @pytest.mark.parametrize( ('identity_data', 'expected_query'), [ ( {'email': 'foo@theorchard.com'}, ( 'MATCH (i:Identity {email: $email})\n SET i.updatedOn = $updatedOn\n SET i.updatedBy = $updatedBy\n RETURN i as identity', # noqa: E501, ), ), ( {'identity_id': 'identity_id'}, ( 'MATCH (i:Identity {id: $identityId})\n SET i.updatedOn = $updatedOn\n SET i.updatedBy = $updatedBy\n RETURN i as identity', # noqa: E501, ), ), ( {'auth0_user_id': 'auth0_user_id'}, ( 'MATCH (i:Identity {auth0UserId: $auth0UserId})\n SET i.updatedOn = $updatedOn\n SET i.updatedBy = $updatedBy\n RETURN i as identity', # noqa: E501, ), ), ], ) def test_set_identity_updated_on_date(make_graph_node, mocker, identity_data, expected_query): """Test update_identity_by_email.""" session_mock = get_session_mock([{'identity': identity_data}]) with mocker.patch('users.models.identities.get_session', return_value=session_mock): identities.set_identity_updated_on_date(identity_data, 'admin_identity_id') args = session_mock.run.call_args assert args[0] == expected_query # noqa @pytest.mark.parametrize('term', ['kat dog', 'KAT DOG', 'kat dog']) @mock.patch('users.models.identities.get_session') def test_get_employees_by_search_term(session_mock: mock.MagicMock, term: str) -> None: """Test get_employees_by_search_term.""" session_mock.return_value.run.return_value = [ {'i.id': 'kat-dog-uuid'}, {'i.id': 'kat-dogue-uuid'}, ] result = identities.get_employees_by_search_term(term) assert result == ['kat-dog-uuid', 'kat-dogue-uuid'] session_mock.return_value.run.assert_called_once_with( textwrap.dedent(""" MATCH(i:Identity {isEmployee: true}) WHERE ( toLower(i.email) CONTAINS $search_term OR toLower(i.name) CONTAINS $search_term OR toLower(i.firstName) CONTAINS $search_term OR toLower(i.lastName) CONTAINS $search_term OR toLower(i.firstName + " " + i.lastName) CONTAINS $search_term OR toLower(i.lastName + " " + i.firstName) CONTAINS $search_term ) RETURN i.id """), search_term='kat dog', ) @mock.patch('users.models.identities.get_session') def test_update_identity_organization_invitation(session_mock: mock.MagicMock): """Test update_identity_organization_invitation successfully updates identity.""" email = 'user@theorchard.com' invitation_id = 'uinv_123' organization_id = 'org_abc' admin_identity_id = 'admin_456' organization_name = 'awal' returned_identity = { 'email': email, 'invitationId': invitation_id, 'organizationId': organization_id, 'auth0Invitations': ( '{"awal": {"invitation_id": "uinv_123", "organization_id": "org_abc",' ' "timestamp": "2026-07-06T12:00:00+00:00"}}' ), 'updatedBy': admin_identity_id, } session_mock.return_value.run.return_value.single.return_value = {'identity': returned_identity} result = identities.update_identity_organization_invitation( email=email, invitation_id=invitation_id, organization_id=organization_id, organization_name=organization_name, admin_identity_id=admin_identity_id, ) assert result is not None assert result['email'] == email assert result['invitationId'] == invitation_id assert result['organizationId'] == organization_id assert result['updatedBy'] == admin_identity_id session_mock.return_value.run.assert_called_once() params = session_mock.return_value.run.call_args[1] assert params['email'] == email assert params['invitationId'] == invitation_id assert params['organizationId'] == organization_id assert params['organizationName'] == organization_name assert params['invitationValue']['invitation_id'] == invitation_id assert params['invitationValue']['organization_id'] == organization_id assert 'timestamp' in params['invitationValue'] assert params['updatedBy'] == admin_identity_id assert params['lastModifiedBy'] == admin_identity_id @mock.patch('users.models.identities.get_session') def test_update_identity_organization_invitation_identity_not_found( session_mock: mock.MagicMock, ): """Test update_identity_organization_invitation when identity is not found.""" session_mock.return_value.run.return_value.single.return_value = None result = identities.update_identity_organization_invitation( email='notfound@theorchard.com', invitation_id='uinv_123', organization_id='org_abc', organization_name='awal', admin_identity_id='admin_456', ) assert result is None session_mock.return_value.run.assert_called_once()