import contextlib import logging.config import pathlib from collections.abc import Iterator, Sequence from typing import Any from unittest import mock from unittest.mock import MagicMock, patch import pytest from anydi import Container from cachelib import BaseCache as Cache, SimpleCache from fansifter_common.adapters.graphql_router import GraphqlRouterClient from fansifter_common.adapters.ows_account import OwsAccountClient from fansifter_common.auth.account import Account, AccountAccess from fansifter_common.auth.identity import Identity from fansifter_common.auth.services import AuthService from fansifter_common.constants import DEFAULT_BRAND from fansifter_common.testing.factories import FactoryService from jinja2sql import Jinja2SQL from owsclient import OwsClient from pytest_docker.plugin import Services from sqlalchemy import text from sqlalchemy.engine import URL from dmp.adapters.aws.kms import BaseKMS, DummyKMS from dmp.adapters.aws.location import LocationClient from dmp.adapters.aws.s3 import S3Client from dmp.adapters.db import DefaultDB, Model, ReportingDB, ReportingModel from dmp.adapters.db.utils import check_db_alive from dmp.adapters.features import Features from dmp.adapters.fivetran import FivetranClient from dmp.adapters.google import GoogleClient from dmp.adapters.meta.client import FacebookClient from dmp.adapters.ows_notifications import OwsNotificationsClient from dmp.adapters.ows_socials import OwsSocialsClient from dmp.adapters.tiktok import TikTokClient from dmp.config import Settings, settings as global_settings from dmp.container import container as global_container from tests.unit.faker import FakerTyped from tests.unit.module import TestModule from tests.unit.types import ( BuildModel, BuildModelBatch, CreateModel, CreateModelBatch, CreateReportingModel, CreateReportingModelBatch, EnableFeatures, OverrideSettings, ) def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--docker-mode", choices=["external", "auto"], dest="docker_mode", default="auto", help=( "Specify Docker usage mode: 'external' (Docker already running) " "or 'auto' (let tests start it)" ), ) def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", "db: mark test as using the db", ) @pytest.fixture(autouse=True) def anyio_backend() -> str: return "asyncio" @pytest.fixture(scope="session") def fake() -> FakerTyped: return FakerTyped() @pytest.fixture(scope="session") def settings() -> Settings: return global_settings @pytest.fixture(scope="session") def override_settings(settings: Settings, container: Container) -> OverrideSettings: @contextlib.contextmanager def wrapper(**kwargs: Any) -> Iterator[None]: new_settings = settings.model_copy(update=kwargs) with ( mock.patch("dmp.config.settings.__wrapped__", new_settings), container.override(Settings, new_settings), ): yield return wrapper @pytest.fixture(scope="session", autouse=True) def container() -> Container: global_container.register_module(TestModule) return global_container @pytest.fixture(scope="session", autouse=True) def configure_logging(settings: Settings) -> None: logging.config.dictConfig(settings.logging_config) @pytest.fixture(scope="session") def features_mock(container: Container) -> Iterator[mock.MagicMock]: features_mock = mock.MagicMock(spec=Features) features_mock.is_on_for_identity = lambda *args, **kwargs: False # noqa: ARG005 with container.override(Features, features_mock): yield features_mock @pytest.fixture(autouse=True) def _reset_features_mock(features_mock: mock.MagicMock) -> None: features_mock.is_on_for_identity = lambda *args, **kwargs: False # noqa: ARG005 @pytest.fixture def enable_features(features_mock: mock.MagicMock) -> EnableFeatures: @contextlib.contextmanager def wrapper(feature_flag_names: Sequence[str]) -> Iterator[None]: def _is_on_for_identity( feature_flag_name: str, *, identity_id: str, # noqa: ARG001 ) -> bool: return feature_flag_name in feature_flag_names features_mock.is_on_for_identity = _is_on_for_identity yield features_mock.is_on_for_identity = lambda *args, **kwargs: False # noqa: ARG005 return wrapper @pytest.fixture(scope="session", autouse=True) def graphql_router_client_mock(container: Container) -> Iterator[mock.MagicMock]: graphql_router_client_mock = mock.MagicMock(spec=GraphqlRouterClient) with container.override(GraphqlRouterClient, graphql_router_client_mock): yield graphql_router_client_mock @pytest.fixture def facebook_client_mock(container: Container) -> Iterator[mock.MagicMock]: facebook_client_mock = mock.MagicMock(spec=FacebookClient) with container.override(FacebookClient, facebook_client_mock): yield facebook_client_mock @pytest.fixture(scope="session", autouse=True) def google_client_mock(container: Container) -> Iterator[mock.MagicMock]: google_client_mock = mock.MagicMock(spec=GoogleClient) with container.override(GoogleClient, google_client_mock): yield google_client_mock @pytest.fixture(autouse=True) def _reset_google_client_mock(google_client_mock: mock.MagicMock) -> None: google_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) def tiktok_client_mock(container: Container) -> Iterator[mock.MagicMock]: tiktok_client_mock = mock.MagicMock(spec=TikTokClient) with container.override(TikTokClient, tiktok_client_mock): yield tiktok_client_mock @pytest.fixture(autouse=True) def _reset_tiktok_client_mock(tiktok_client_mock: mock.MagicMock) -> None: tiktok_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture def dummy_kms() -> BaseKMS: return DummyKMS() @pytest.fixture def identity(fake: FakerTyped) -> Identity: return Identity( id=fake.uuid4_string(), is_internal_employee=False, brand=DEFAULT_BRAND, ) @pytest.fixture def internal_employee_identity(fake: FakerTyped) -> Identity: return Identity( id=fake.uuid4_string(), is_internal_employee=True, brand=DEFAULT_BRAND, ) @pytest.fixture def identity_id(identity: Identity) -> str: return identity.id @pytest.fixture(scope="session") def account() -> Account: return Account(vendor_id=1, subaccount_id=0) @pytest.fixture def ows_client() -> Iterator[OwsClient]: yield OwsClient(environment="test", service_name="dmp") @pytest.fixture(scope="session", autouse=True) def ows_client_mock(container: Container) -> Iterator[OwsClient]: ows_client_mock = mock.MagicMock(spec=OwsClient) with container.override(OwsClient, ows_client_mock): yield ows_client_mock @pytest.fixture def cache(container: Container) -> Iterator[Cache]: cache = SimpleCache() with container.override(Cache, cache): yield cache @pytest.fixture(scope="session", autouse=True) def s3_client_mock(container: Container) -> Iterator[mock.MagicMock]: s3_client_mock = mock.MagicMock(spec=S3Client) with container.override(S3Client, instance=s3_client_mock): yield s3_client_mock @pytest.fixture(scope="session", autouse=True) def location_client_mock(container: Container) -> Iterator[mock.MagicMock]: location_client_mock = mock.MagicMock(spec=LocationClient) with container.override(LocationClient, instance=location_client_mock): yield location_client_mock @pytest.fixture(scope="session", autouse=True) def ows_account_client_mock(container: Container) -> Iterator[mock.MagicMock]: ows_account_client_mock = mock.MagicMock(spec=OwsAccountClient) with container.override(OwsAccountClient, instance=ows_account_client_mock): yield ows_account_client_mock @pytest.fixture(autouse=True) def _reset_ows_account_client_mock(ows_account_client_mock: mock.MagicMock) -> None: ows_account_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture def ows_notifications_client_mock(container: Container) -> Iterator[mock.MagicMock]: ows_notifications_client_mock = mock.MagicMock(spec=OwsNotificationsClient) with container.override( OwsNotificationsClient, instance=ows_notifications_client_mock ): yield ows_notifications_client_mock @pytest.fixture def ows_socials_client_mock(container: Container) -> Iterator[mock.MagicMock]: ows_socials_client_mock = mock.MagicMock(spec=OwsSocialsClient) with container.override(OwsSocialsClient, instance=ows_socials_client_mock): yield ows_socials_client_mock @pytest.fixture(scope="session", autouse=True) def fivetran_client_mock(container: Container) -> Iterator[mock.MagicMock]: fivetran_client_mock = mock.MagicMock(spec=FivetranClient) with container.override(FivetranClient, instance=fivetran_client_mock): yield fivetran_client_mock @pytest.fixture(autouse=True) def _reset_fivetran_client_mock(fivetran_client_mock: mock.MagicMock) -> None: fivetran_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session") def account_access(account: Account) -> AccountAccess: return AccountAccess(accounts=[account]) @pytest.fixture(scope="session", autouse=True) def auth_service_mock( container: Container, account_access: AccountAccess ) -> Iterator[mock.MagicMock]: auth_service_mock = mock.MagicMock(spec=AuthService) auth_service_mock.authorize_account.return_value = account_access auth_service_mock.authorize_optional_account.return_value = account_access auth_service_mock.authorize_for_permission.return_value = account_access auth_service_mock.get_account_access_for_permission.return_value = account_access with container.override(AuthService, instance=auth_service_mock): yield auth_service_mock @pytest.fixture(autouse=True) def _reset_auth_service_mock( auth_service_mock: mock.MagicMock, account_access: AccountAccess ) -> None: auth_service_mock.reset_mock(return_value=True, side_effect=True) auth_service_mock.authorize_account.return_value = account_access auth_service_mock.authorize_optional_account.return_value = account_access auth_service_mock.authorize_for_permission.return_value = account_access auth_service_mock.get_account_access_for_permission.return_value = account_access @pytest.fixture(scope="session") def docker_compose_file(pytestconfig: pytest.Config) -> list[pathlib.Path]: base_dir = pytestconfig.rootpath / "tests/unit" return [base_dir / "docker-compose.yaml"] @pytest.fixture(scope="session") def _docker_postgres_setup( docker_services: Services, docker_ip: str, settings: Settings ) -> None: port = docker_services.port_for("postgres", 5432) assert port == 7432 docker_services.wait_until_responsive( timeout=60.0, pause=0.5, check=lambda: check_db_alive( settings.postgres_url.set( host=docker_ip, port=port, ) ), ) @pytest.fixture(scope="session") def _docker_snowflake_setup( docker_services: Services, docker_ip: str, settings: Settings ) -> None: port = docker_services.port_for("snowflake", 5443) assert port == 7443 url = URL.create( drivername="postgresql+psycopg", username=settings.snowflake_user, password=settings.snowflake_password, host=docker_ip, port=port, database=settings.snowflake_database, ) docker_services.wait_until_responsive( timeout=60.0, pause=0.5, check=lambda: check_db_alive(url), ) @pytest.fixture(scope="session") def jinja2sql(container: Container, features_mock: mock.MagicMock) -> Jinja2SQL: jinja2sql = container.resolve(Jinja2SQL) jinja2sql.env.globals["features"] = features_mock with container.override(Jinja2SQL, instance=jinja2sql): return jinja2sql @pytest.fixture(scope="session") def db(request: pytest.FixtureRequest, container: Container) -> Iterator[DefaultDB]: db = container.resolve(DefaultDB) if ( db.engine.dialect.name.startswith("postgres") and request.config.getoption("docker_mode") == "auto" ): request.getfixturevalue("_docker_postgres_setup") Model.metadata.create_all(bind=db.engine) yield db db.close() Model.metadata.drop_all(bind=db.engine) @pytest.fixture(scope="session") def reporting_db( request: pytest.FixtureRequest, container: Container ) -> Iterator[ReportingDB]: db = container.resolve(ReportingDB) if ( db.engine.dialect.name.startswith("postgres") and request.config.getoption("docker_mode") == "auto" ): request.getfixturevalue("_docker_snowflake_setup") ReportingModel.metadata.create_all(bind=db.engine) # Add postgresql module to use levenshtein function # as a replacement to EDITDISTANCE snowflake function in test cases with db.engine.connect() as conn: conn.execute(text("CREATE EXTENSION IF NOT EXISTS cube;")) conn.execute(text("CREATE EXTENSION IF NOT EXISTS earthdistance;")) conn.execute(text("CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;")) conn.commit() yield db db.close() ReportingModel.metadata.drop_all(bind=db.engine) @pytest.fixture(autouse=True) def _db_marker(request: pytest.FixtureRequest) -> Iterator[None]: marker = request.node.get_closest_marker("db") if marker is None: yield return db: DefaultDB = request.getfixturevalue("db") reporting_db: ReportingDB = request.getfixturevalue("reporting_db") with ( db.global_context(), db.rollback_transaction(), reporting_db.global_context(), reporting_db.rollback_transaction(), ): yield # Factories @pytest.fixture(scope="session") def factory_service() -> FactoryService: factory_service = FactoryService() factory_service.scan("tests.unit.factories") return factory_service @pytest.fixture def build_model(factory_service: FactoryService) -> BuildModel: def wrapper[T](model: type[T], **kwargs: Any) -> T: return factory_service.build(model, **kwargs) return wrapper @pytest.fixture def build_model_batch(factory_service: FactoryService) -> BuildModelBatch: def wrapper[T](model: type[T], *, size: int, **kwargs: Any) -> list[T]: return factory_service.build_batch(model, size=size, **kwargs) return wrapper @pytest.fixture def create_model(factory_service: FactoryService, db: DefaultDB) -> CreateModel: def wrapper[T: Model](model: type[T], **kwargs: Any) -> T: return factory_service.create(db.session, model, **kwargs) return wrapper @pytest.fixture def create_model_batch( factory_service: FactoryService, db: DefaultDB ) -> CreateModelBatch: def wrapper[T: Model](model: type[T], *, size: int, **kwargs: Any) -> list[T]: return factory_service.create_batch(db.session, model, size=size, **kwargs) return wrapper @pytest.fixture def create_reporting_model( factory_service: FactoryService, reporting_db: ReportingDB ) -> CreateReportingModel: def wrapper[T: ReportingModel](model: type[T], **kwargs: Any) -> T: return factory_service.create(reporting_db.session, model, **kwargs) return wrapper @pytest.fixture def create_reporting_model_batch( factory_service: FactoryService, reporting_db: ReportingDB ) -> CreateReportingModelBatch: def wrapper[T: ReportingModel]( model: type[T], *, size: int, **kwargs: Any ) -> list[T]: return factory_service.create_batch( reporting_db.session, model, size=size, **kwargs ) return wrapper @pytest.fixture def mock_cortex_complete() -> Iterator[MagicMock]: target_path = "dmp.artists.handlers.get_artist_segments_explained.complete" with patch(target_path) as mock: mock.return_value = "This segment has high participation in campaigns" yield mock