import contextlib import pathlib from collections.abc import Iterator from typing import Any from unittest import mock import faker import pytest import sqlalchemy as sa from anydi import Container from fansifter_common.adapters.db.utils import check_db_alive from fansifter_common.adapters.ows_account import OwsAccountClient from fansifter_common.adapters.sendgrid import SendGridClient from fansifter_common.adapters.stripo import StripoClient 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 import Services from app.adapters.aws.s3 import S3Client from app.adapters.db import Database, Model from app.config import Settings, settings as global_settings from app.container import container as global_container from app.services import SeedlistService from tests.unit.module import TestModule from tests.unit.types import BuildModel, CreateModel, 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(scope="session") def container() -> Container: global_container.register_module(TestModule) return global_container @pytest.fixture(scope="session") def settings() -> Settings: return global_settings @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_snowflake_setup( docker_services: Services, docker_ip: str, settings: Settings ) -> None: port = docker_services.port_for("snowflake", 5443) assert port == 7443 url = sa.URL.create( drivername="postgresql+psycopg", username=settings.snowflake_user, password=settings.snowflake_password.get_secret_value() if settings.snowflake_password else None, 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[Database]: db = container.resolve(Database) 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(autouse=True) def _db_marker(request: pytest.FixtureRequest) -> Iterator[None]: """Use the `db` marker to run a test in a transaction.""" marker = request.node.get_closest_marker("db") if not marker: yield return db: Database = request.getfixturevalue("db") with db.global_context(), db.rollback_transaction(): yield @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("app.config.settings.__wrapped__", new_settings), container.override(Settings, new_settings), ): yield return wrapper @pytest.fixture(scope="session") def ows_account_client_mock() -> mock.MagicMock: return mock.MagicMock(spec=OwsAccountClient) @pytest.fixture(scope="session") def sendgrid_client_mock(_session_faker: faker.Faker) -> mock.MagicMock: return mock.MagicMock(spec=SendGridClient) @pytest.fixture(autouse=True) def _reset_sendgrid_client_mock(sendgrid_client_mock: mock.MagicMock) -> None: sendgrid_client_mock.reset_mock(return_value=True, side_effect=True) @pytest.fixture(scope="session") def stripo_client_mock(_session_faker: faker.Faker) -> mock.MagicMock: stripo_client_mock = mock.MagicMock(spec=StripoClient) return 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") def preference_center_encrypter_mock() -> mock.MagicMock: return mock.MagicMock(spec=JWTEncrypter) @pytest.fixture(scope="session") def legal_info_service_mock() -> mock.MagicMock: return mock.MagicMock(spec=LegalInfoService) @pytest.fixture(scope="session") def translation_service_mock() -> mock.MagicMock: return mock.MagicMock(spec=TranslationService) @pytest.fixture(scope="session") def s3_client_mock() -> mock.MagicMock: return mock.MagicMock(spec=S3Client) @pytest.fixture(scope="session") def seedlist_service_mock() -> mock.MagicMock: return mock.MagicMock(spec=SeedlistService) @pytest.fixture(autouse=True) def _reset_seedlist_service_mock(seedlist_service_mock: mock.MagicMock) -> None: seedlist_service_mock.reset_mock(side_effect=True) @pytest.fixture(scope="session", autouse=True) def override_dependencies( container: Container, sendgrid_client_mock: mock.MagicMock, stripo_client_mock: mock.MagicMock, ows_account_client_mock: mock.MagicMock, preference_center_encrypter_mock: mock.MagicMock, legal_info_service_mock: mock.MagicMock, translation_service_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, seedlist_service_mock: mock.MagicMock, ) -> None: """Override container dependencies.""" container.register( OwsAccountClient, lambda: ows_account_client_mock, scope="singleton", override=True, ) container.register( SendGridClient, lambda: sendgrid_client_mock, scope="singleton", override=True, ) container.register( StripoClient, lambda: stripo_client_mock, scope="singleton", override=True, ) container.register( JWTEncrypter, lambda: preference_center_encrypter_mock, scope="singleton", override=True, ) container.register( LegalInfoService, lambda: legal_info_service_mock, scope="singleton", override=True, ) container.register( TranslationService, lambda: translation_service_mock, scope="singleton", override=True, ) container.register( S3Client, lambda: s3_client_mock, scope="singleton", override=True, ) container.register( SeedlistService, lambda: seedlist_service_mock, scope="singleton", override=True, ) @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 create_model(factory_service: FactoryService, db: Database) -> CreateModel: def wrapper[T: Model](model: type[T], **kwargs: Any) -> T: return factory_service.create(db.session, model, **kwargs) return wrapper