2026-11-08 11:02:38 +04:00

185 lines
6.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()
@pytest.fixture
def isp2_tokens(client, isp_password: str):
# Исполнитель в org_unit формы id=1 (isp1 в нём намеренно не состоит — на этом
# держатся тесты «исполнитель из чужой организации»). Нужен вместе с isp3_tokens,
# чтобы проверять, что исполнитель не может удалить чужую строку.
response = client.post(
"/api/v1/auth/login",
json={"username": "isp2", "password": isp_password},
)
assert response.status_code == 200
return response.json()
@pytest.fixture
def isp3_tokens(client, isp_password: str):
# Второй исполнитель в том же org_unit, что и isp2 (форма id=1).
response = client.post(
"/api/v1/auth/login",
json={"username": "isp3", "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)