"""conftest. This file gets picked up when running py.test tests: http://pytest.org/latest/writing_plugins.html#conftest """ import uuid from typing import Any, AsyncIterator, Dict, Generator, Iterator, List from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID import boto3 import pytest import redis.asyncio as redis from cerbos.sdk.client import AsyncCerbosClient from cerbos.sdk.model import ( CheckResourcesResponse, CheckResourcesResult, Effect, Resource, ) from fastapi import FastAPI from fastapi.testclient import TestClient from owsclient import AsyncOwsClient from splitio.client.factory import Client as SplitioClient from pdp.connectors.features import FEATURE_OFF from pdp.connectors.ows_account import ( LookupSubaccountsResponse, LookupVendorFetchFlags, LookupVendorsResponse, OwsAccountClient, ) from pdp.connectors.ows_participant import ( LookupParticipantsResponse, OwsParticipantClient, ) from pdp.connectors.ows_permissions import ( AdminableResourcesResponse, OwsPagination, OwsPermissionsClient, OwsPermissionsResource, ) from pdp.connectors.redis_client import RedisConnector from pdp.fastapi.auth import ( check_authorization, check_authorization_infra, get_principal_pdp_tenant_roles_from_scope, identity_uuid_from_scope, impersonated_by_identity_uuid_from_scope, user_type_from_scope, ) from pdp.fastapi.datasources import ( get_async_cerbos_client, get_boto_connector, get_ows_account_client, get_ows_participant_client, get_ows_permissions_client, get_redis_connector, get_splitio_client, ) from pdp.fastapi.main import app as _app from pdp.fastapi.schemas.identity import Role, TenantRoles from pdp.proxies.tenant_validators import ( assert_valid_attach_detach_request, ) @pytest.fixture(scope="session", autouse=True) def anyio_backend() -> str: """Return AnyIO backend.""" return "asyncio" @pytest.fixture() def identity_uuid() -> str: """Return a valid identity_uuid for testing.""" return "ab123456-1234-4c2b-9c23-123ab4000a1b" @pytest.fixture() def identity_uuid_as_uuid() -> UUID: """Return a valid identity_uuid as UUID for testing.""" return UUID("ab123456-1234-4c2b-9c23-123ab4000a1b", version=4) @pytest.fixture() def identity_uuid_two() -> str: """Return a valid identity_uuid for testing.""" return "f77c57bc-839c-4878-bfe1-7c91947745d3" @pytest.fixture def decoded_jwt(identity_uuid: str) -> Dict[str, Any]: """Return a JWT for testing.""" return { "https://grass.theorchard.com/profiles": [ { "brand": "orchard", "full_catalog_access": False, "profile_id": 12345, "profile_name": "Test Profile Name", "profile_type": "SettingsProfile", "uuid": "123ab1a1-123a-12b2-1234-e12ce34aeefb", } ], "https://grass.theorchard.com/version": "0.2", "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_uuid }, "https://grass.theorchard.com/app_metadata": {}, "https://grass.theorchard.com/internal_employee": False, "https://grass.theorchard.com/brand": "orchard", "iss": "https://qalogin.theorchard.com/", "sub": "google-apps|test@theorchard.com", "aud": [ "https://workstation.qaorch.com/api", "https://qa-orchard.auth0.com/userinfo", ], "iat": 1234567891, "exp": 9876543210, "azp": "test123", "scope": "openid profile email offline_access", "org_id": "org_test123456", } @pytest.fixture def decoded_impersonated_jwt( identity_uuid: str, identity_uuid_two: str ) -> Dict[str, Any]: """Return an impersonated JWT for testing.""" return { "https://grass.theorchard.com/profiles": [ { "brand": "orchard", "full_catalog_access": False, "profile_id": 12345, "profile_name": "Test Profile Name", "profile_type": "SettingsProfile", "uuid": "123ab1a1-123a-12b2-1234-e12ce34aeefb", } ], "https://grass.theorchard.com/version": "0.2", "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_uuid }, "https://grass.theorchard.com/impersonated_by": identity_uuid_two, "https://grass.theorchard.com/app_metadata": {}, "https://grass.theorchard.com/internal_employee": False, "https://grass.theorchard.com/brand": "orchard", "iss": "https://qalogin.theorchard.com/", "sub": "google-apps|test@theorchard.com", "aud": [ "https://workstation.qaorch.com/api", "https://qa-orchard.auth0.com/userinfo", ], "iat": 1234567891, "exp": 9876543210, "azp": "test123", "scope": "openid profile email offline_access", "org_id": "org_test123456", } @pytest.fixture def app( identity_uuid_as_uuid: UUID, mock_identity_ddb_connector: MagicMock, mock_ows_account_client: OwsAccountClient, mock_ows_participant_client: OwsParticipantClient, mock_ows_permissions_client: OwsPermissionsClient, mock_async_cerbos_client: AsyncCerbosClient, mock_pdp_tenant_roles: Dict[UUID, TenantRoles], mock_splitio_client: SplitioClient, mock_redis_connector: RedisConnector, ) -> Generator[FastAPI, None, None]: """Override app dependencies for the tests.""" # Mock the DDB connector used in datasources.datasources_lifespan. # Otherwise, the unit tests will attempt to create a DDB connection, # causing timeouts. with patch( "pdp.fastapi.datasources.DynamoDbConnector", mock_identity_ddb_connector ): _app.dependency_overrides[identity_uuid_from_scope] = ( lambda: identity_uuid_as_uuid ) _app.dependency_overrides[impersonated_by_identity_uuid_from_scope] = ( lambda: None ) _app.dependency_overrides[user_type_from_scope] = lambda: "human" _app.dependency_overrides[check_authorization] = lambda: True _app.dependency_overrides[check_authorization_infra] = lambda: True _app.dependency_overrides[get_boto_connector] = ( lambda: mock_identity_ddb_connector ) _app.dependency_overrides[get_ows_permissions_client] = ( lambda: mock_ows_permissions_client ) _app.dependency_overrides[get_async_cerbos_client] = ( lambda: mock_async_cerbos_client ) _app.dependency_overrides[get_principal_pdp_tenant_roles_from_scope] = ( lambda: mock_pdp_tenant_roles ) _app.dependency_overrides[get_splitio_client] = lambda: mock_splitio_client _app.dependency_overrides[get_ows_participant_client] = ( lambda: mock_ows_participant_client ) _app.dependency_overrides[get_ows_account_client] = ( lambda: mock_ows_account_client ) _app.dependency_overrides[get_redis_connector] = lambda: mock_redis_connector _app.dependency_overrides[assert_valid_attach_detach_request] = lambda: True _app.debug = False yield _app _app.dependency_overrides = {} @pytest.fixture def test_client(app: FastAPI) -> Iterator[TestClient]: """Fastapi test client fixture.""" with TestClient(app) as client: yield client def factory_dynamo_identity_role( identity_uuid: str = "c5879365-2c07-4c19-a3f0-cc030c01b6a5", tenant_uuid: str = "test-uuid-123456", roles: List[str] = ["audience_development_admin"], ) -> Dict[str, Any]: """Represent an Identity's tenant permissions.""" return { "identity_uuid": {"S": identity_uuid}, "tenant_uuid": {"S": tenant_uuid}, "roles": {"L": [{"M": {"role": {"S": role}}} for role in roles]}, "tenant_type": {"S": "account"}, } @pytest.fixture def dynamo_last_evaluated_key() -> Dict[str, Any]: """Return sample dynamodb LastEvaluatedKey.""" item = factory_dynamo_identity_role() item.pop("roles") item.pop("tenant_type") return item @pytest.fixture def dynamodb_query_response() -> Dict[str, Any]: """Return sample dynamodb query() response.""" return { "Items": [ factory_dynamo_identity_role(tenant_uuid="tenant-uuid-1"), factory_dynamo_identity_role(tenant_uuid="tenant-uuid-2"), ], "Count": 1, "ScannedCount": 1, "ResponseMetadata": {}, } @pytest.fixture def dynamodb_update_response() -> Dict[str, Any]: """Return sample dynamodb update() response.""" return { "Attributes": [factory_dynamo_identity_role()], "Count": 1, "ScannedCount": 1, "ResponseMetadata": {"HTTPStatusCode": 200}, } @pytest.fixture() def mock_identity_ddb_connector(mock_dynamo_client: MagicMock) -> MagicMock: """Mock DynamoDbConnector.""" identity_ddb_connector = MagicMock() identity_ddb_connector.client = mock_dynamo_client return identity_ddb_connector @pytest.fixture() def mock_my_adminable_resources() -> AdminableResourcesResponse: """Mock my adminable resources response.""" return AdminableResourcesResponse( items=[ OwsPermissionsResource( type="Vendor", id=7123, uuid="573d0372-7f2f-48a6-8deb-c9a6558f9549" ) ], pagination=OwsPagination(type="classico", total_records=1), ) @pytest.fixture() def mock_my_adminable_tenant_roles() -> Dict[UUID, TenantRoles]: """Mock my adminable resources as tenant roles.""" return { UUID("573d0372-7f2f-48a6-8deb-c9a6558f9549"): TenantRoles( roles=[Role(role="ows_permissions_rap_admin")], tenant_uuid="573d0372-7f2f-48a6-8deb-c9a6558f9549", tenant_type="account", ) } @pytest.fixture() def mock_pdp_tenant_roles() -> Dict[UUID, TenantRoles]: """Mock get_identity_pdp_tenant_roles_from_scope response.""" return { UUID("04b48f72-5b47-425f-8b49-21f1ebc3f0cd"): TenantRoles( roles=[Role(role="dog_whisperer")], tenant_uuid="04b48f72-5b47-425f-8b49-21f1ebc3f0cd", tenant_type="subaccount", ) } @pytest.fixture async def mock_async_ows_client() -> AsyncIterator[AsyncOwsClient]: """Mock async_ows_client.""" async_ows_client = AsyncOwsClient(environment="test", service_name="ows-test") yield async_ows_client await async_ows_client.close() @pytest.fixture() def mock_ows_permissions_client( mock_my_adminable_resources: AdminableResourcesResponse, ) -> OwsPermissionsClient: """Mock ows_permissions_client.""" ows_permissions_client = MagicMock(spec=OwsPermissionsClient) ows_permissions_client.get_my_adminable_resources = AsyncMock( return_value=mock_my_adminable_resources ) ows_permissions_client.collect_get_my_adminable_resources = AsyncMock( return_value=mock_my_adminable_resources.items ) return ows_permissions_client @pytest.fixture def mock_ows_participant_client() -> OwsParticipantClient: """Mock ows-participant client.""" ows_participant_client = MagicMock(spec=OwsParticipantClient) ows_participant_client.lookup_participants_by_uuids = AsyncMock( return_value=LookupParticipantsResponse(label_participants=[]) ) return ows_participant_client def lookup_response_by_fetch_flag_side_effect( uuids: List[UUID], fetch_flags: List[LookupVendorFetchFlags] = [] ) -> LookupVendorsResponse: """Return a LookupVendorsResponse for uuids and fetch_flags in the request.""" lookup_response: Dict[str, List[Any]] = {"vendors": []} if not fetch_flags: fetch_flags = [] for vendor_id, _uuid in enumerate(uuids): vendor = { "vendor_id": vendor_id, "uuid": str(_uuid), } for fetch_flag in fetch_flags: if fetch_flag == LookupVendorFetchFlags.TENANT_HIERARCHY: vendor["company_brand_uuid"] = "d25a4cd1-e820-45f2-be5c-56edcfeb8298" lookup_response["vendors"].append(vendor) return LookupVendorsResponse.model_validate(lookup_response) def lookup_subaccounts_response_by_fetch_flag_side_effect( uuids: List[UUID], fetch_flags: List[LookupVendorFetchFlags] = [] ) -> LookupSubaccountsResponse: """Return a LookupSubaccountsResponse for uuids and fetch_flags in the request.""" lookup_response: Dict[str, List[Any]] = {"subaccounts": []} if not fetch_flags: fetch_flags = [] for subaccount_id, _uuid in enumerate(uuids): subaccount = { "subaccount_id": subaccount_id, "uuid": str(_uuid), } for fetch_flag in fetch_flags: if fetch_flag == LookupVendorFetchFlags.TENANT_HIERARCHY: subaccount["company_brand_uuid"] = ( "d25a4cd1-e820-45f2-be5c-56edcfeb8298" ) subaccount["vendor_uuid"] = "fff741c2-6def-4493-bfdf-c2bcb1128e02" lookup_response["subaccounts"].append(subaccount) return LookupSubaccountsResponse.model_validate(lookup_response) @pytest.fixture def mock_ows_account_client() -> OwsAccountClient: """Mock ows_account client.""" mock_client = AsyncMock(spec=OwsAccountClient) mock_client.lookup_vendors_by_uuids.side_effect = ( lookup_response_by_fetch_flag_side_effect ) mock_client.lookup_subaccounts_by_uuids.side_effect = ( lookup_subaccounts_response_by_fetch_flag_side_effect ) return mock_client @pytest.fixture() def mock_splitio_client() -> SplitioClient: """Mock split.io client connection for dependency injection.""" splitio_client = MagicMock(spec=SplitioClient) splitio_client.get_treatment.return_value = FEATURE_OFF return splitio_client @pytest.fixture() def mock_async_cerbos_client( mock_cerbos_allow_response: CheckResourcesResponse, ) -> AsyncCerbosClient: """Mock Cerbos response for dependency injection.""" cerbos_client = AsyncMock(spec=AsyncCerbosClient) cerbos_client.check_resources = AsyncMock(return_value=mock_cerbos_allow_response) return cerbos_client @pytest.fixture() def mock_redis_client() -> redis.Redis: """Mock redis client.""" return AsyncMock(spec=redis.Redis) @pytest.fixture() def mock_redis_connector(mock_redis_client: AsyncMock) -> RedisConnector: """Mock redis connector.""" redis_connector = RedisConnector(redis_url="no.such.host", use_redis_cache=False) redis_connector._client = mock_redis_client return redis_connector TEST_TENANT_UUID = UUID("8a245b3d-716a-4e07-9e46-78fca5ba8ac4") @pytest.fixture() def mock_cerbos_allow_response() -> CheckResourcesResponse: """Test cerbos response.""" return CheckResourcesResponse( request_id="test-cerbos-request-id", results=[ CheckResourcesResult( resource=Resource(id="5678", kind="fan_data_list"), actions={"connect": Effect.ALLOW}, validation_errors=[], ) ], ) @pytest.fixture() def mock_dynamo_client( dynamodb_query_response: Dict[str, Any], dynamodb_update_response: Dict[str, Any] ) -> MagicMock: """Mock boto3 dynamodb client.""" mock_dynamo_client = MagicMock() mock_dynamo_client.query.return_value = dynamodb_query_response mock_dynamo_client.update_item.return_value = dynamodb_update_response return mock_dynamo_client @pytest.fixture() def mock_boto3_session() -> MagicMock: """Mock boto3 session.""" mock_session = MagicMock() mock_session.Session.return_value = MagicMock(spec=boto3.Session) return mock_session @pytest.fixture() def tenant_1_uuid_as_string() -> str: """Fixture for a tenant uuid as string.""" # Use UUIDv1 for testing (see PP-583) return "a9dd9b42-e53d-11ee-be6d-4a2888760682" @pytest.fixture() def tenant_1_uuid(tenant_1_uuid_as_string: str) -> UUID: """Fixture for a tenant uuid.""" return UUID(tenant_1_uuid_as_string) @pytest.fixture() def tenant_2_uuid_as_string() -> str: """Fixture for a second tenant uuid as string.""" return "4378cd91-7852-49eb-b39f-653dec222db8" @pytest.fixture() def tenant_2_uuid(tenant_2_uuid_as_string: str) -> UUID: """Fixture for a second tenant uuid.""" return UUID(tenant_2_uuid_as_string) @pytest.fixture() def tenant_1_id() -> int: """Fixture for a tenant id as integer.""" return 998 @pytest.fixture() def tenant_2_id() -> int: """Fixture for a tenant id as integer.""" return 999 @pytest.fixture() def role_data_for_update( tenant_1_uuid_as_string: str, tenant_1_uuid: uuid.UUID ) -> Dict[uuid.UUID, TenantRoles]: """Mock role data for update testing.""" return { tenant_1_uuid: TenantRoles( **{ "tenant_uuid": tenant_1_uuid_as_string, "tenant_type": "account", "roles": [ {"role": "content_review"}, {"role": "audience_development_admin"}, {"role": "dog_tamer"}, ], } ) } @pytest.fixture() def role_data_for_delete(tenant_1_uuid_as_string: str) -> Dict[str, TenantRoles]: """Mock role data for delete testing.""" return { tenant_1_uuid_as_string: TenantRoles( **{ "tenant_uuid": tenant_1_uuid_as_string, "tenant_type": "account", "roles": [ {"role": "content_review"}, {"role": "audience_development_admin"}, {"role": "dog_tamer"}, ], } ) } @pytest.fixture() def get_roles_by_tenant_response(tenant_1_uuid_as_string: str) -> Dict[str, Any]: """Mock response for get_roles_by_tenant call.""" return { "items": [ { "tenant_type": {"S": "account"}, "tenant_uuid": {"S": tenant_1_uuid_as_string}, "version": {"S": "1"}, "roles": { "L": [ {"M": {"role": {"S": "content_review"}}}, {"M": {"role": {"S": "audience_development_admin"}}}, {"M": {"role": {"S": "dog_tamer"}}}, ] }, "identity_uuid": {"S": "c5879365-2c07-4c19-a3f0-cc030c01b6a4"}, "created_at": {"S": "2023-04-06"}, "created_by": {"S": "jess"}, } ], "cursor": {}, } @pytest.fixture def mock_existing_roles() -> List[Dict[str, str]]: """Existing Roles Fixture.""" return [{"role": "zookeeper"}, {"role": "beekeeper"}]