test: чётность частей 07–09 с DBML, полнота 43 таблиц и 27 enum, partial unique consolidation_lock на PG16
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
48525731ff
commit
ef55485dfc
@ -3,6 +3,7 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import uuid
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
@ -40,7 +41,6 @@ PART_04_06_TABLES = {
|
||||
"financial_statement_line",
|
||||
"financial_statement_value",
|
||||
}
|
||||
EXPECTED_TABLE_COUNT = 25 # на этом этапе: 16 (части 01–03) + 9 (части 04–06); задача 8 добавит остальные
|
||||
|
||||
|
||||
def orm_tables() -> dict[str, Table]:
|
||||
@ -48,8 +48,7 @@ def orm_tables() -> dict[str, Table]:
|
||||
|
||||
|
||||
def test_part_04_06_tables_registered_in_metadata() -> None:
|
||||
assert set(orm_tables()) == PART_01_03_TABLES | PART_04_06_TABLES
|
||||
assert len(Base.metadata.sorted_tables) == EXPECTED_TABLE_COUNT
|
||||
assert PART_01_03_TABLES | PART_04_06_TABLES <= set(orm_tables()) # полный состав и счёт — в тесте части 07–09
|
||||
assert all(t.schema == "dkb" for t in Base.metadata.sorted_tables)
|
||||
|
||||
|
||||
@ -78,7 +77,9 @@ def test_fk_cycle_is_broken_by_use_alter_without_warning() -> None:
|
||||
job_fk = next(f for f in orm_tables()["recognition_job"].foreign_keys if f.parent.name == "file_id")
|
||||
assert job_fk.constraint is not None
|
||||
assert job_fk.constraint.use_alter is False
|
||||
names = [t.name for t in Base.metadata.sorted_tables] # pytest -W error: SAWarning о цикле стал бы ошибкой
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error") # SAWarning о неразрешённом цикле зависимостей стал бы ошибкой
|
||||
names = [t.name for t in Base.metadata.sorted_tables]
|
||||
assert names.index("stored_file") < names.index("recognition_job")
|
||||
|
||||
|
||||
@ -90,7 +91,7 @@ def test_document_unique_index_is_nulls_not_distinct() -> None:
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_create_all_and_behavior_on_scratch_db(scratch_db_url: str) -> None:
|
||||
"""`create_all` частей 01–06 (25 таблиц); NULLS NOT DISTINCT и Decimal работают на реальной PostgreSQL 16."""
|
||||
"""`create_all`; таблицы частей 01–06 созданы; NULLS NOT DISTINCT и Decimal работают на реальной PostgreSQL 16."""
|
||||
engine = create_async_engine(scratch_db_url)
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
@ -99,8 +100,7 @@ async def test_create_all_and_behavior_on_scratch_db(scratch_db_url: str) -> Non
|
||||
|
||||
async with engine.connect() as conn:
|
||||
tables = await conn.run_sync(lambda c: set(inspect(c).get_table_names(schema="dkb")))
|
||||
assert tables == PART_01_03_TABLES | PART_04_06_TABLES
|
||||
assert len(tables) == EXPECTED_TABLE_COUNT
|
||||
assert PART_01_03_TABLES | PART_04_06_TABLES <= tables
|
||||
fk_names = {
|
||||
row[0]
|
||||
for row in await conn.execute(
|
||||
|
||||
241
api/tests/test_models_part_07_09.py
Normal file
241
api/tests/test_models_part_07_09.py
Normal file
@ -0,0 +1,241 @@
|
||||
"""ORM частей 07–09 DBML v5.13 (консолидация, КФО, аудит/ЕХ/прочее) и ИТОГОВАЯ полнота всех 43 таблиц."""
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
|
||||
from src.models import Base
|
||||
from src.models.access import AppUser
|
||||
from src.models.consolidation import Consolidation, ConsolidationLock, ConsolidationVersion
|
||||
from src.models.directories import CompanyGroup, RegionalOffice
|
||||
from src.models.enums import (
|
||||
ENUMS,
|
||||
ConsolidationApproach,
|
||||
ConsolidationScenario,
|
||||
ConsolidationStatus,
|
||||
PeriodQuarter,
|
||||
)
|
||||
from tests.dbml_helpers import load_dbml
|
||||
from tests.dbml_parity import find_problems
|
||||
from tests.test_models_part_01_03 import PART_01_03_TABLES, PG_IDENTIFIER_LIMIT
|
||||
from tests.test_models_part_04_06 import PART_04_06_TABLES
|
||||
|
||||
PART_07_09_TABLES = {
|
||||
# consolidation
|
||||
"consolidation",
|
||||
"consolidation_version",
|
||||
"consolidation_status_history",
|
||||
"consolidation_lock",
|
||||
"consolidation_member",
|
||||
"consolidation_document",
|
||||
# kfo
|
||||
"correction",
|
||||
"goodwill",
|
||||
"kfo_result",
|
||||
"kfo_result_line",
|
||||
"kfo_result_line_company",
|
||||
"client_kfo_line",
|
||||
# audit_ex (+ required_document_rule по рулингу R-4)
|
||||
"comment",
|
||||
"audit_event",
|
||||
"ex_snapshot",
|
||||
"ex_snapshot_member",
|
||||
"ex_snapshot_item",
|
||||
"required_document_rule",
|
||||
}
|
||||
ALL_TABLES = PART_01_03_TABLES | PART_04_06_TABLES | PART_07_09_TABLES
|
||||
EXPECTED_TABLE_COUNT = 43
|
||||
EXPECTED_ENUM_COUNT = 27
|
||||
|
||||
|
||||
def test_part_07_09_tables_are_new() -> None:
|
||||
assert PART_07_09_TABLES.isdisjoint(PART_01_03_TABLES | PART_04_06_TABLES)
|
||||
|
||||
|
||||
def test_orm_matches_dbml_part_07_09() -> None:
|
||||
"""Колонки (имя, порядок, тип, nullable, PK, unique, default), FK и индексы (вкл. partial WHERE) — как в DBML."""
|
||||
assert find_problems(Base.metadata, PART_07_09_TABLES) == []
|
||||
|
||||
|
||||
def test_all_constraint_names_fit_postgres_limit() -> None:
|
||||
"""Имена всех ограничений и индексов всех таблиц metadata заданы и не длиннее 63 символов."""
|
||||
for table in Base.metadata.sorted_tables:
|
||||
for constraint in [*table.constraints, *table.indexes]:
|
||||
assert constraint.name, f"{table.name}: безымянное ограничение"
|
||||
assert len(str(constraint.name)) <= PG_IDENTIFIER_LIMIT, f"{table.name}: {constraint.name}"
|
||||
|
||||
|
||||
def test_consolidation_lock_index_is_partial_unique_on_version() -> None:
|
||||
table = Base.metadata.tables["dkb.consolidation_lock"]
|
||||
index = next(i for i in table.indexes if i.name == "uq_consolidation_lock_version_active")
|
||||
assert index.unique is True
|
||||
assert [c.name for c in index.columns] == ["consolidation_version_id"]
|
||||
assert str(index.dialect_options["postgresql"]["where"]) == "released_at IS NULL"
|
||||
|
||||
|
||||
def test_composite_primary_keys() -> None:
|
||||
tables = Base.metadata.tables
|
||||
assert [c.name for c in tables["dkb.kfo_result_line_company"].primary_key] == [
|
||||
"kfo_result_line_id",
|
||||
"legal_entity_id",
|
||||
]
|
||||
assert [c.name for c in tables["dkb.ex_snapshot_member"].primary_key] == ["ex_snapshot_id", "legal_entity_id"]
|
||||
|
||||
|
||||
def test_ex_snapshot_has_no_fk_to_consolidation() -> None:
|
||||
"""v5.12: срез ЕХ самодостаточен — ни одна таблица ex_snapshot* не ссылается на консолидацию/КФО."""
|
||||
consolidation_side = {
|
||||
"consolidation",
|
||||
"consolidation_version",
|
||||
"consolidation_status_history",
|
||||
"consolidation_lock",
|
||||
"consolidation_member",
|
||||
"consolidation_document",
|
||||
"correction",
|
||||
"goodwill",
|
||||
"kfo_result",
|
||||
"kfo_result_line",
|
||||
"kfo_result_line_company",
|
||||
"client_kfo_line",
|
||||
}
|
||||
for name in ("ex_snapshot", "ex_snapshot_member", "ex_snapshot_item"):
|
||||
for fk in Base.metadata.tables[f"dkb.{name}"].foreign_keys:
|
||||
assert fk.column.table.name not in consolidation_side, f"{name}.{fk.parent.name}"
|
||||
assert not Base.metadata.tables["dkb.ex_snapshot"].c.source_version_id.foreign_keys
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- итоговая полнота моделей (43 таблицы)
|
||||
|
||||
|
||||
def test_all_43_tables_registered_and_match_dbml_names() -> None:
|
||||
"""Состав metadata == таблицы DBML v5.13 (имена буквально), всего 43, все в схеме dkb."""
|
||||
tables = list(Base.metadata.sorted_tables)
|
||||
assert len(tables) == EXPECTED_TABLE_COUNT
|
||||
assert all(t.schema == "dkb" for t in tables)
|
||||
dbml_names = {t.name for t in load_dbml().tables}
|
||||
assert len(dbml_names) == EXPECTED_TABLE_COUNT
|
||||
assert {t.name for t in tables} == dbml_names
|
||||
assert ALL_TABLES == dbml_names # разбиение по частям покрывает модель без пропусков и дублей
|
||||
|
||||
|
||||
def test_all_27_enums_defined() -> None:
|
||||
assert len(ENUMS) == EXPECTED_ENUM_COUNT
|
||||
assert set(ENUMS) == {e.name for e in load_dbml().enums}
|
||||
|
||||
|
||||
def test_orm_matches_dbml_for_all_43_tables() -> None:
|
||||
"""Сквозная сверка всех 43 таблиц одним вызовом компаратора."""
|
||||
assert find_problems(Base.metadata, ALL_TABLES) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- интеграция: PostgreSQL 16
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_create_all_43_tables_on_scratch_db(scratch_db_url: str) -> None:
|
||||
"""`create_all` всех 43 таблиц; 27 enum-типов в dkb; partial unique `consolidation_lock` работает поведением."""
|
||||
engine = create_async_engine(scratch_db_url)
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS dkb"))
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with engine.connect() as conn:
|
||||
tables = await conn.run_sync(lambda c: set(inspect(c).get_table_names(schema="dkb")))
|
||||
assert len(tables) == EXPECTED_TABLE_COUNT
|
||||
assert tables == ALL_TABLES
|
||||
enums = {
|
||||
row[0]
|
||||
for row in await conn.execute(
|
||||
text(
|
||||
"SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace "
|
||||
"WHERE n.nspname = 'dkb' AND t.typtype = 'e'"
|
||||
)
|
||||
)
|
||||
}
|
||||
assert len(enums) == EXPECTED_ENUM_COUNT
|
||||
assert enums == set(ENUMS)
|
||||
indexdef = (
|
||||
await conn.execute(
|
||||
text(
|
||||
"SELECT indexdef FROM pg_indexes "
|
||||
"WHERE schemaname = 'dkb' AND indexname = 'uq_consolidation_lock_version_active'"
|
||||
)
|
||||
)
|
||||
).scalar_one()
|
||||
assert "UNIQUE" in indexdef
|
||||
assert "WHERE (released_at IS NULL)" in indexdef
|
||||
# у ex_snapshot.source_version_id нет FK; составные PK созданы
|
||||
fk_columns = {
|
||||
row[0]
|
||||
for row in await conn.execute(
|
||||
text(
|
||||
"SELECT a.attname FROM pg_constraint c "
|
||||
"JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey) "
|
||||
"WHERE c.contype = 'f' AND c.conrelid = 'dkb.ex_snapshot'::regclass"
|
||||
)
|
||||
)
|
||||
}
|
||||
assert "source_version_id" not in fk_columns
|
||||
pk = await conn.run_sync(lambda c: inspect(c).get_pk_constraint("ex_snapshot_member", schema="dkb"))
|
||||
assert pk["constrained_columns"] == ["ex_snapshot_id", "legal_entity_id"]
|
||||
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
await _check_active_lock_is_unique(session)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def _check_active_lock_is_unique(session: AsyncSession) -> None:
|
||||
"""Два активных lock на одну версию -> IntegrityError; после `released_at` новый lock допустим."""
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
office = RegionalOffice(name="ГО")
|
||||
session.add(office)
|
||||
await session.flush()
|
||||
user = AppUser(full_name="Аналитик", regional_office_id=office.id)
|
||||
group = CompanyGroup(code="G1", name="ГК", valid_from=now)
|
||||
session.add_all([user, group])
|
||||
await session.flush()
|
||||
consolidation = Consolidation(
|
||||
company_group_id=group.id, year=2025, quarter=PeriodQuarter.Q1, reporting_date=datetime.date(2025, 4, 1)
|
||||
)
|
||||
session.add(consolidation)
|
||||
await session.flush()
|
||||
version = ConsolidationVersion(
|
||||
consolidation_id=consolidation.id,
|
||||
version_no=1,
|
||||
status=ConsolidationStatus.INITIATED,
|
||||
scenario=ConsolidationScenario.CREATE,
|
||||
initiator_id=user.id,
|
||||
initiator_office_id=office.id,
|
||||
)
|
||||
session.add(version)
|
||||
await session.flush()
|
||||
await session.refresh(version)
|
||||
assert version.approach == ConsolidationApproach.BASIC # server_default 'BASIC'
|
||||
assert version.goodwill_enabled is False
|
||||
assert version.is_in_work is True
|
||||
version_id, user_id = version.id, user.id
|
||||
await session.commit()
|
||||
|
||||
def lock() -> ConsolidationLock:
|
||||
return ConsolidationLock(consolidation_version_id=version_id, locked_by=user_id, locked_at=now)
|
||||
|
||||
first = lock()
|
||||
session.add(first)
|
||||
await session.flush()
|
||||
first_id = first.id
|
||||
session.add(lock())
|
||||
with pytest.raises(IntegrityError):
|
||||
await session.flush() # второй активный lock на ту же версию
|
||||
await session.rollback()
|
||||
|
||||
await session.execute(
|
||||
text("UPDATE dkb.consolidation_lock SET released_at = :now WHERE id = :id"), {"now": now, "id": first_id}
|
||||
)
|
||||
session.add(lock()) # после снятия первой блокировки — можно
|
||||
await session.flush()
|
||||
await session.rollback()
|
||||
Loading…
x
Reference in New Issue
Block a user