from __future__ import annotations import json import sys from datetime import date from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) FIXTURES = ROOT / "tests" / "fixtures" FIXTURE_ISIN = "RU000A106169" FIXTURE_DATE = date(2026, 10, 8) @pytest.fixture def fixtures_dir() -> Path: """Сохранённые реальные ответы MOEX ISS / ЦБ (байт в байт, см. .gitattributes).""" return FIXTURES @pytest.fixture def session_local(tmp_path: Path, monkeypatch): """Временный SQLite со схемой из миграций (с сидами) вместо SessionLocal во всех модулях сервисов. Postgres не нужен.""" from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from alembic import command from alembic.config import Config from app.core.config import settings from app.services import ( business_calendar, funding, fx_rates, manual_operations, market_data, positions, quik_import, request_log, securities, ) db_url = f"sqlite+aiosqlite:///{(tmp_path / 'drrk.db').as_posix()}" monkeypatch.setattr(settings, "DATABASE_URL", db_url) monkeypatch.setattr(settings, "DATABASE_SCHEMA", "") cfg = Config() cfg.set_main_option("script_location", str(ROOT / "alembic")) command.upgrade(cfg, "head") factory = async_sessionmaker( bind=create_async_engine(db_url, poolclass=NullPool), autoflush=False, expire_on_commit=False, ) for module in ( business_calendar, securities, market_data, fx_rates, funding, positions, request_log, quik_import, manual_operations, ): monkeypatch.setattr(module, "SessionLocal", factory) return factory @pytest.fixture def add_trade(session_local): """add_trade(ISIN, код операции, дата, количество, портфель="UW") — сделка напрямую в БД, без файла. Бумага уже в справочнике.""" from decimal import Decimal from sqlalchemy import select from app.db.base import run_db from app.db.models import Operation, Security from app.repositories.reference import ReferenceRepository async def _add( isin: str, code: str, on_date: date, quantity: str, portfolio: str ) -> None: async with session_local() as session: reference = ReferenceRepository(session) security = ( await session.execute(select(Security).where(Security.isin == isin)) ).scalar_one() session.add( Operation( security_id=security.id, portfolio_id=await reference.get_portfolio_id(portfolio), operation_type_id=(await reference.operation_type_ids())[code], currency_id=security.currency_id, trade_date=on_date, quantity=Decimal(quantity), amount=Decimal("1000"), created_by="system", updated_by="system", ) ) await session.commit() def add_trade( isin: str, code: str, on_date: date, quantity: str, portfolio: str = "UW" ) -> None: run_db(_add(isin, code, on_date, quantity, portfolio)) return add_trade @pytest.fixture def fixture_moex(fixtures_dir: Path): """Фейковый MOEX, отвечающий реальными фикстурами РЭО-02 за 08.10.2026.""" from app.integrations.moex import parse_history, parse_security from tests.unit.fakes import FakeMoex, search_hit security_payload = json.loads( (fixtures_dir / "moex_security_RU000A106169.json").read_text("utf-8") ) history_payload = json.loads( (fixtures_dir / "moex_history_RU000A106169_2026-10-08.json").read_text("utf-8") ) return FakeMoex( search={FIXTURE_ISIN: search_hit(FIXTURE_ISIN, FIXTURE_ISIN)}, securities={FIXTURE_ISIN: parse_security(security_payload)}, history={ FIXTURE_ISIN: parse_history(history_payload, FIXTURE_DATE, ["TQCB", "TQOB"]) }, )