import contextlib import logging.config import pathlib from collections.abc import Iterator, Sequence from typing import Any from unittest import mock import faker import pytest from anydi import Container from fansifter_common.adapters.graphql_router import GraphqlRouterClient from fansifter_common.adapters.ows_account import OwsAccountClient from fansifter_common.adapters.ows_users import OwsUsersClient from fansifter_common.adapters.sendgrid import SendGridClient from fansifter_common.adapters.stripo import StripoClient from fansifter_common.auth.account import Account, AccountAccess from fansifter_common.auth.services import AuthService from fansifter_common.auth.validators import AccountValidator from fansifter_common.encrypter import JWTEncrypter from fansifter_common.legal_info.services import LegalInfoService from fansifter_common.testing.factories import FactoryService from fansifter_common.translation.services import TranslationService from pytest_docker.plugin import Services from sqlalchemy.engine import URL from email_campaigns.adapters.db import DB, Model, PgDB, PgModel from email_campaigns.adapters.db.utils import check_db_alive from email_campaigns.adapters.features import Features from email_campaigns.adapters.stripo import ( AutomatedStripoClient, AutomatedStripoClientV2, CampaignStripoClient, CampaignStripoClientV2, ) from email_campaigns.config import settings from email_campaigns.container import container as global_container from email_campaigns.emails.cache import EmailPublicPreviewCache from email_campaigns.fonts.cache import GoogleFontsCache from email_campaigns.fonts.services.google_fonts import GoogleFont, GoogleFontsService from tests.unit.module import TestModule from tests.unit.types import ( BuildModel, BuildModelBatch, CreateModel, CreateModelBatch, CreatePgModel, CreatePgModelBatch, EnableFeatures, ) 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 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 container() -> Container: global_container.register_module(TestModule) return global_container @pytest.fixture(scope="session", autouse=True) def configure_logging() -> None: logging.config.dictConfig(settings.logging_config) @pytest.fixture(scope="session") def _docker_postgres_setup(docker_services: Services, docker_ip: str) -> 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) -> 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 db(request: pytest.FixtureRequest, container: Container) -> Iterator[DB]: db = container.resolve(DB) if ( db.engine.dialect.name.startswith("postgres") and request.config.getoption("docker_mode") == "auto" ): request.getfixturevalue("_docker_snowflake_setup") Model.metadata.create_all(bind=db.engine) yield db db.close() Model.metadata.drop_all(bind=db.engine) @pytest.fixture(scope="session") def pg_db(request: pytest.FixtureRequest, container: Container) -> Iterator[PgDB]: db = container.resolve(PgDB) if ( db.engine.dialect.name.startswith("postgres") and request.config.getoption("docker_mode") == "auto" ): request.getfixturevalue("_docker_postgres_setup") PgModel.metadata.create_all(bind=db.engine) yield db db.close() PgModel.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: DB = request.getfixturevalue("db") pg_db: DB = request.getfixturevalue("pg_db") with ( db.global_context(), db.rollback_transaction(), pg_db.global_context(), pg_db.rollback_transaction(), ): yield @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, 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(scope="session", autouse=True) def ows_users_client_mock(container: Container) -> Iterator[mock.MagicMock]: ows_users_client_mock = mock.MagicMock(spec=OwsUsersClient) with container.override(OwsUsersClient, ows_users_client_mock): yield ows_users_client_mock @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(scope="session", autouse=True) def sendgrid_client_mock(container: Container) -> Iterator[mock.MagicMock]: sendgrid_client_mock = mock.MagicMock(spec=SendGridClient) with container.override(SendGridClient, sendgrid_client_mock): yield sendgrid_client_mock @pytest.fixture(scope="session", autouse=True) def stripo_client_mock(container: Container) -> Iterator[mock.MagicMock]: stripo_client_mock = mock.MagicMock(spec=StripoClient) with container.override(StripoClient, stripo_client_mock): yield stripo_client_mock @pytest.fixture(autouse=True) def _reset_stripo_client_mock(stripo_client_mock: mock.MagicMock) -> None: stripo_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) def campaign_stripo_client_mock(container: Container) -> Iterator[mock.MagicMock]: campaign_stripo_client_mock = mock.MagicMock(spec=CampaignStripoClient) with container.override(CampaignStripoClient, campaign_stripo_client_mock): yield campaign_stripo_client_mock @pytest.fixture(autouse=True) def _reset_campaign_stripo_client_mock( campaign_stripo_client_mock: mock.MagicMock, ) -> None: campaign_stripo_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) def automated_stripo_client_mock(container: Container) -> Iterator[mock.MagicMock]: automated_stripo_client_mock = mock.MagicMock(spec=AutomatedStripoClient) with container.override(AutomatedStripoClient, automated_stripo_client_mock): yield automated_stripo_client_mock @pytest.fixture(autouse=True) def _reset_automated_stripo_client_mock( automated_stripo_client_mock: mock.MagicMock, ) -> None: automated_stripo_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) def campaign_stripo_client_v2_mock( container: Container, ) -> Iterator[mock.MagicMock]: campaign_stripo_client_v2_mock = mock.MagicMock(spec=CampaignStripoClientV2) with container.override(CampaignStripoClientV2, campaign_stripo_client_v2_mock): yield campaign_stripo_client_v2_mock @pytest.fixture(autouse=True) def _reset_campaign_stripo_client_v2_mock( campaign_stripo_client_v2_mock: mock.MagicMock, ) -> None: campaign_stripo_client_v2_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) def automated_stripo_client_v2_mock( container: Container, ) -> Iterator[mock.MagicMock]: automated_stripo_client_v2_mock = mock.MagicMock(spec=AutomatedStripoClientV2) with container.override(AutomatedStripoClientV2, automated_stripo_client_v2_mock): yield automated_stripo_client_v2_mock @pytest.fixture(autouse=True) def _reset_automated_stripo_client_v2_mock( automated_stripo_client_v2_mock: mock.MagicMock, ) -> None: automated_stripo_client_v2_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session", autouse=True) 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 preference_center_encrypter_mock(container: Container) -> mock.MagicMock: preference_center_encrypter_mock = mock.MagicMock(spec=JWTEncrypter) container.register( JWTEncrypter, lambda: preference_center_encrypter_mock, scope="singleton", override=True, ) return preference_center_encrypter_mock @pytest.fixture(scope="session", autouse=True) def legal_info_service_mock(container: Container) -> Iterator[mock.MagicMock]: legal_info_service_mock = mock.MagicMock(spec=LegalInfoService) with container.override(LegalInfoService, legal_info_service_mock): yield legal_info_service_mock @pytest.fixture(scope="session", autouse=True) def translation_service_mock(container: Container) -> Iterator[mock.MagicMock]: from fansifter_common.translation.types import Terms translation_service_mock = mock.MagicMock(spec=TranslationService) # Configure mock to return proper structure terms_mock = Terms( optInInfo="test opt in info", optInInfoExternal="test opt in info external", unsubscribe="test unsubscribe", ) translation_service_mock.get_translations_by_country_code.return_value = { "en": terms_mock } with container.override(TranslationService, translation_service_mock): yield translation_service_mock @pytest.fixture(scope="session", autouse=True) def campaign_public_preview_cache_mock(container: Container) -> mock.MagicMock: cache_mock = mock.MagicMock(spec=EmailPublicPreviewCache) cache_mock.get.return_value = None container.register( EmailPublicPreviewCache, lambda: cache_mock, scope="singleton", override=True, ) return cache_mock def _make_google_font(name: str) -> GoogleFont: return GoogleFont( name=name, fallback_font="Arial", generic_font_family="sans-serif", css_font_family=f"'{name}', Arial, sans-serif", url=f"https://fonts.googleapis.com/css2?family={name}", ) @pytest.fixture(scope="session", autouse=True) def google_fonts_cache_mock(container: Container) -> mock.MagicMock: cache_mock = mock.MagicMock(spec=GoogleFontsCache) cache_mock.is_populated.return_value = True cache_mock.search.return_value = [] cache_mock.get_font.side_effect = _make_google_font container.register( GoogleFontsCache, lambda: cache_mock, scope="singleton", override=True, ) return cache_mock @pytest.fixture(autouse=True) def _reset_google_fonts_cache_mock(google_fonts_cache_mock: mock.MagicMock) -> None: google_fonts_cache_mock.reset_mock(return_value=True, side_effect=True) google_fonts_cache_mock.is_populated.return_value = True google_fonts_cache_mock.search.return_value = [] google_fonts_cache_mock.get_font.side_effect = _make_google_font @pytest.fixture(scope="session", autouse=True) def google_fonts_service_mock(container: Container) -> mock.MagicMock: service_mock = mock.MagicMock(spec=GoogleFontsService) service_mock.get_fonts.return_value = [] container.register( GoogleFontsService, lambda: service_mock, scope="singleton", override=True, ) return service_mock @pytest.fixture(autouse=True) def _reset_google_fonts_service_mock(google_fonts_service_mock: mock.MagicMock) -> None: google_fonts_service_mock.reset_mock(return_value=True, side_effect=True) google_fonts_service_mock.get_fonts.return_value = [] @pytest.fixture(scope="session") def account() -> Account: return Account(vendor_id=1, subaccount_id=0) @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 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 @pytest.fixture(scope="session", autouse=True) def account_validator_mock(container: Container) -> Iterator[mock.MagicMock]: account_validator_mock = mock.MagicMock(spec=AccountValidator) with container.override(AccountValidator, instance=account_validator_mock): yield account_validator_mock @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: DB) -> 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: DB) -> 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_pg_model(factory_service: FactoryService, pg_db: PgDB) -> CreatePgModel: def wrapper[T: PgModel](model: type[T], **kwargs: Any) -> T: return factory_service.create(pg_db.session, model, **kwargs) return wrapper @pytest.fixture def create_pg_model_batch( factory_service: FactoryService, pg_db: PgDB ) -> CreatePgModelBatch: def wrapper[T: PgModel](model: type[T], *, size: int, **kwargs: Any) -> list[T]: return factory_service.create_batch(pg_db.session, model, size=size, **kwargs) return wrapper @pytest.fixture def identity_id(faker: faker.Faker) -> str: return faker.uuid4()