Andrey Epifancev 79c841dcaf Merge origin/feat/220 в feat/236 (сервер): свод 2 постранично поверх test (AURORA-1843)
Конфликты summary.py, summary_repository.py, summary_service.py,
test_summary2_related_records_sql.py: сохранены новые методы страниц и фасетов,
режим без параметров идёт по быстрому JSON-пути test (AURORA-1848).
Миграция переименована в 0057 (down_revision 0056); в svod2_lines добавлена
поправка 0050 (СПОД аренды, только ГО). Эталон svod2_legacy_golden перезаписан
под ответ без null-полей; legacy_full оракулов строится прямым вызовом функции.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:02:30 +04:00

161 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
from contextlib import contextmanager
import os
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
from src.db.base import SessionLocal
from src.db.session import get_db
from src.main import app
@pytest.fixture
def db_session():
"""Сессия БД теста: её же использует client, всё откатывается в конце теста."""
db_ref = SessionLocal()
yield db_ref
@pytest.fixture
def client(db_session):
db_ref = db_session
loop_ref = None
async def _get_db():
nonlocal loop_ref
loop_ref = asyncio.get_running_loop()
try:
yield db_ref
await db_ref.flush()
except:
await db_ref.rollback()
raise
app.dependency_overrides[get_db] = _get_db
with TestClient(app) as test_client:
try:
test_client.db_session = db_ref
yield test_client
finally:
if db_ref is not None and loop_ref:
async def cleanup():
await db_ref.rollback()
await db_ref.close()
future = asyncio.run_coroutine_threadsafe(cleanup(), loop_ref)
future.result()
app.dependency_overrides.clear()
@pytest.fixture
def admin_password() -> str:
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
@pytest.fixture
def isp_password() -> str:
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
return os.getenv("OPENBAO__TEST_EXECUTOR_PASSWORD", "admin123")
@pytest.fixture
def admin_tokens(client, admin_password: str):
response = client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": admin_password},
)
assert response.status_code == 200
return response.json()
@pytest.fixture
def auth_headers():
def _build(tokens: dict) -> dict:
return {"Authorization": f"Bearer {tokens['access_token']}"}
return _build
@pytest.fixture
def isp_tokens(client, isp_password: str):
response = client.post(
"/api/v1/auth/login",
json={"username": "isp1", "password": isp_password},
)
assert response.status_code == 200
return response.json()
SVOD2_YEAR = 2031
SVOD2_SEED_PATH = os.path.join(os.path.dirname(__file__), "..", "sqls", "svod2_seed.sql")
def legacy_full(client, sheet: str, headers: dict = None, org_ids=None) -> list[dict]:
"""Прежний полный ответ свода 2 без параметров нового режима (оракульная база).
Строится прямым вызовом функции представления, а не через GET без параметров: после AURORA-1848
этот GET отдаёт data без null-полей (jsonb_strip_nulls), а оракул экрана «до» работает с полным
data - в нём пустая секция contract_detail затирает поля contract_summary значением null.
Запрос идёт от имени администратора, headers оставлен для совместимости вызовов.
"""
function_name = {"GO": "v3.v_svod2_go", "RF": "v3.v_svod2_rf"}[sheet]
async def _fetch():
result = await client.db_session.execute(
text(
f"SELECT sort_order, data FROM {function_name}("
"CAST(:year AS INT), CAST(:org_ids AS INT[]), NULL, NULL, 'org_name', 'asc')"
),
{"year": SVOD2_YEAR, "org_ids": org_ids},
)
return [
{"row_type": "INPUT", "depth": 3, "sort_order": sort_order, "data": data}
for sort_order, data in result.fetchall()
]
return client.portal.call(_fetch)
@pytest.fixture
def svod2_sql(client):
"""Выполняет text(sql) на сессии клиента (тот же цикл событий, что у запросов)."""
def _run(sql: str, params: dict | None = None):
async def _exec():
result = await client.db_session.execute(text(sql), params or {})
return list(result.fetchall()) if result.returns_rows else []
return client.portal.call(_exec)
return _run
@pytest.fixture
def svod2_data(client, svod2_sql) -> dict:
"""Засевает данные свода 2 (откатываются вместе с сессией клиента)."""
with open(SVOD2_SEED_PATH, "r", encoding="utf-8") as f:
script = f.read()
for statement in script.split(";"):
statement = statement.strip()
if statement:
svod2_sql(statement)
# id строки contract_detail: contract_detail.line_id = 910002, технический id,
# который попадает в шапку строки, — expense_item_id этой строки
cd_id = svod2_sql("SELECT expense_item_id FROM v3.budget_line WHERE id = 910002")[0][0]
yield {
"year": SVOD2_YEAR,
"org_a": 910001,
"org_b": 910002,
"org_b_twin": 910003,
"org_b_case": 910004,
"cd_id": cd_id,
"ckk_only_value": "777.25",
}
# откат, пока portal открыт: иначе при отсутствии запросов к API транзакция засева
# остаётся висеть и блокирует следующий засев
client.portal.call(client.db_session.rollback)