"""conftest. This file gets picked up when running py.test tests: http://pytest.org/latest/writing_plugins.html#conftest """ import uuid from collections.abc import Callable from typing import AsyncGenerator, Generator, Optional import boto3 import pytest from jwtauth.testing import ( JwtAuthSecretsManager, SecretLookupInfo, ) from mypy_boto3_dynamodb import DynamoDBClient from pdp import config from pdp.connectors.cerbos_policy_parser import ( CerbosPolicyParser, PolicyMetadataDatabase, ) from pdp.connectors.dynamo import DynamoDbConnector from pdp.connectors.redis_client import RedisConnector from pdp.constants.constants import CACHE_ENTRY_CERBOS_POLICY_METADATA from pdp.models.identity import HASH_KEY as IDENTITY_HASH_KEY from pdp.models.identity import RANGE_KEY as IDENTITY_RANGE_KEY from pdp.utils.dynamo import get_opts from tests.integration import utils from tests.integration.utils import ( run_command, ) pytest_plugins = ["jwtauth.testing.pytest_plugin"] @pytest.fixture(scope="session", autouse=True) def anyio_backend() -> str: """Return AnyIO backend.""" return "asyncio" @pytest.fixture() def boto_client() -> DynamoDBClient: """Test fixture for interacting with boto3 dynamodb.""" return boto3.client("dynamodb", **get_opts(config)) @pytest.fixture() def dynamodb_table_identity() -> str: """Fixture for the dynamodb table name.""" return config.DYNAMODB_TABLE_IDENTITY @pytest.fixture() def default_boto_client( boto_client: DynamoDBClient, ) -> Generator[DynamoDBClient, None, None]: """Test fixture for recreating the test_pp_identity table.""" # Delete the table, if it exists tables = boto_client.list_tables() if config.DYNAMODB_TABLE_IDENTITY in tables["TableNames"]: boto_client.delete_table(TableName=config.DYNAMODB_TABLE_IDENTITY) # Create the table boto_client.create_table( TableName=config.DYNAMODB_TABLE_IDENTITY, AttributeDefinitions=[ {"AttributeName": IDENTITY_HASH_KEY, "AttributeType": "S"}, {"AttributeName": IDENTITY_RANGE_KEY, "AttributeType": "S"}, ], KeySchema=[ {"AttributeName": IDENTITY_HASH_KEY, "KeyType": "HASH"}, {"AttributeName": IDENTITY_RANGE_KEY, "KeyType": "RANGE"}, ], ProvisionedThroughput={"ReadCapacityUnits": 10, "WriteCapacityUnits": 5}, ) # Enable TTL on the table boto_client.update_time_to_live( TableName=config.DYNAMODB_TABLE_IDENTITY, TimeToLiveSpecification={"Enabled": True, "AttributeName": "expires_at"}, ) yield boto_client # Delete the table boto_client.delete_table(TableName=config.DYNAMODB_TABLE_IDENTITY) @pytest.fixture(autouse=True) async def local_redis_connector() -> AsyncGenerator[RedisConnector, None]: """Test fixture to connect to a local Redis server.""" client = RedisConnector(redis_url=config.REDIS_URL, use_redis_cache=True) await client._client.flushall() yield client # cleanup after the test await client._client.flushall() @pytest.fixture(scope="session") def seeded_policy_metadata_db() -> PolicyMetadataDatabase: """Build the policy metadata database from the cerbos policy files. Mirrors the production seed path (CerbosPolicyParser.build_database) so integration tests read the same metadata the seed job writes to Redis. """ return CerbosPolicyParser().build_database() @pytest.fixture() async def seed_policy_metadata_cache( local_redis_connector: RedisConnector, seeded_policy_metadata_db: PolicyMetadataDatabase, ) -> None: """Seed cerbos_policy_metadata into Redis. Not autouse: only the tests that exercise the policy metadata cache opt in (the api/identity suite autouses it via its local conftest; other tests request it directly). Depends on local_redis_connector so it runs *after* that fixture's setup flushall(); its teardown flushall() clears the key again. Error-path tests delete this key via policy_metadata_cache_key_absent. """ await local_redis_connector.client.set( CACHE_ENTRY_CERBOS_POLICY_METADATA, seeded_policy_metadata_db.to_json(), ) @pytest.fixture() async def policy_metadata_cache_key_absent( local_redis_connector: RedisConnector, seed_policy_metadata_cache: None, ) -> AsyncGenerator[None, None]: """Delete the cerbos_policy_metadata key seeded by seed_policy_metadata_cache. Depends on seed_policy_metadata_cache so the delete runs after the seed. The per-test flushall() in local_redis_connector handles cleanup, so no restore is needed. """ await local_redis_connector.client.delete(CACHE_ENTRY_CERBOS_POLICY_METADATA) yield @pytest.fixture() def local_dynamo_connector() -> DynamoDbConnector: """Create dynamo connector for identity table..""" # Note that this Dynamo Connector is not passed a range key return DynamoDbConnector(config.DYNAMODB_TABLE_IDENTITY, IDENTITY_HASH_KEY) @pytest.fixture() def local_dynamo_connector_with_range() -> DynamoDbConnector: """Create dynamo connector for identity table.""" # Note that this Dynamo Connector IS passed a range key return DynamoDbConnector( config.DYNAMODB_TABLE_IDENTITY, IDENTITY_HASH_KEY, IDENTITY_RANGE_KEY ) @pytest.fixture() def seed_test_table(local_dynamo_connector: DynamoDbConnector) -> None: """Seed test table.""" local_dynamo_connector._client.put_item( TableName=config.DYNAMODB_TABLE_IDENTITY, Item={ IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "howdy-test-uuid"}, }, ) local_dynamo_connector._client.put_item( TableName=config.DYNAMODB_TABLE_IDENTITY, Item={ IDENTITY_HASH_KEY: {"S": "hello-test-uuid"}, IDENTITY_RANGE_KEY: {"S": "bonjour-test-uuid"}, }, ) @pytest.fixture() def seed_test_table_stress_test_complicated_user() -> None: """Seed test tables with complicated user.""" run_command( [ "uv", "run", "pdpcli", "dynamodb", "load_csv", "tests/integration/data/stress_test_complicated_user.csv", ], timeout=60, ) def seed_test_pp_identity( boto_client: DynamoDBClient, identity_uuid: str, tenant_uuid: str, role: str, tenant_type: str = "account", ) -> None: """Reusable function for seeding data into test_pp_identity.""" boto_client.put_item( TableName=config.DYNAMODB_TABLE_IDENTITY, Item={ IDENTITY_HASH_KEY: {"S": identity_uuid}, IDENTITY_RANGE_KEY: {"S": tenant_uuid}, "version": {"S": "1"}, "tenant_type": {"S": tenant_type}, "roles": { "L": [ {"M": {"role": {"S": role}}}, ] }, }, ) @pytest.fixture(scope="session") def identity_uuid() -> uuid.UUID: """Generate a random identity uuid.""" return uuid.uuid4() @pytest.fixture(scope="session") def bearer_token_pdptest_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for the PDP test user. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_pdptest_user_identity_uuid( bearer_token_pdptest_user: str, ) -> Optional[str]: """Extract identity_uuid from pdptest user bearer_token.""" return utils.get_bearer_token_identity_uuid(bearer_token_pdptest_user) @pytest.fixture(scope="session") def bearer_token_pdptest_rap_admin_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for the test RAP Admin user. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_RAP_ADMIN_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_pdptest_rap_admin_user_identity_uuid( bearer_token_pdptest_rap_admin_user: str, ) -> Optional[str]: """Extract identity_uuid from pdptest RAP Admin user bearer_token.""" return utils.get_bearer_token_identity_uuid(bearer_token_pdptest_rap_admin_user) @pytest.fixture(scope="session") def bearer_token_pdptest_not_rap_admin_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for a user that is not a RAP Admin. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_NOT_RAP_ADMIN_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_pdptest_d3_rap_admin_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for a user that is a D3 RAP Admin. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_RAP_ADMIN_D3_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_complicated_identity_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for a user that has many tenant permissions. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.STRESS_TEST_APPLICATION, secret_name=utils.COMPLICATED_IDENTITY_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_simple_identity_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for a user that has simple tenant permissions. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.STRESS_TEST_APPLICATION, secret_name=utils.SIMPLE_IDENTITY_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_pdptest_seat_rap_admin_user( generate_bearer_token: Callable[..., str], jwtauth_secrets_manager: JwtAuthSecretsManager, ) -> str: """ Generate a bearer token for integration test user for SEAT-related tests. """ return generate_bearer_token( get_user_creds_args=SecretLookupInfo( environment="qa", service_name=utils.APPLICATION, secret_name=utils.PDP_TEST_SEAT_RAP_ADMIN_USER_CREDENTIALS, ), get_auth0_creds_args=SecretLookupInfo( environment="qa", service_name="pdp-integration-test", secret_name=utils.PDP_TEST_APP_AUTH0_CREDENTIALS, ), secrets_manager=jwtauth_secrets_manager, ) @pytest.fixture(scope="session") def bearer_token_pdptest_seat_rap_admin_user_identity_uuid( bearer_token_pdptest_seat_rap_admin_user: str, ) -> Optional[str]: """Extract identity_uuid from pdptest_seat_rap_admin_user bearer_token.""" return utils.get_bearer_token_identity_uuid( bearer_token_pdptest_seat_rap_admin_user )