"""Test the Tenant Model.""" import logging from typing import Any, Awaitable, Callable, Dict, List, Optional from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID, uuid4 import pytest from fastapi import HTTPException from httpx import HTTPStatusError, Request, Response from pdp.connectors.ows_account import ( LookupCompanyBrandsResponse, LookupParentCompaniesResponse, LookupSubaccountsResponse, LookupVendorFetchFlags, LookupVendorsResponse, OwsAccountClient, ) from pdp.connectors.ows_participant import ( LookupParticipantsResponse, OwsParticipantClient, ) from pdp.connectors.redis_client import RedisConnector from pdp.constants.constants import TenantType from pdp.fastapi.schemas.tenant import Tenant, TenantHierarchy from pdp.proxies.multi_tenant_proxy import ( MultiTenantProxy, TenantHierarchyLookupError, ) from pdp.utils.tenant_cache import TENANT_HIERARCHY_KEY_PREFIX from tests.unit.fastapi.schemas.test_tenant import ( TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_COMPANY_BRAND, TENANT_LABEL_PARTICIPANT, TENANT_MAYBE_HIERARCHY_ACCOUNT, TENANT_NO_HIERARCHY_LP, TENANT_PARENT_COMPANY, TENANT_SUBACCOUNT, ) @pytest.fixture def list_of_tenants() -> List[Tenant]: """Return test fixture list of tenants.""" return [ TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_SUBACCOUNT, TENANT_COMPANY_BRAND, TENANT_LABEL_PARTICIPANT, TENANT_MAYBE_HIERARCHY_ACCOUNT, TENANT_NO_HIERARCHY_LP, TENANT_PARENT_COMPANY, ] @pytest.fixture def mock_ows_account_client() -> OwsAccountClient: """Mock ows account connector.""" return AsyncMock(spec=OwsAccountClient) @pytest.fixture def mock_ows_participant_client() -> OwsParticipantClient: """Mock ows participant connector.""" return AsyncMock(spec=OwsParticipantClient) @pytest.fixture def mock_redis_connector() -> RedisConnector: """Mock redis-connector.""" return AsyncMock(spec=RedisConnector) @pytest.fixture def mock_multi_tenant_proxy_model( mock_ows_participant_client: OwsParticipantClient, mock_ows_account_client: OwsAccountClient, mock_redis_connector: RedisConnector, list_of_tenants: List[Tenant], ) -> MultiTenantProxy: """Mock MultiTenantProxy model.""" return MultiTenantProxy( tenants=list_of_tenants, redis_client=mock_redis_connector, ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, ) @pytest.mark.parametrize( "description, lookup_response, expected_result", [ ( "Should handle a valid lookup response.", LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 6971, "uuid": str(TENANT_ACCOUNT_1.tenant_uuid), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, None, None, ] }, ), { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy.model_validate( { "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), }, ), ( "Passes through any excess of results from the lookup response.", LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 6971, "uuid": str(TENANT_ACCOUNT_1.tenant_uuid), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, None, None, { "vendor_id": 9999, "uuid": "a9f468fd-e42e-42a7-a54e-a3761b875eec", "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, ] } ), { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy.model_validate( { "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), UUID( "a9f468fd-e42e-42a7-a54e-a3761b875eec" ): TenantHierarchy.model_validate( { "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), }, ), ( "Should handle a valid lookup response with no results.", LookupVendorsResponse.model_validate({"vendors": []}), {}, ), ( "Should handle null result.", None, {}, ), ], ) async def test_get_account_tenant_hierarchies_from_ows_account( description: str, lookup_response: LookupVendorsResponse, expected_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, caplog: Any, ) -> None: """Test the ows-account lookup method.""" mock_ows_account_client.lookup_vendors_by_uuids.return_value = lookup_response result = await ( mock_multi_tenant_proxy_model._get_account_tenant_hierarchies_from_ows_account() ) assert result == expected_result, description mock_ows_account_client.lookup_vendors_by_uuids.assert_called_once_with( uuids=[ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ], fetch_flags=[LookupVendorFetchFlags.TENANT_HIERARCHY], ) if len(expected_result): with caplog.at_level(logging.WARNING): assert "ows-account lookup did not return results" in caplog.text assert ( f"for every requested tenant: {TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid},{TENANT_ACCOUNT_1.tenant_uuid}" # noqa: E501 in caplog.text ), description else: with caplog.at_level(logging.WARNING): assert "ows-account lookup did not return results." in caplog.text @pytest.mark.parametrize( "description, side_effect_error, expected_error", [ ( "Should raise an HTTPException for a 400 error.", HTTPStatusError( request=Request("GET", "https://no.such/endpoint"), response=Response(status_code=401), message="testing...", ), HTTPException, ), ( "Should raise a TenantHierarchyLookupError for a 500 error.", HTTPStatusError( request=Request("GET", "https://no.such/endpoint"), response=Response(status_code=500), message="testing...", ), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError for an unhandled error.", RuntimeError("testing...."), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if company_brand_uuid is null.", [ LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 6971, "uuid": str(TENANT_ACCOUNT_1.tenant_uuid), "company_brand_uuid": None, "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, ] } ), ], TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if parent_company_uuid is null.", [ LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 6971, "uuid": str(TENANT_ACCOUNT_1.tenant_uuid), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": None, }, ] } ), ], TenantHierarchyLookupError, ), ], ) async def test_get_account_tenant_hierarchies_from_ows_account_error( description: str, side_effect_error: BaseException, expected_error: type[BaseException], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, ) -> None: """Test lookup_vendors_by_uuids errors.""" mock_ows_account_client.lookup_vendors_by_uuids.side_effect = AsyncMock( side_effect=side_effect_error ) with pytest.raises(expected_error): await mock_multi_tenant_proxy_model._get_account_tenant_hierarchies_from_ows_account() # noqa: E501 async def test_get_account_tenant_hierarchies_no_accounts( mock_ows_account_client: MagicMock, mock_ows_participant_client: MagicMock, mock_redis_connector: MagicMock, ) -> None: """Test no ows-account lookup occurs when no tenants are Accounts.""" multi_tenant_proxy = MultiTenantProxy( tenants=[ TENANT_SUBACCOUNT, TENANT_COMPANY_BRAND, ], ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, redis_client=mock_redis_connector, ) result = await multi_tenant_proxy._get_account_tenant_hierarchies_from_ows_account() assert result == {}, "Empty dict returned when no tenants are Accounts" mock_ows_account_client.lookup_vendors_by_uuids.assert_not_called() @pytest.mark.parametrize( "description, lookup_response, expected_result", [ ( "Should handle a valid lookup response.", LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 21989, "uuid": str(TENANT_SUBACCOUNT.tenant_uuid), "vendor_uuid": "a46f992b-ee06-4aaf-8765-e3532ae5e8fb", "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", # noqa: E501 }, ] } ), { TENANT_SUBACCOUNT.tenant_uuid: TenantHierarchy.model_validate( { "parent_company": { "tenant_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", "tenant_type": "parent_company", }, "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "account": { "tenant_uuid": "a46f992b-ee06-4aaf-8765-e3532ae5e8fb", "tenant_type": "account", }, } ), }, ), ( "Passes through any excess of results from the lookup response.", LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 21989, "uuid": str(TENANT_SUBACCOUNT.tenant_uuid), "vendor_uuid": "a46f992b-ee06-4aaf-8765-e3532ae5e8fb", "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", # noqa: E501 }, { "subaccount_id": 96843, "uuid": "b41ad9da-9f6d-4aea-95fd-25aff26521c6", "vendor_uuid": "2268cdf4-8896-4e91-8ec6-dc15d0ffefa7", "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", # noqa: E501 }, ] } ), { TENANT_SUBACCOUNT.tenant_uuid: TenantHierarchy.model_validate( { "parent_company": { "tenant_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", "tenant_type": "parent_company", }, "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "account": { "tenant_uuid": "a46f992b-ee06-4aaf-8765-e3532ae5e8fb", "tenant_type": "account", }, } ), UUID( "b41ad9da-9f6d-4aea-95fd-25aff26521c6" ): TenantHierarchy.model_validate( { "parent_company": { "tenant_uuid": "25113cf2-7e33-4545-84d5-9c4825bbb6d2", "tenant_type": "parent_company", }, "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "account": { "tenant_uuid": "2268cdf4-8896-4e91-8ec6-dc15d0ffefa7", "tenant_type": "account", }, } ), }, ), ( "Should handle a valid lookup response with no results.", LookupSubaccountsResponse.model_validate({"subaccounts": []}), {}, ), ], ) async def test_get_subaccount_tenant_hierarchies_from_ows_account( description: str, lookup_response: LookupSubaccountsResponse, expected_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, caplog: Any, ) -> None: """Test the ows-account subaccount lookup method.""" mock_ows_account_client.lookup_subaccounts_by_uuids.return_value = lookup_response result = ( await mock_multi_tenant_proxy_model._get_subaccount_tenant_hierarchies_from_ows_account() # noqa: E501 ) assert result == expected_result, description mock_ows_account_client.lookup_subaccounts_by_uuids.assert_called_once_with( uuids=[TENANT_SUBACCOUNT.tenant_uuid], fetch_flags=[LookupVendorFetchFlags.TENANT_HIERARCHY], ) if not len(expected_result): with caplog.at_level(logging.WARNING): assert "ows-account lookup did not return results." in caplog.text @pytest.mark.parametrize( "description, side_effect_error, expected_error", [ ( "Should raise an HTTPException for a 400 error.", HTTPStatusError( request=Request("GET", "https://no.such/endpoint"), response=Response(status_code=401), message="testing...", ), HTTPException, ), ( "Should raise a TenantHierarchyLookupError for a 500 error.", HTTPStatusError( request=Request("GET", "https://no.such/endpoint"), response=Response(status_code=500), message="testing...", ), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError for an unhandled error.", RuntimeError("testing...."), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if company_brand_uuid is null.", [ LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 58697, "uuid": str(TENANT_SUBACCOUNT.tenant_uuid), "company_brand_uuid": None, "vendor_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", }, ] } ), ], TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if vendor_uuid is null.", [ LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 58697, "uuid": str(TENANT_SUBACCOUNT.tenant_uuid), "vendor_uuid": None, "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 }, ] } ), ], TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if parent_company_uuid is null.", [ LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 58697, "uuid": str(TENANT_SUBACCOUNT.tenant_uuid), "vendor_uuid": uuid4(), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": None, }, ] } ), ], TenantHierarchyLookupError, ), ], ) async def test_get_subaccount_tenant_hierarchies_from_ows_account_error( description: str, side_effect_error: BaseException, expected_error: type[BaseException], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, ) -> None: """Test lookup_subaccounts_by_uuids errors.""" mock_ows_account_client.lookup_subaccounts_by_uuids.side_effect = AsyncMock( side_effect=side_effect_error ) with pytest.raises(expected_error): await mock_multi_tenant_proxy_model._get_subaccount_tenant_hierarchies_from_ows_account() # noqa: E501 async def test_get_subaccount_tenant_hierarchies_no_subaccounts( mock_ows_account_client: MagicMock, mock_ows_participant_client: MagicMock, mock_redis_connector: MagicMock, ) -> None: """Test no ows-account lookup occurs when no tenants are Subaccounts.""" multi_tenant_proxy = MultiTenantProxy( tenants=[ TENANT_ACCOUNT_1, TENANT_COMPANY_BRAND, ], ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, redis_client=mock_redis_connector, ) result = ( await multi_tenant_proxy._get_subaccount_tenant_hierarchies_from_ows_account() ) assert result == {}, "Empty dict returned when no tenants are Subccounts" mock_ows_account_client.lookup_subaccounts_by_uuids.assert_not_called() async def test_multi_tenant_proxy_ignores_duplicate_tenants( mock_ows_account_client: OwsAccountClient, mock_ows_participant_client: OwsParticipantClient, mock_redis_connector: RedisConnector, ) -> None: """Test multi tenant proxy discards duplicate tenants.""" multi_tenant_proxy = MultiTenantProxy( tenants=[ TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_SUBACCOUNT, TENANT_SUBACCOUNT, TENANT_LABEL_PARTICIPANT, TENANT_COMPANY_BRAND, TENANT_ACCOUNT_1, TENANT_COMPANY_BRAND, TENANT_SUBACCOUNT, ], ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, redis_client=mock_redis_connector, ) assert len(multi_tenant_proxy._tenants) == 5 assert multi_tenant_proxy._tenants == [ TENANT_ACCOUNT_2, TENANT_ACCOUNT_1, TENANT_COMPANY_BRAND, TENANT_LABEL_PARTICIPANT, TENANT_SUBACCOUNT, ] async def test_get_tenant_hierarchy_from_cache( mock_ows_account_client: MagicMock, mock_ows_participant_client: MagicMock, mock_redis_connector: MagicMock, ) -> None: """Test cache lookups for tenant hierarchies.""" mock_redis_connector.mget.return_value = [ TenantHierarchy( company_brand=Tenant( tenant_uuid=UUID("a2a40d74-7f44-4fa7-a4b3-b0e1dffb5f9b"), tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, ) ), None, ] multi_tenant_proxy = MultiTenantProxy( tenants=[ Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("8ed044d1-5d3e-495a-accc-9a0d77bc9cdf"), ), Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("86136b29-1449-439e-85ac-0ba399f178f3"), ), ], ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, redis_client=mock_redis_connector, ) result = await multi_tenant_proxy._get_tenant_hierarchy_from_cache( tenant_uuids=[ UUID("8ed044d1-5d3e-495a-accc-9a0d77bc9cdf"), UUID("86136b29-1449-439e-85ac-0ba399f178f3"), ] ) assert result == { UUID("8ed044d1-5d3e-495a-accc-9a0d77bc9cdf"): TenantHierarchy( company_brand=Tenant( tenant_uuid=UUID("a2a40d74-7f44-4fa7-a4b3-b0e1dffb5f9b"), tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, ) ), UUID("86136b29-1449-439e-85ac-0ba399f178f3"): None, } async def test_get_tenant_hierarchy_from_cache_error( mock_ows_account_client: MagicMock, mock_ows_participant_client: MagicMock, mock_redis_connector: MagicMock, ) -> None: """Test _get_tenant_hierarchy_from_cache errors when list of items from cache is not length of tenant_uuids.""" # noqa: E501 # This [] should not happen, but it allows us to test that an Error is raised # if the list lengths do not match mock_redis_connector.mget.return_value = [] multi_tenant_proxy = MultiTenantProxy( tenants=[ Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("8ed044d1-5d3e-495a-accc-9a0d77bc9cdf"), ), Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("86136b29-1449-439e-85ac-0ba399f178f3"), ), ], ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, redis_client=mock_redis_connector, ) with pytest.raises(TenantHierarchyLookupError) as exc_info: await multi_tenant_proxy._get_tenant_hierarchy_from_cache( tenant_uuids=[ UUID("8ed044d1-5d3e-495a-accc-9a0d77bc9cdf"), UUID("86136b29-1449-439e-85ac-0ba399f178f3"), ] ) assert "Redis MGET returned different number of items than requested" in str( exc_info.value ) @patch("pdp.proxies.multi_tenant_proxy.PydanticSchemaSerializer") async def test_save_tenant_hierarchy_to_cache( mock_serializer: MagicMock, mock_multi_tenant_proxy_model: MagicMock, mock_redis_connector: MagicMock, ) -> None: """Test cache lookups for tenant hierarchies.""" cached_tenant_uuid = "8ed044d1-5d3e-495a-accc-9a0d77bc9cdf" not_cached_tenant_uuid = "86136b29-1449-439e-85ac-0ba399f178f3" not_returned_as_cached_tenant_uuid = "0532dd2b-1ee2-4d7a-a105-1f8a05bc0d13" not_requested_tenant_uuid = "a2a40d74-7f44-4fa7-a4b3-b0e1dffb5f9b" mock_redis_connector.mset_with_pipeline.return_value = { f"tenant_hierarchy_{cached_tenant_uuid}": True, f"tenant_hierarchy_{not_cached_tenant_uuid}": False, f"tenant_hierarchy_{not_requested_tenant_uuid}": True, } result = await mock_multi_tenant_proxy_model._save_tenant_hierarchy_to_cache( { UUID(cached_tenant_uuid): TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), UUID(not_cached_tenant_uuid): TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), UUID(not_returned_as_cached_tenant_uuid): TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, "tenant_hierarchy", ) assert len(result) == 3, ( f"Tenant {not_requested_tenant_uuid} was not part of original_request" ) assert result[UUID(cached_tenant_uuid)], "Tenant's mset.return_value was True" assert not result[UUID(not_cached_tenant_uuid)], ( "Tenant's mset.return_value was False" ) assert not result[UUID(not_returned_as_cached_tenant_uuid)], ( "No mset.return_value for this Tenant, default False" ) mock_redis_connector.mset_with_pipeline.assert_called_once_with( { f"tenant_hierarchy_{cached_tenant_uuid}": TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), f"tenant_hierarchy_{not_cached_tenant_uuid}": TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), f"tenant_hierarchy_{not_returned_as_cached_tenant_uuid}": TenantHierarchy( # noqa: E501 company_brand=TENANT_COMPANY_BRAND ), }, serializer=mock_serializer(TenantHierarchy), ) @pytest.mark.parametrize( "description, cache_result, expected_callback_uuids, cb_result, expected_get_tenant_hierarchies_result", # noqa [ ( "3 UUIDs cache miss, lookup 3 UUIDs, lookup response has 3 non-null items, response includes 3 items.", # noqa { TENANT_ACCOUNT_1.tenant_uuid: None, TENANT_ACCOUNT_2.tenant_uuid: None, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: None, }, [ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ], { TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, ), ( "3 UUIDs cache miss, lookup 3 UUIDs, callback response has 1 non-null item, response includes 1 item.", # noqa { TENANT_ACCOUNT_1.tenant_uuid: None, TENANT_ACCOUNT_2.tenant_uuid: None, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: None, }, [ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ], { TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, { TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, ), ( "1 UUID cache hit, lookup 2 UUIDs, callback response has 1 non-null item, response includes 2 items.", # noqa { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: None, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: None, }, [ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, ], { TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, ), ( "3 UUIDs cache hit, callback not called, response includes 2 items.", { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, [], None, { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, ), ( "3 UUIDs cache miss, lookup 3 UUIDs, callback response has 0 items, response includes 0 items.", # noqa { TENANT_ACCOUNT_1.tenant_uuid: None, TENANT_ACCOUNT_2.tenant_uuid: None, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid: None, }, [ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ], {}, {}, ), ], ) async def test_get_tenant_hierarchies( description: str, cache_result: Dict[UUID, bool], expected_callback_uuids: List[UUID], cb_result: Dict[UUID, TenantHierarchy], expected_get_tenant_hierarchies_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, ) -> None: """Test get_tenant_hierarchies.""" mock_multi_tenant_proxy_model._save_tenant_hierarchy_to_cache = AsyncMock( return_value={} ) mock_multi_tenant_proxy_model._get_tenant_hierarchy_from_cache = AsyncMock( return_value=cache_result ) mock_tenant_hierarchy_lookup_cb = AsyncMock(return_value=cb_result) result = await mock_multi_tenant_proxy_model._get_tenant_hierarchies( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_hierarchy_lookup_cb=mock_tenant_hierarchy_lookup_cb, ) if expected_callback_uuids: # Calls callback and saves to cache mock_tenant_hierarchy_lookup_cb.assert_called_with(expected_callback_uuids) mock_multi_tenant_proxy_model._save_tenant_hierarchy_to_cache.assert_called_with( # noqa cb_result, cache_prefix=TENANT_HIERARCHY_KEY_PREFIX ) else: # Does not call callback and saves to cache mock_tenant_hierarchy_lookup_cb.assert_not_called() mock_multi_tenant_proxy_model._save_tenant_hierarchy_to_cache.assert_not_called() # noqa assert result == expected_get_tenant_hierarchies_result, description # All cases search both UUIDs mock_multi_tenant_proxy_model._get_tenant_hierarchy_from_cache.assert_called_with( tenant_uuids=[ TENANT_ACCOUNT_2.tenant_uuid, TENANT_MAYBE_HIERARCHY_ACCOUNT.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ] ) @pytest.mark.parametrize( "description, lookup_response, expected_result", [ ( "Should handle a valid lookup response.", LookupCompanyBrandsResponse.model_validate( { "company_brands": [ { "uuid": str(TENANT_COMPANY_BRAND.tenant_uuid), "company_brand_id": 1, "parent_company_uuid": TENANT_PARENT_COMPANY.tenant_uuid, # noqa: E501 }, ] } ), { TENANT_COMPANY_BRAND.tenant_uuid: TenantHierarchy.model_validate( { "company_brand": None, "parent_company": { "tenant_uuid": str(TENANT_PARENT_COMPANY.tenant_uuid), "tenant_type": "parent_company", }, } ), }, ), ( "Should handle a valid lookup response with no results.", LookupCompanyBrandsResponse.model_validate({"company_brands": []}), {}, ), ( "Passes through any excess of results from the lookup response.", LookupCompanyBrandsResponse.model_validate( { "company_brands": [ None, { "uuid": str(TENANT_COMPANY_BRAND.tenant_uuid), "company_brand_id": 1, "parent_company_uuid": TENANT_PARENT_COMPANY.tenant_uuid, # noqa: E501 }, None, None, { "uuid": "dccef470-3908-4b14-8e94-afa05a487133", "company_brand_id": 2, "parent_company_uuid": TENANT_PARENT_COMPANY.tenant_uuid, # noqa: E501 }, ] } ), { TENANT_COMPANY_BRAND.tenant_uuid: TenantHierarchy.model_validate( { "company_brand": None, "parent_company": { "tenant_uuid": str(TENANT_PARENT_COMPANY.tenant_uuid), "tenant_type": "parent_company", }, } ), UUID( "dccef470-3908-4b14-8e94-afa05a487133" ): TenantHierarchy.model_validate( { "company_brand": None, "parent_company": { "tenant_uuid": str(TENANT_PARENT_COMPANY.tenant_uuid), "tenant_type": "parent_company", }, } ), }, ), ( "Should handle null result.", None, {}, ), ], ) async def test_get_company_brand_tenant_hierarchies( description: str, lookup_response: LookupCompanyBrandsResponse, expected_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, caplog: Any, ) -> None: """Test get_company_brand_tenant_hierarchies.""" mock_ows_account_client.lookup_company_brands_by_uuids.return_value = ( lookup_response ) result = await mock_multi_tenant_proxy_model._get_company_brand_tenant_hierarchies_from_ows_account() # noqa: E501 assert result == expected_result, description @pytest.mark.parametrize( "description, lookup_response, expected_result", [ ( "Should handle a valid lookup response.", LookupParticipantsResponse.model_validate( { "label_participants": [ None, { "vendor_uuid": "d08706fa-0355-46ff-bcb4-d96fe4131e90", "uuid": str(TENANT_LABEL_PARTICIPANT.tenant_uuid), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, ] }, ), { TENANT_LABEL_PARTICIPANT.tenant_uuid: TenantHierarchy.model_validate( { "account": { "tenant_uuid": "d08706fa-0355-46ff-bcb4-d96fe4131e90", "tenant_type": "account", }, "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), }, ), ( "Passes through any excess of results from the lookup response.", LookupParticipantsResponse.model_validate( { "label_participants": [ None, { "vendor_uuid": "d08706fa-0355-46ff-bcb4-d96fe4131e90", "uuid": str(TENANT_LABEL_PARTICIPANT.tenant_uuid), "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, { "vendor_uuid": "5c029ab4-0e09-4b6c-b29d-46c6d5303cf8", "uuid": "a9f468fd-e42e-42a7-a54e-a3761b875eec", "company_brand_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", # noqa: E501 "parent_company_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", # noqa: E501 }, ] } ), { TENANT_LABEL_PARTICIPANT.tenant_uuid: TenantHierarchy.model_validate( { "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "account": { "tenant_uuid": "d08706fa-0355-46ff-bcb4-d96fe4131e90", "tenant_type": "account", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), UUID( "a9f468fd-e42e-42a7-a54e-a3761b875eec" ): TenantHierarchy.model_validate( { "company_brand": { "tenant_uuid": "8cdadb10-73f2-413c-8379-c319a38faf73", "tenant_type": "company_brand", }, "account": { "tenant_uuid": "5c029ab4-0e09-4b6c-b29d-46c6d5303cf8", "tenant_type": "account", }, "parent_company": { "tenant_uuid": "955a1bbd-b623-4ea1-ab5f-8d6620c442fb", "tenant_type": "parent_company", }, } ), }, ), ( "Should handle a valid lookup response with no results.", LookupParticipantsResponse.model_validate({"label_participants": []}), {}, ), ( "Should handle null result.", None, {}, ), ], ) async def test_get_label_participant_tenant_hierarchies_from_ows_participant( description: str, lookup_response: LookupParticipantsResponse, expected_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, mock_ows_participant_client: MagicMock, caplog: Any, ) -> None: """Test the ows-participant label participant lookup method.""" mock_ows_participant_client.lookup_participants_by_uuids.return_value = ( lookup_response ) result = ( await mock_multi_tenant_proxy_model._get_label_participant_tenant_hierarchies_from_ows_participant() # noqa: E501 ) assert result == expected_result, description mock_ows_participant_client.lookup_participants_by_uuids.assert_called_once_with( uuids=[ TENANT_NO_HIERARCHY_LP.tenant_uuid, TENANT_LABEL_PARTICIPANT.tenant_uuid, ], ) if len(expected_result): with caplog.at_level(logging.WARNING): assert ( f"ows-participant lookup did not return results for every requested tenant: {TENANT_NO_HIERARCHY_LP.tenant_uuid}" # noqa: E501 in caplog.text ), description else: with caplog.at_level(logging.WARNING): assert "ows-participant lookup did not return results." in caplog.text @pytest.mark.parametrize( "description, lookup_response, expected_result", [ ( "Should handle a valid lookup response.", LookupParentCompaniesResponse.model_validate( { "parent_companies": [ { "parent_company_id": 1, "uuid": str(TENANT_PARENT_COMPANY.tenant_uuid), # noqa: E501 }, ] } ), { TENANT_PARENT_COMPANY.tenant_uuid: TenantHierarchy.model_validate( { "parent_company": { "tenant_uuid": str(TENANT_PARENT_COMPANY.tenant_uuid), "tenant_type": "parent_company", }, "company_brand": None, } ), }, ), ( "Should handle a valid lookup response with no results.", LookupParentCompaniesResponse.model_validate({"parent_companies": []}), {}, ), ], ) async def test_get_parent_company_tenant_hierarchies_from_ows_account( description: str, lookup_response: LookupParentCompaniesResponse, expected_result: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, mock_ows_account_client: MagicMock, caplog: Any, ) -> None: """Test the ows-account parent company lookup method.""" mock_ows_account_client.lookup_parent_companies_by_uuids.return_value = ( lookup_response # noqa: E501 ) result = ( await mock_multi_tenant_proxy_model._get_parent_company_tenant_hierarchies_from_ows_account() # noqa: E501 ) assert result == expected_result, description mock_ows_account_client.lookup_parent_companies_by_uuids.assert_called_once_with( uuids=[TENANT_PARENT_COMPANY.tenant_uuid], ) if not len(expected_result): with caplog.at_level(logging.WARNING): assert "ows-account lookup did not return results." in caplog.text @pytest.mark.parametrize( "description,account_tenant_results,subaccount_tenant_results,company_brand_tenant_results,parent_company_tenant_results,all_hierarchies", # noqa [ ( "Gathered response should return an empty result.", {}, {}, {}, {}, {}, ), ( "Gathered response should include TENANT_TYPE_ACCOUNT, TENANT_TYPE_SUBACCOUNT, TENANT_COMPANY_BRAND and TENANT_PARENT_COMPANY results.", # noqa { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), }, { TENANT_SUBACCOUNT.tenant_uuid: TenantHierarchy( account=TENANT_ACCOUNT_1, company_brand=TENANT_COMPANY_BRAND ), }, { TENANT_COMPANY_BRAND.tenant_uuid: TenantHierarchy( company_brand=None, parent_company=TENANT_PARENT_COMPANY ), }, { TENANT_PARENT_COMPANY.tenant_uuid: TenantHierarchy( company_brand=None, parent_company=TENANT_PARENT_COMPANY ), }, { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_ACCOUNT_2.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_SUBACCOUNT.tenant_uuid: TenantHierarchy( account=TENANT_ACCOUNT_1, company_brand=TENANT_COMPANY_BRAND ), TENANT_COMPANY_BRAND.tenant_uuid: TenantHierarchy( company_brand=None, parent_company=TENANT_PARENT_COMPANY ), TENANT_PARENT_COMPANY.tenant_uuid: TenantHierarchy( company_brand=None, parent_company=TENANT_PARENT_COMPANY ), }, ), ], ) async def test_gather_tenant_hierarchies( description: str, account_tenant_results: Dict[UUID, TenantHierarchy], subaccount_tenant_results: Dict[UUID, TenantHierarchy], company_brand_tenant_results: Dict[UUID, TenantHierarchy], parent_company_tenant_results: Dict[UUID, TenantHierarchy], all_hierarchies: Dict[UUID, TenantHierarchy], mock_multi_tenant_proxy_model: MagicMock, ) -> None: """Test gather_tenant_hierarchies.""" async def side_effect_cb( tenant_type: TenantType, tenant_hierarchy_lookup_cb: Callable[ [List[UUID]], Awaitable[Dict[UUID, TenantHierarchy]] ], ) -> Dict[UUID, TenantHierarchy]: """Side effect function for gather_tenant_hierarchies.""" # NOTE: Please add results for new tenant types here. if tenant_type == TenantType.TENANT_TYPE_ACCOUNT: return account_tenant_results if tenant_type == TenantType.TENANT_TYPE_SUBACCOUNT: return subaccount_tenant_results if tenant_type == TenantType.TENANT_TYPE_COMPANY_BRAND: return company_brand_tenant_results if tenant_type == TenantType.TENANT_TYPE_PARENT_COMPANY: return parent_company_tenant_results else: return {} mock_multi_tenant_proxy_model._get_tenant_hierarchies = AsyncMock( side_effect=side_effect_cb ) result = await mock_multi_tenant_proxy_model.gather_tenant_hierarchies() assert result == all_hierarchies, description @pytest.mark.parametrize( "description, tenants, expected", [ ( "MultiTenantProxy with accounts, subaccounts, company_brand and parent company. Map should include company_brand and parent company.", # noqa [ TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_SUBACCOUNT, TENANT_COMPANY_BRAND, TENANT_PARENT_COMPANY, ], { TenantType.TENANT_TYPE_ACCOUNT: [ TENANT_ACCOUNT_2.tenant_uuid, TENANT_ACCOUNT_1.tenant_uuid, ], TenantType.TENANT_TYPE_SUBACCOUNT: [ TENANT_SUBACCOUNT.tenant_uuid, ], TenantType.TENANT_TYPE_LABEL_PARTICIPANT: [], TenantType.TENANT_TYPE_COMPANY_BRAND: [ TENANT_COMPANY_BRAND.tenant_uuid, ], TenantType.TENANT_TYPE_PARENT_COMPANY: [ TENANT_PARENT_COMPANY.tenant_uuid, ], }, ), ( "MultiTenantProxy with accounts, subaccounts, and label participant. Map should include all.", # noqa [ TENANT_ACCOUNT_1, TENANT_SUBACCOUNT, TENANT_LABEL_PARTICIPANT, ], { TenantType.TENANT_TYPE_ACCOUNT: [ TENANT_ACCOUNT_1.tenant_uuid, ], TenantType.TENANT_TYPE_SUBACCOUNT: [ TENANT_SUBACCOUNT.tenant_uuid, ], TenantType.TENANT_TYPE_LABEL_PARTICIPANT: [ TENANT_LABEL_PARTICIPANT.tenant_uuid ], TenantType.TENANT_TYPE_COMPANY_BRAND: [], TenantType.TENANT_TYPE_PARENT_COMPANY: [], }, ), ( "MultiTenantProxy initialized with 0 tenants. Map should be empty. ", [], { TenantType.TENANT_TYPE_ACCOUNT: [], TenantType.TENANT_TYPE_SUBACCOUNT: [], TenantType.TENANT_TYPE_LABEL_PARTICIPANT: [], TenantType.TENANT_TYPE_COMPANY_BRAND: [], TenantType.TENANT_TYPE_PARENT_COMPANY: [], }, ), ], ) def test_init_tenants_by_type( description: str, tenants: List[Tenant], expected: Dict[TenantType, List[UUID]], mock_ows_account_client: OwsAccountClient, mock_ows_participant_client: OwsParticipantClient, mock_redis_connector: RedisConnector, ) -> None: """Test _init_tenants_by_type.""" assert ( MultiTenantProxy( tenants=tenants, redis_client=mock_redis_connector, ows_account_client=mock_ows_account_client, ows_participant_client=mock_ows_participant_client, )._tenants_uuids_by_type == expected ), description @pytest.mark.parametrize( "description, tenants, expected", [ ( "A list of one tenant should return that one tenant", [TENANT_ACCOUNT_1], [TENANT_ACCOUNT_1], ), ( "A list of two tenants with the same tenant should return one tenant", [TENANT_ACCOUNT_1, TENANT_ACCOUNT_1], [TENANT_ACCOUNT_1], ), ( "A list of multiple tenants should return list of unique tenants", [ TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_SUBACCOUNT, TENANT_SUBACCOUNT, TENANT_COMPANY_BRAND, ], [ TENANT_ACCOUNT_2, TENANT_ACCOUNT_1, TENANT_COMPANY_BRAND, TENANT_SUBACCOUNT, ], ), ( "A list of unsorted tenants should return list of sorted tenants", [ TENANT_ACCOUNT_1, TENANT_ACCOUNT_2, TENANT_SUBACCOUNT, TENANT_COMPANY_BRAND, ], [ TENANT_ACCOUNT_2, TENANT_ACCOUNT_1, TENANT_COMPANY_BRAND, TENANT_SUBACCOUNT, ], ), ], ) def test_get_unique_tenants( description: str, tenants: List[Tenant], expected: List[Tenant], mock_multi_tenant_proxy_model: MultiTenantProxy, ) -> None: """Test _get_unique_tenants.""" unique_tenant_list = mock_multi_tenant_proxy_model._get_unique_tenants(tenants) assert unique_tenant_list == expected, description @pytest.mark.parametrize( "description, uuid, expected", [ ( "Lookup a UUID found in _gathered_hierarchies.", TENANT_ACCOUNT_1.tenant_uuid, TenantHierarchy(company_brand=TENANT_COMPANY_BRAND), ), ( "Lookup a UUID not found in _gathered_hierarchies.", UUID("903c03ce-1227-11ef-964a-4a2888760682"), None, ), ], ) def test_get_tenant_hierarchy( description: str, uuid: UUID, expected: Optional[TenantHierarchy], mock_multi_tenant_proxy_model: MultiTenantProxy, ) -> None: """Test get_tenant_hierarchy.""" mock_multi_tenant_proxy_model._gathered_hierarchies = { TENANT_ACCOUNT_1.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), TENANT_SUBACCOUNT.tenant_uuid: TenantHierarchy( company_brand=TENANT_COMPANY_BRAND ), } assert mock_multi_tenant_proxy_model.get_tenant_hierarchy(uuid) == expected, ( description ) def test_get_tenant_hierarchy_error( mock_multi_tenant_proxy_model: MultiTenantProxy, ) -> None: """Test get_tenant_hierarchy errors.""" with pytest.raises(TenantHierarchyLookupError): _ = mock_multi_tenant_proxy_model.gathered_hierarchies