"""Tests for UuidToIdExchangeTenantProxy.""" import uuid from typing import Any, Awaitable, Callable, Dict, List, Optional from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID import pytest from fastapi import HTTPException from httpx import HTTPStatusError, Request, Response from pdp.connectors.ows_account import ( LookupCompanyBrandsResponse, LookupSubaccountsResponse, LookupVendorFetchFlags, LookupVendorsResponse, OwsAccountClient, ) from pdp.connectors.redis_client import RedisConnector from pdp.constants.constants import TenantType from pdp.fastapi.schemas.tenant import ( IdExchangeTenantHierarchy, Tenant, UuidToIdExchangeTenant, ) from pdp.proxies.multi_tenant_proxy import TenantHierarchyLookupError from pdp.proxies.uuid_to_id_exchange_tenant_proxy import ( UuidToIdExchangeLookupError, UuidToIdExchangeTenantProxy, ) from pdp.utils.tenant_cache import TENANT_HIERARCHY_CACHE_TTL_BY_TENANT_TYPE UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1 = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("fff741c2-6def-4493-bfdf-c2bcb1128e02"), ) UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2 = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("f39efd58-82f5-4257-859a-36e29fa69cda"), ) UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3 = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("3f21f411-5d7e-4205-814e-c2f56d58d890"), ) UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1 = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_SUBACCOUNT, tenant_uuid=UUID("ea035856-b346-11ef-9531-3e17271fba70"), ) UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2 = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_SUBACCOUNT, tenant_uuid=UUID("2c03bd86-ba0d-42a6-bcaf-a273861412a8"), ) UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND = UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=UUID("fb22886e-5cfc-4092-991c-62acfa673c12"), ) UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1 = IdExchangeTenantHierarchy( tenant_id="556790", tenant_type=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_type, tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2 = IdExchangeTenantHierarchy( tenant_id="23496", tenant_type=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_type, tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3 = IdExchangeTenantHierarchy( tenant_id="200697", tenant_type=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_type, tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid, company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) @pytest.fixture def mock_redis_connector() -> RedisConnector: """Mock RedisConnector.""" return AsyncMock(spec=RedisConnector) @pytest.fixture() def list_of_tenants() -> List[UuidToIdExchangeTenant]: """Return test fixture list of UuidToIdExchangeTenant.""" return [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, ] @pytest.fixture def mock_uuid_exchange_proxy( mock_redis_connector: RedisConnector, mock_ows_account_client: OwsAccountClient, list_of_tenants: List[UuidToIdExchangeTenant], ) -> UuidToIdExchangeTenantProxy: """Mock UuidToIdExchangeTenantProxy.""" return UuidToIdExchangeTenantProxy( tenants=list_of_tenants, redis_connector=mock_redis_connector, ows_account_client=mock_ows_account_client, ) @pytest.mark.parametrize( "gathered_tenant_exchanges_value, expect_error, expected", [ pytest.param(None, True, None), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1 # noqa: E501 }, False, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1 # noqa: E501 }, ), ], ) def test_gathered_tenant_exchanges( gathered_tenant_exchanges_value: Any, expect_error: bool, expected: Any, mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, ) -> None: """Test gathered_tenant_exchanges.""" if expect_error: with pytest.raises(UuidToIdExchangeLookupError): _ = mock_uuid_exchange_proxy.gathered_tenant_exchanges else: mock_uuid_exchange_proxy._gathered_tenant_exchanges = ( gathered_tenant_exchanges_value ) assert mock_uuid_exchange_proxy.gathered_tenant_exchanges == expected @pytest.mark.parametrize( "description, tenants, expected", [ ( "A list of one tenant should return that one tenant", [UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1], [UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1], ), ( "A list of multiple tenants should return list of unique tenants", [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, ], [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, ], ), ( "A list of unsorted tenants should return list of sorted tenants", [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, ], [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, ], ), ], ) def test__get_unique_tenants( description: str, tenants: List[UuidToIdExchangeTenant], expected: List[UuidToIdExchangeTenant], mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, ) -> None: """Test _get_unique_tenants.""" unique_tenant_list = mock_uuid_exchange_proxy._get_unique_tenants(tenants) assert unique_tenant_list == expected, description @pytest.mark.parametrize( "uuid, gathered_tenant_exchanges_data, expected_result", [ pytest.param( UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, {}, None, id="Handle when no tenants have been gathered", ), pytest.param( UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, {UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: "fake exchange"}, "fake exchange", id="Handle when a tenant uuid is in the gathered data", ), pytest.param( uuid.uuid1(), {UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: "no return"}, None, id="Handle when a tenant uuid is not in the gathered data", ), ], ) def test_get_tenant_exchange( uuid: uuid.UUID, gathered_tenant_exchanges_data: Dict[uuid.UUID, Any], expected_result: Any, mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, ) -> None: """Test get_tenant_exchange.""" mock_uuid_exchange_proxy._gathered_tenant_exchanges = gathered_tenant_exchanges_data assert mock_uuid_exchange_proxy.get_tenant_exchange(uuid) == expected_result def test_get_unique_tenants( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, ) -> None: """Test _get_unique_tenants.""" assert mock_uuid_exchange_proxy._get_unique_tenants(tenants=[]) == [] def test__init_tenants_by_type( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, ) -> None: """Test _init_tenants_by_type.""" assert mock_uuid_exchange_proxy._init_tenants_by_type() == { TenantType.TENANT_TYPE_ACCOUNT: [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, ], TenantType.TENANT_TYPE_SUBACCOUNT: [ UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1, ], TenantType.TENANT_TYPE_COMPANY_BRAND: [ UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, ], } @pytest.mark.parametrize( "uuids, uuids_to_be_called, side_effect, expected_result, tenants_by_type", [ pytest.param( [ "44514bbd-7837-49f5-baf0-892bd88fa561", "4378cd91-7852-49eb-b39f-653dec222db8", ], [ "44514bbd-7837-49f5-baf0-892bd88fa561", "4378cd91-7852-49eb-b39f-653dec222db8", ], lambda uuids, fetch_flags=[]: LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 1, "uuid": "44514bbd-7837-49f5-baf0-892bd88fa561", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 "parent_company_uuid": "631a9c4c-8f60-4f64-b4b9-46b42efb15b4", # noqa: E501 }, { "vendor_id": 2, "uuid": "4378cd91-7852-49eb-b39f-653dec222db8", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 "parent_company_uuid": "631a9c4c-8f60-4f64-b4b9-46b42efb15b4", # noqa: E501 }, ] } ), { UUID("44514bbd-7837-49f5-baf0-892bd88fa561"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=1, tenant_uuid=UUID("44514bbd-7837-49f5-baf0-892bd88fa561"), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=UUID("547e07ac-a421-4cb9-96ce-81d7fba67815"), ), parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("631a9c4c-8f60-4f64-b4b9-46b42efb15b4"), ), ), UUID("4378cd91-7852-49eb-b39f-653dec222db8"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=2, tenant_uuid=UUID("4378cd91-7852-49eb-b39f-653dec222db8"), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=UUID("547e07ac-a421-4cb9-96ce-81d7fba67815"), ), parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("631a9c4c-8f60-4f64-b4b9-46b42efb15b4"), ), ), }, False, id="""Use provided tenant_uuids instead of the ones created on instantiating the proxy. Return the UUIDs and IdExchangeTenantHierarchy dictionary.""", ), pytest.param( [], [ UUID("3f21f411-5d7e-4205-814e-c2f56d58d890"), UUID("f39efd58-82f5-4257-859a-36e29fa69cda"), UUID("fff741c2-6def-4493-bfdf-c2bcb1128e02"), ], None, {}, False, id="""Use tenant_uuids from instantiating the proxy when no tenant_uuids are provided. Return empty dict when ows-account callable returns nothing. """, ), pytest.param( [], [], None, {}, {TenantType.TENANT_TYPE_ACCOUNT: []}, id="Should return an empty dict when there are no accounts", ), ], ) async def test__exchange_accounts_from_ows_account( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, uuids: List[uuid.UUID], uuids_to_be_called: Optional[List[uuid.UUID]], side_effect: Callable[ [List[str], List[str]], Dict[TenantType, List[UuidToIdExchangeTenant]] ], expected_result: Dict[uuid.UUID, IdExchangeTenantHierarchy], tenants_by_type: Dict[TenantType, List[UuidToIdExchangeTenant]], ) -> None: """Test _exchange_accounts_from_ows_account.""" mock_ows_account_client.lookup_vendors_by_uuids.side_effect = side_effect if tenants_by_type: mock_uuid_exchange_proxy._tenants_by_type = tenants_by_type result = await mock_uuid_exchange_proxy._exchange_accounts_from_ows_account( tenant_uuids=uuids ) assert result == expected_result if not uuids_to_be_called: mock_ows_account_client.lookup_vendors_by_uuids.assert_not_called() else: mock_ows_account_client.lookup_vendors_by_uuids.assert_called_once_with( uuids=uuids_to_be_called, fetch_flags=[LookupVendorFetchFlags.TENANT_HIERARCHY], ) async def test__exchange_accounts_from_ows_account__missing_parent_company( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, ) -> None: """ Verify that `_exchange_accounts_from_ows_account` raises `TenantHierarchyLookupError` when the FF is enabled and the ows-account response does not include a parent company uuid. """ mock_ows_account_client.lookup_vendors_by_uuids.return_value = ( LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 1, "uuid": "44514bbd-7837-49f5-baf0-892bd88fa561", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 }, ] } ) ) mock_uuid_exchange_proxy._tenants_by_type = {} with pytest.raises(TenantHierarchyLookupError) as exc_info: await mock_uuid_exchange_proxy._exchange_accounts_from_ows_account( tenant_uuids=[UUID("44514bbd-7837-49f5-baf0-892bd88fa561")] ) assert ( "tenant_hierarchy missing parent_company_uuid for: '44514bbd-7837-49f5-baf0-892bd88fa561'" # noqa: E501 in str(exc_info.value) ) @pytest.mark.parametrize( "description, side_effect_error, expected_error", [ ( "Should raise an HTTPException for a 400 error.", HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=401), message="Bad request to ows-account for tenant proxy.", ), HTTPException, ), ( "Should raise an HTTPException for a 500 error.", HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=500), message="Account lookup by tenant_uuids failed for tenant proxy.", ), Exception, ), ( "Should raise a TenantHierarchyLookupError for an unhandled error.", RuntimeError("Unhandled error."), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if company_brand_uuid is None.", [ LookupVendorsResponse.model_validate( { "vendors": [ { "vendor_id": 1, "uuid": str( UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid ), "company_brand_uuid": None, }, ] } ), ], TenantHierarchyLookupError, ), ], ) async def test__exchange_accounts_from_ows_account_fails( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, description: str, side_effect_error: BaseException, expected_error: type[BaseException], tenant_1_uuid: uuid.UUID, ) -> None: """Test _exchange_accounts_from_ows_account fails.""" mock_ows_account_client.lookup_vendors_by_uuids.side_effect = AsyncMock( side_effect=side_effect_error ) with pytest.raises(expected_error): await mock_uuid_exchange_proxy._exchange_accounts_from_ows_account( tenant_uuids=[tenant_1_uuid] ) @pytest.mark.parametrize( "uuids, uuids_to_be_called, side_effect, expected_result, tenants_by_type", [ pytest.param( [ "a9dd9b42-e53d-11ee-be6d-4a2888760682", "4378cd91-7852-49eb-b39f-653dec222db8", ], [ "a9dd9b42-e53d-11ee-be6d-4a2888760682", "4378cd91-7852-49eb-b39f-653dec222db8", ], lambda uuids, fetch_flags=[]: LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 998, "uuid": "a9dd9b42-e53d-11ee-be6d-4a2888760682", "vendor_uuid": "db65e460-8539-4035-87af-63391efc5a19", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 "parent_company_uuid": "631a9c4c-8f60-4f64-b4b9-46b42efb15b4", # noqa: E501 }, { "subaccount_id": 999, "uuid": "4378cd91-7852-49eb-b39f-653dec222db8", "vendor_uuid": "db65e460-8539-4035-87af-63391efc5a19", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 "parent_company_uuid": "631a9c4c-8f60-4f64-b4b9-46b42efb15b4", # noqa: E501 }, ] } ), { UUID("a9dd9b42-e53d-11ee-be6d-4a2888760682"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_SUBACCOUNT, tenant_id=998, tenant_uuid=UUID("a9dd9b42-e53d-11ee-be6d-4a2888760682"), account=Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("db65e460-8539-4035-87af-63391efc5a19"), ), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=UUID("547e07ac-a421-4cb9-96ce-81d7fba67815"), ), parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("631a9c4c-8f60-4f64-b4b9-46b42efb15b4"), ), ), UUID("4378cd91-7852-49eb-b39f-653dec222db8"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_SUBACCOUNT, tenant_id=999, tenant_uuid=UUID("4378cd91-7852-49eb-b39f-653dec222db8"), account=Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid=UUID("db65e460-8539-4035-87af-63391efc5a19"), ), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=UUID("547e07ac-a421-4cb9-96ce-81d7fba67815"), ), parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("631a9c4c-8f60-4f64-b4b9-46b42efb15b4"), ), ), }, False, id="""Use provided subaccount tenant_uuids instead of the ones created on instantiating the proxy. Return the UUIDs and IdExchangeTenantHierarchy dictionary.""", ), pytest.param( [], [ UUID("2c03bd86-ba0d-42a6-bcaf-a273861412a8"), UUID("ea035856-b346-11ef-9531-3e17271fba70"), ], None, {}, False, id="""Use subaccount tenant_uuids from instantiating the proxy when no tenant_uuids are provided. Return empty dict when ows-account callable returns nothing.""", ), pytest.param( [], [], None, {}, {TenantType.TENANT_TYPE_SUBACCOUNT: []}, id="Should return an empty dict when there are no subaccounts", ), ], ) async def test__exchange_subaccounts_from_ows_account( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, uuids: List[uuid.UUID], uuids_to_be_called: Optional[List[uuid.UUID]], side_effect: Callable[ [List[str], List[str]], Dict[TenantType, List[UuidToIdExchangeTenant]] ], expected_result: Dict[uuid.UUID, IdExchangeTenantHierarchy], tenants_by_type: Dict[TenantType, List[UuidToIdExchangeTenant]], ) -> None: """Test _exchange_subaccounts_from_ows_account.""" mock_ows_account_client.lookup_subaccounts_by_uuids.side_effect = side_effect if tenants_by_type: mock_uuid_exchange_proxy._tenants_by_type = tenants_by_type result = await mock_uuid_exchange_proxy._exchange_subaccounts_from_ows_account( tenant_uuids=uuids ) assert result == expected_result if not uuids_to_be_called: mock_ows_account_client.lookup_subaccounts_by_uuids.assert_not_called() else: mock_ows_account_client.lookup_subaccounts_by_uuids.assert_called_once_with( uuids=uuids_to_be_called, fetch_flags=[LookupVendorFetchFlags.TENANT_HIERARCHY], ) async def test__exchange_subaccounts_from_ows_account__missing_parent_company( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, ) -> None: """ Verify that `_exchange_subaccounts_from_ows_account` raises `TenantHierarchyLookupError` when the FF is enabled and the ows-account response does not include a parent company uuid. """ mock_ows_account_client.lookup_subaccounts_by_uuids.return_value = ( LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 998, "uuid": "a9dd9b42-e53d-11ee-be6d-4a2888760682", "vendor_uuid": "a9dd9b42-e53d-11ee-be6d-4a2888760682", "company_brand_uuid": "547e07ac-a421-4cb9-96ce-81d7fba67815", # noqa: E501 }, ] } ) ) mock_uuid_exchange_proxy._tenants_by_type = {} with pytest.raises(TenantHierarchyLookupError) as exc_info: _ = await mock_uuid_exchange_proxy._exchange_subaccounts_from_ows_account( tenant_uuids=[UUID("a9dd9b42-e53d-11ee-be6d-4a2888760682")] ) assert ( "tenant_hierarchy missing parent_company_uuid for: 'a9dd9b42-e53d-11ee-be6d-4a2888760682'" # noqa: E501 in str(exc_info.value) ) @pytest.mark.parametrize( "description, side_effect_error, expected_error", [ ( "Should raise an HTTPException for a 400 error.", HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=401), message="Bad request to ows-account for tenant proxy (subaccount).", ), HTTPException, ), ( "Should raise an HTTPException for a 500 error.", HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=500), message="Subaccount lookup by uuids failed for tenant proxy.", ), Exception, ), ( "Should raise a TenantHierarchyLookupError for an unhandled error.", RuntimeError("Unhandled error."), TenantHierarchyLookupError, ), ( "Should raise a TenantHierarchyLookupError if company_brand_uuid is null.", [ LookupSubaccountsResponse.model_validate( { "subaccounts": [ { "subaccount_id": 1, "uuid": "61577c79-d788-4b88-bf6a-2def82e0b4ed", "vendor_uuid": "61577c79-d788-4b88-bf6a-2def82e0b4ed", "company_brand_uuid": None, }, ] } ), ], TenantHierarchyLookupError, ), ], ) async def test__exchange_subaccounts_from_ows_account_fails( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, description: str, side_effect_error: BaseException, expected_error: type[BaseException], tenant_1_uuid: UUID, ) -> None: mock_ows_account_client.lookup_subaccounts_by_uuids.side_effect = AsyncMock( side_effect=side_effect_error ) with pytest.raises(expected_error): await mock_uuid_exchange_proxy._exchange_subaccounts_from_ows_account( tenant_uuids=[tenant_1_uuid] ) @pytest.mark.parametrize( "uuids, uuids_to_be_called, side_effect, expected_result, tenants_by_type", [ pytest.param( [ "5c7eeb64-d1d9-42de-b84d-7543f9a651c2", "1e75b54e-e15e-4377-ae74-04d5347858b6", ], [ "5c7eeb64-d1d9-42de-b84d-7543f9a651c2", "1e75b54e-e15e-4377-ae74-04d5347858b6", ], lambda uuids, fetch_flags=[]: LookupCompanyBrandsResponse.model_validate( { "company_brands": [ { "company_brand_id": 1, "uuid": "5c7eeb64-d1d9-42de-b84d-7543f9a651c2", "parent_company_uuid": "1e4086cc-79d7-4a83-9ce9-383503431a4d", # noqa: E501 }, { "company_brand_id": 2, "uuid": "1e75b54e-e15e-4377-ae74-04d5347858b6", "parent_company_uuid": "75203523-2356-46fd-8322-909dbf71ce1c", # noqa: E501 }, ] } ), { UUID("5c7eeb64-d1d9-42de-b84d-7543f9a651c2"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_id=1, tenant_uuid=UUID("5c7eeb64-d1d9-42de-b84d-7543f9a651c2"), company_brand=None, parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("1e4086cc-79d7-4a83-9ce9-383503431a4d"), ), ), UUID("1e75b54e-e15e-4377-ae74-04d5347858b6"): IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_id=2, tenant_uuid=UUID("1e75b54e-e15e-4377-ae74-04d5347858b6"), company_brand=None, parent_company=Tenant( tenant_type=TenantType.TENANT_TYPE_PARENT_COMPANY, tenant_uuid=UUID("75203523-2356-46fd-8322-909dbf71ce1c"), ), ), }, False, id="""Use provided tenant_uuids instead of the ones created on instantiating the proxy. Return the UUIDs and IdExchangeTenantHierarchy dictionary.""", ), pytest.param( [], [ UUID("fb22886e-5cfc-4092-991c-62acfa673c12"), ], None, {}, False, marks=pytest.mark.xfail( reason="needs updated _init_tenant_by_type changes", ), id="""Use tenant_uuids from instantiating the proxy when no tenant_uuids are provided. Return empty dict when ows-account callable returns nothing. """, ), pytest.param( [], [], None, {}, {TenantType.TENANT_TYPE_COMPANY_BRAND: []}, id="Should return an empty dict when there are no company brands", ), ], ) async def test__exchange_company_brands_from_ows_account( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, uuids: List[uuid.UUID], uuids_to_be_called: Optional[List[uuid.UUID]], side_effect: Callable[ [List[str], List[str]], Dict[TenantType, List[UuidToIdExchangeTenant]] ], expected_result: Dict[uuid.UUID, IdExchangeTenantHierarchy], tenants_by_type: Dict[TenantType, List[UuidToIdExchangeTenant]], ) -> None: """Test _exchange_company_brands_from_ows_account.""" mock_ows_account_client.lookup_company_brands_by_uuids.side_effect = side_effect if tenants_by_type: mock_uuid_exchange_proxy._tenants_by_type = tenants_by_type result = await mock_uuid_exchange_proxy._exchange_company_brands_from_ows_account( tenant_uuids=uuids ) assert result == expected_result if not uuids_to_be_called: mock_ows_account_client.lookup_company_brands_by_uuids.assert_not_called() else: mock_ows_account_client.lookup_company_brands_by_uuids.assert_called_once_with( uuids=uuids_to_be_called, ) @pytest.mark.parametrize( "side_effect_error, expected_error", [ pytest.param( HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=401), message="Bad request to ows-account for tenant proxy.", ), HTTPException, id="Should raise an HTTPException for a 400 error.", ), pytest.param( HTTPStatusError( request=Request("POST", "https://no.such/endpoint"), response=Response(status_code=500), message="Account lookup by tenant_uuids failed for tenant proxy.", ), Exception, id="Should raise an HTTPException for a 500 error.", ), pytest.param( RuntimeError("Unhandled error."), TenantHierarchyLookupError, id="Should raise a TenantHierarchyLookupError for an unhandled error.", ), pytest.param( [ LookupCompanyBrandsResponse.model_validate( { "company_brands": [ { "company_brand_id": 1, "uuid": str( UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND.tenant_uuid ), "parent_company_uuid": None, }, ] } ), ], TenantHierarchyLookupError, id="Should raise a TenantHierarchyLookupError if parent_company_uuid is None.", # noqa: E501 ), ], ) async def test__exchange_company_brands_from_ows_account_fails( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_ows_account_client: MagicMock, side_effect_error: BaseException, expected_error: type[BaseException], tenant_1_uuid: uuid.UUID, ) -> None: """Test _exchange_company_brands_from_ows_account fails.""" mock_ows_account_client.lookup_company_brands_by_uuids.side_effect = AsyncMock( side_effect=side_effect_error ) with pytest.raises(expected_error): await mock_uuid_exchange_proxy._exchange_company_brands_from_ows_account( tenant_uuids=[tenant_1_uuid] ) mock_ows_account_client.lookup_company_brands_by_uuids.assert_called_once_with( uuids=[tenant_1_uuid] ) @pytest.mark.parametrize( "tenant_type, tenants_by_type, lookup_response, expected", [ pytest.param( TenantType.TENANT_TYPE_ACCOUNT, {TenantType.TENANT_TYPE_ACCOUNT: [], TenantType.TENANT_TYPE_SUBACCOUNT: []}, {}, {}, id="If proxy has no _tenants_by_type, return empty dict", ), pytest.param( TenantType.TENANT_TYPE_ACCOUNT, { TenantType.TENANT_TYPE_ACCOUNT: [UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1], TenantType.TENANT_TYPE_SUBACCOUNT: [], }, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: IdExchangeTenantHierarchy( # noqa E501 tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, tenant_type=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_type, tenant_id="6157", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) }, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: IdExchangeTenantHierarchy( # noqa E501 tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, tenant_type=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_type, tenant_id="6157", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) }, id="""Proxy has one _tenants_by_type. Callback returns the IdExchangeTenantHierarchy for that tenant. _do_tenant_exchange should return that tenant in a dict.""", ), pytest.param( TenantType.TENANT_TYPE_SUBACCOUNT, { TenantType.TENANT_TYPE_ACCOUNT: [UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1], TenantType.TENANT_TYPE_SUBACCOUNT: [], }, {}, {}, id="""Proxy has one account in _tenants_by_type but caller is asking or subaccount tenants. Empty dict returned.""", ), pytest.param( TenantType.TENANT_TYPE_SUBACCOUNT, { TenantType.TENANT_TYPE_ACCOUNT: [UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1], TenantType.TENANT_TYPE_SUBACCOUNT: [ UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1 ], }, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: IdExchangeTenantHierarchy( # noqa E501 tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid, tenant_type=UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_type, tenant_id="71588", account=Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", ), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) }, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: IdExchangeTenantHierarchy( # noqa E501 tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid, tenant_type=UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_type, tenant_id="71588", account=Tenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", ), company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="b5b487b6-d8f8-4ebf-8de5-7cc9e7797bda", ), ) }, id="""Proxy has accounts and subacccount, but caller is asking for subaccount tenants. Returned dict includes only subaccount tenant hierarchies.""", ), ], ) @patch.object(UuidToIdExchangeTenantProxy, "_save_tenant_exchange_to_cache") @patch.object(UuidToIdExchangeTenantProxy, "_get_tenant_exchange_from_cache") async def test_do_tenant_exchange( mock__get_tenant_exchange_from_cache: AsyncMock, mock__save_tenant_exchange_to_cache: AsyncMock, mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, tenant_type: TenantType, tenants_by_type: Dict[TenantType, List[UuidToIdExchangeTenant]], lookup_response: Dict[UUID, IdExchangeTenantHierarchy], expected: Dict[UUID, IdExchangeTenantHierarchy], ) -> None: """Test _do_tenant_exchange.""" mock__get_tenant_exchange_from_cache.return_value = {} mock__save_tenant_exchange_to_cache.return_value = {} mock_uuid_exchange_proxy._tenants_by_type = tenants_by_type async def mock_lookup_cb( tenant_uuids: List[UUID], ) -> Dict[UUID, IdExchangeTenantHierarchy]: return lookup_response actual = await mock_uuid_exchange_proxy._do_tenant_exchange( tenant_type, mock_lookup_cb, ) assert actual == expected @pytest.mark.parametrize( "get_from_cache_result, expected_lookup_cb_ids, lookup_cb_result, expected_save_to_cache, expected_tenant_exchange_result", # noqa: E501 [ pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, ], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3, # noqa E501 }, [ UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3, ], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3, # noqa E501 }, id="""3 cache miss, lookup 3 ids, lookup response has 3 non-null items, response should include the 3 items""", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, ], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, id="""3 cache miss, lookup 3 ids, lookup response as 1 non-null item, response should include 1 item""", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid, ], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, id="""1 cache hit, lookup 2 ids, lookup response has 1 non-null response, response should include 2 items""", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3, # noqa E501 }, [], {}, [], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_2, # noqa E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_3, # noqa E501 }, id="""3 cache hits, lookup not called, response should include 3 items""", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid, ], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, [], { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_3.tenant_uuid: None, }, id="""3 cache miss, lookup 3 ids, lookup response has 0 non-null responses, response should include 0 items""", ), ], ) async def test__do_tenant_exchange_cache_behavior( get_from_cache_result: Dict[UUID, Optional[IdExchangeTenantHierarchy]], expected_lookup_cb_ids: List[UUID], lookup_cb_result: Dict[UUID, Optional[IdExchangeTenantHierarchy]], expected_save_to_cache: List[IdExchangeTenantHierarchy], expected_tenant_exchange_result: Dict[UUID, Optional[IdExchangeTenantHierarchy]], mock_uuid_exchange_proxy: MagicMock, ) -> None: """Test _do_tenant_exchange on Account tenant type with various cache hit and miss scenarios.""" mock_uuid_exchange_proxy._get_tenant_exchange_from_cache = AsyncMock( return_value=get_from_cache_result ) mock_uuid_exchange_proxy._save_tenant_exchange_to_cache = AsyncMock(return_value={}) mock_lookup_cb = AsyncMock(return_value=lookup_cb_result) actual = await mock_uuid_exchange_proxy._do_tenant_exchange( TenantType.TENANT_TYPE_ACCOUNT, mock_lookup_cb, ) assert actual == expected_tenant_exchange_result mock_uuid_exchange_proxy._get_tenant_exchange_from_cache.assert_awaited_once_with( mock_uuid_exchange_proxy._tenants_by_type[TenantType.TENANT_TYPE_ACCOUNT] ) if expected_lookup_cb_ids: mock_lookup_cb.assert_awaited_once_with(expected_lookup_cb_ids) else: mock_lookup_cb.assert_not_called() if expected_save_to_cache: mock_uuid_exchange_proxy._save_tenant_exchange_to_cache.assert_awaited_once_with( entries=expected_save_to_cache, tenant_type=TenantType.TENANT_TYPE_ACCOUNT ) else: mock_uuid_exchange_proxy._save_tenant_exchange_to_cache.assert_not_awaited() @pytest.mark.parametrize( "account_lookup_results,subaccount_lookup_results,company_brand_lookup_results,expected", [ pytest.param({}, {}, {}, {}, id="Empty dict is returned"), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, }, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: None, }, {}, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: None, }, id="Account and subaccount data is combined", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, }, {}, {}, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: None, }, id="Subaccount return results can be empty dict", ), pytest.param( {}, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: None, }, {}, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: None, }, id="Account return results can be empty dict", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 }, { UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1, # noqa: E501 }, { UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, # noqa: E501 }, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_SUBACCOUNT_1, # noqa: E501 UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND, # noqa: E501 }, id="account, subaccount, and company_brand data is combined", ), pytest.param( {}, {}, { UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND.tenant_uuid: {}, }, { UUID_TO_ID_EXCHANGE_TENANT_COMPANY_BRAND.tenant_uuid: {}, }, id="Company brand return results can be empty dict", ), pytest.param( { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: UUID_TO_ID_EXCHANGE_TENANT_HIERARCHY_ACCOUNT_1, # noqa: E501 }, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, }, {}, { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, }, id="""Data is overwritten when UUIDs overlap across tenant types. This scenario should not occur, but test case shows behavior""", ), ], ) async def test_gather_tenant_exchange( account_lookup_results: Dict[UUID, Optional[IdExchangeTenantHierarchy]], subaccount_lookup_results: Dict[UUID, Optional[IdExchangeTenantHierarchy]], company_brand_lookup_results: Dict[UUID, Optional[IdExchangeTenantHierarchy]], expected: Dict[UUID, Optional[IdExchangeTenantHierarchy]], mock_uuid_exchange_proxy: MagicMock, ) -> None: """Test gather_tenant_exchange.""" async def mock_do_tenant_exchange( tenant_type: TenantType, tenant_exchange_lookup_cb: Callable[ [List[UUID]], Awaitable[Dict[UUID, IdExchangeTenantHierarchy]], ], ) -> Dict[UUID, Optional[IdExchangeTenantHierarchy]]: """Mock _do_tenant_exchange.""" if tenant_type == TenantType.TENANT_TYPE_ACCOUNT: return account_lookup_results if tenant_type == TenantType.TENANT_TYPE_SUBACCOUNT: return subaccount_lookup_results if tenant_type == TenantType.TENANT_TYPE_COMPANY_BRAND: return company_brand_lookup_results # Default return should never appear in expected # This shows that we're only supporting the tenant types above return {uuid.uuid4(): None} mock_uuid_exchange_proxy._do_tenant_exchange = AsyncMock( side_effect=mock_do_tenant_exchange, ) actual = await mock_uuid_exchange_proxy.gather_tenant_exchange() assert actual == expected async def test__get_tenant_exchange_from_cache_empty_tenants( mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_redis_connector: MagicMock, ) -> None: """Test _get_tenant_exchange_from_cache when tenants are empty.""" actual = await mock_uuid_exchange_proxy._get_tenant_exchange_from_cache([]) assert actual == {} mock_redis_connector.list.assert_not_called() mock_redis_connector.mget.assert_not_called() @patch("pdp.proxies.uuid_to_id_exchange_tenant_proxy.PydanticSchemaSerializer") async def test__get_tenant_exchange_from_cache( mock_pydantic_schema_serializer: MagicMock, mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_redis_connector: MagicMock, ) -> None: """Test _get_tenant_exchange_from_cache.""" ACCOUNT_2_TENANT_HIERARCHY = IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=22227, tenant_uuid=UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid, company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid=uuid.uuid4(), ), ) mock_redis_connector.mget.return_value = [ None, ACCOUNT_2_TENANT_HIERARCHY, None, ] actual = await mock_uuid_exchange_proxy._get_tenant_exchange_from_cache( [ UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2, UuidToIdExchangeTenant( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_uuid="76154146-87d1-4a5b-b1bd-701759b49d89", ), ] ) assert actual == { UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid: None, UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid: ACCOUNT_2_TENANT_HIERARCHY, uuid.UUID("76154146-87d1-4a5b-b1bd-701759b49d89"): None, } mock_redis_connector.mget.assert_called_once_with( keys=[ f"tenant_hierarchy@tenant_type#account|tenant_uuid#{UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_1.tenant_uuid}", f"tenant_hierarchy@tenant_type#account|tenant_uuid#{UUID_TO_ID_EXCHANGE_TENANT_ACCOUNT_2.tenant_uuid}", "tenant_hierarchy@tenant_type#account|tenant_uuid#76154146-87d1-4a5b-b1bd-701759b49d89", ], serializer=mock_pydantic_schema_serializer(IdExchangeTenantHierarchy), ) @pytest.mark.parametrize( "entries, tenant_type, expected_mset_entries, mset_return, expected", [ pytest.param( [], TenantType.TENANT_TYPE_ACCOUNT, {}, {}, {}, id="Empty entries list returns empty dict", ), pytest.param( [ IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ) ], TenantType.TENANT_TYPE_ACCOUNT, { "tenant_hierarchy@tenant_id#123|tenant_type#account": IdExchangeTenantHierarchy( # noqa E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": True, # noqa E501 }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": True, # noqa E501 }, id="Two entries saved successfully in cache", ), pytest.param( [ IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), ], TenantType.TENANT_TYPE_ACCOUNT, { "tenant_hierarchy@tenant_id#123|tenant_type#account": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": False, # noqa E501 }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": False, # noqa E501 }, id="mset will return False if failed to save entry", ), pytest.param( [ IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), ], TenantType.TENANT_TYPE_ACCOUNT, { "tenant_hierarchy@tenant_id#123|tenant_type#account": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": False, # noqa E501 }, id="Entries not returned from mset will default to False in response dict", ), pytest.param( [ IdExchangeTenantHierarchy( tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ) ], TenantType.TENANT_TYPE_ACCOUNT, { "tenant_hierarchy@tenant_id#123|tenant_type#account": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": IdExchangeTenantHierarchy( # noqa: E501 tenant_type=TenantType.TENANT_TYPE_ACCOUNT, tenant_id=123, tenant_uuid="61577c79-d788-4b88-bf6a-2def82e0b4ed", company_brand=Tenant( tenant_type=TenantType.TENANT_TYPE_COMPANY_BRAND, tenant_uuid="15b8e358-cd42-4fc3-a050-7a1a5c7f3ea7", ), ), }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": True, # noqa E501 "tenant_hierarchy@something": False, }, { "tenant_hierarchy@tenant_id#123|tenant_type#account": True, "tenant_hierarchy@tenant_type#account|tenant_uuid#61577c79-d788-4b88-bf6a-2def82e0b4ed": True, # noqa E501 }, id="Cache keys returned from mset that were not passed in will be ignored in response", # noqa E501 ), ], ) @patch("pdp.proxies.uuid_to_id_exchange_tenant_proxy.PydanticSchemaSerializer") async def test_save_tenant_exchange_to_cache( mock_serializer: MagicMock, mock_uuid_exchange_proxy: UuidToIdExchangeTenantProxy, mock_redis_connector: MagicMock, entries: List[IdExchangeTenantHierarchy], tenant_type: TenantType, expected_mset_entries: Dict[str, IdExchangeTenantHierarchy], mset_return: Dict[str, bool], expected: Dict[str, bool], ) -> None: """Test _save_tenant_exchange_to_cache.""" mock_redis_connector.mset.return_value = mset_return mock_redis_connector.mset_with_pipeline.return_value = mset_return actual = await mock_uuid_exchange_proxy._save_tenant_exchange_to_cache( entries=entries, tenant_type=tenant_type, ) assert actual == expected if entries: mock_redis_connector.mset.assert_not_called() mock_redis_connector.mset_with_pipeline.assert_awaited_once_with( expected_mset_entries, serializer=mock_serializer(IdExchangeTenantHierarchy), ttl=TENANT_HIERARCHY_CACHE_TTL_BY_TENANT_TYPE[tenant_type], ) else: mock_redis_connector.mset.assert_not_called() mock_redis_connector.mset_with_pipeline.assert_not_called()