import contextlib import importlib import os import pathlib from collections.abc import AsyncIterator, Iterator from typing import Any, TypeVar from unittest import mock import psycopg import pytest import pyxdi from audience_common.auth.account import AccountAccess from pytest_docker.plugin import Services from sqlalchemy.engine import URL from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_utils import create_database, database_exists, drop_database from campaigns import bootstrap from campaigns.auth.dtos import User, UserId from campaigns.auth.services import AuthService from campaigns.config import settings from campaigns.connectors.aws.kms import BaseKMS, DummyKMS from campaigns.connectors.aws.s3 import S3Client from campaigns.connectors.aws.sts import STSClient from campaigns.connectors.db import Database from campaigns.connectors.db.orm import Base from campaigns.connectors.facebook.base import FacebookClient from campaigns.connectors.ows_account import OwsAccount from campaigns.connectors.ows_product import OwsProduct from tests.unit.faker import FakerTyped from tests.unit.services import FactoryService from tests.unit.types import ( BuildModel, BuildModelBatch, CreateModel, CreateModelBatch, MockDependency, ) T = TypeVar("T") @pytest.fixture(scope="session", 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.yml"] def _postgres_check_alive(url: URL, port: int) -> bool: try: conn = psycopg.connect( host=url.host, port=url.port or port, user=url.username, password=str(url.password), dbname=url.database, ) cur = conn.cursor() cur.execute("SELECT 1") conn.close() except psycopg.DatabaseError: return False return True @pytest.fixture(scope="session") def _docker_postgres_setup(docker_services: Services, docker_ip: str) -> str: port = docker_services.port_for("fansifter-postgres", 5432) assert port == 5402 url = settings.postgres_url.set(host=docker_ip, port=port) docker_services.wait_until_responsive( timeout=60.0, pause=0.5, check=lambda: _postgres_check_alive(url, port=port), ) return url.render_as_string(hide_password=False) @pytest.fixture(scope="session") def _config_postgres_url() -> str: return settings.postgres_url.render_as_string(hide_password=False) RUN_WITH_LINT_AND_TEST = os.getenv("RUN_WITH_LINT_AND_TEST") if RUN_WITH_LINT_AND_TEST: @pytest.fixture(scope="session") def postgres_url(_config_postgres_url: str) -> str: return _config_postgres_url else: @pytest.fixture(scope="session") def postgres_url(_docker_postgres_setup: str) -> str: return _docker_postgres_setup @pytest.fixture(scope="session", autouse=True) def scan_orm_models() -> None: for model_path in settings.orm_models: importlib.import_module(model_path) @pytest.fixture(scope="session") async def _db(postgres_url: str) -> AsyncIterator[Database]: if not database_exists(postgres_url): create_database(postgres_url) class TestDatabase(Database): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self._session: AsyncSession | None = None @property def session(self) -> AsyncSession: if self._session is None: raise return self._session @property def is_connected(self) -> bool: return self._session is not None @contextlib.contextmanager def set_session(self, session: AsyncSession) -> Iterator[None]: self._session = session yield self._session = None db = TestDatabase( url=postgres_url, session_args={"autoflush": True, "expire_on_commit": True}, template_searchpath=settings.jinjasql_template_searchpath, ) async with db.engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield db await db.close() async with db.engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) drop_database(postgres_url) @pytest.fixture(scope="session") def di(_db: Database) -> Iterator[pyxdi.PyxDI]: di = bootstrap.configure() with di.override(Database, _db): yield di @pytest.fixture async def db(_db: Database) -> AsyncIterator[Database]: # noqa async with _db.session_context(in_transaction=True): yield _db await _db.session.rollback() @pytest.fixture def fake() -> FakerTyped: return FakerTyped() @pytest.fixture def mock_dependency(di: pyxdi.PyxDI, request: pytest.FixtureRequest) -> MockDependency: def _factory(interface: Any) -> mock.Mock: m = mock.MagicMock(spec=interface) stack = contextlib.ExitStack() cm = di.override(interface, instance=m) stack.enter_context(cm) def _finalize() -> None: stack.close() request.addfinalizer(_finalize) return m return _factory @pytest.fixture def identity_id(fake: FakerTyped) -> str: return fake.uuid4_string() @pytest.fixture def profile_id(fake: FakerTyped) -> int: return fake.integer() @pytest.fixture def account_access() -> AccountAccess: return AccountAccess.full_access() @pytest.fixture def user_id(identity_id: str, profile_id: int) -> UserId: return UserId(identity_id=identity_id, profile_id=profile_id) @pytest.fixture def user(user_id: UserId, account_access: AccountAccess) -> User: return User(id=user_id, account_access=account_access) @pytest.fixture def dummy_kms() -> BaseKMS: return DummyKMS() @pytest.fixture def s3_client_mock() -> mock.MagicMock: return mock.MagicMock(spec=S3Client) @pytest.fixture def sts_client_mock() -> mock.MagicMock: return mock.MagicMock(spec=STSClient) @pytest.fixture def ows_product_mock() -> mock.MagicMock: return mock.MagicMock(spec=OwsProduct) @pytest.fixture def ows_account_mock() -> mock.MagicMock: return mock.MagicMock(spec=OwsAccount) @pytest.fixture def facebook_client_mock() -> mock.MagicMock: return mock.MagicMock(spec=FacebookClient) @pytest.fixture def auth_service_mock(account_access: AccountAccess) -> AuthService: async def _authorize_account(*args: Any, **kwargs: Any) -> AccountAccess: return account_access return mock.MagicMock( spec=AuthService, authorize_account=_authorize_account, ) @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 impl(model: type[T], **kwargs: Any) -> T: return factory_service.build(model, **kwargs) return impl @pytest.fixture def build_model_batch(factory_service: FactoryService) -> BuildModelBatch: def impl(model: type[T], *, size: int, **kwargs: Any) -> list[T]: return factory_service.build_batch(model, size=size, **kwargs) return impl @pytest.fixture async def create_model(factory_service: FactoryService, db: Database) -> CreateModel: async def impl(model: type[T], **kwargs: Any) -> T: return await factory_service.create( db.session, model, persistence="flush", **kwargs ) return impl @pytest.fixture async def create_model_batch( factory_service: FactoryService, db: Database ) -> CreateModelBatch: async def impl(model: type[T], *, size: int, **kwargs: Any) -> list[T]: return await factory_service.create_batch( db.session, model, size=size, persistence="flush", **kwargs ) return impl