fix(tests): реальный прогон интеграционных тестов СПОД против Postgres

Прогнал api/tests/integration/test_form2_ahr_satellite_spod_integration.py
против живого Postgres (perf/perf/perf на localhost:55432, БД dfip_1906,
alembic upgrade head + фикстура) — нашёл и поправил два бага самого теста
(фичу/миграцию не трогал):

- _find_row не учитывал служебную META-строку (editable/role/user_id без
  id/header), которую v3.v_form_view добавляет первой в любой лист —
  KeyError: 'id' на первой же выборке. Теперь ищет только среди INPUT-строк.
- test_satellite_spod_phase_gates_non_admin_user делал один тест из
  "запрет без fact_q4.spod → добавили ключ → разрешено": неадминский
  403/400 от isp1 внутри теста вызывает db_ref.rollback() в
  tests/integration/conftest.py (per-request откат на ошибке), который
  стирает все несохранённые изменения этого же теста — включая только
  что созданные строку сателлита и form_phase. Разбил на два независимых
  теста (test_satellite_spod_rejected_before_phase_includes_spod_column /
  test_satellite_spod_allowed_once_phase_includes_spod_column), в каждом
  ровно один запрос, которому разрешено провалиться.

pytest tests/integration — 157 passed, 1 failed (pre-existing,
не связан: test_backend_calls_form3_add_project_and_add_line_functions,
FORM_3/add_project, 400 на /api/v1/projects/.../report/.../line), 4 skipped.
Новый файл — 15/15.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-31 00:41:10 +04:00
parent d41668dc27
commit a5ffd476a6

View File

@ -62,7 +62,12 @@ def _get_sheet_rows(client, admin_tokens, form2_id, sheet) -> list[dict]:
def _find_row(rows: list[dict], predicate) -> dict:
# v_form_view prepends a META row ({"editable": [...], "role": ..., ...},
# row_type="META", depth=-1) with no "id"/"header" — only INPUT rows carry
# satellite/AHR line data.
for row in rows:
if row.get("row_type") != "INPUT":
continue
if predicate(row["data"]):
return row
raise AssertionError("строка не найдена")
@ -141,14 +146,22 @@ def test_satellite_spod_rejects_other_quarters(
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
def test_satellite_spod_phase_gates_non_admin_user(
def test_satellite_spod_rejected_before_phase_includes_spod_column(
client, admin_tokens, isp_tokens, form2_id, sheet
):
"""DFIP-пользователь (isp1): без 'fact_q4.spod' в этапе — отказ,
после появления ключа (как после data-миграции) — успех."""
"""DFIP-пользователь (isp1) с этапом, где column_keys ещё не включает
'fact_q4.spod' (как до data-миграции): запись отклоняется.
Note: a failed request (403/400) makes the shared test-session's
`_get_db` override call `db_ref.rollback()`, which would wipe out any
uncommitted setup done earlier in the *same* test (the satellite line,
the phase row) — see conftest.py's `client` fixture. So the "before"
and "after" migration scenarios must be two separate tests, each
ending in exactly one request that is allowed to fail.
"""
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
phase_code = f"spod_test_{sheet.lower()}"
phase_code = f"spod_test_before_{sheet.lower()}"
create_body = {
"budget_form_id": form2_id,
"sheet": sheet,
@ -173,9 +186,28 @@ def test_satellite_spod_phase_gates_non_admin_user(
)
assert response.status_code in (400, 403, 409), response.text
response = client.patch(
f"/api/v1/stages/form/{form2_id}/{sheet}/{phase_code}",
json={**create_body, "column_keys": ["fact_q4.dec", "fact_q4.spod"]},
@pytest.mark.parametrize("sheet", ["AHR_RENT", "AHR_UTILITY", "AHR_SECURITY"])
def test_satellite_spod_allowed_once_phase_includes_spod_column(
client, admin_tokens, isp_tokens, form2_id, sheet
):
"""DFIP-пользователь (isp1) с этапом, где column_keys уже включает
'fact_q4.spod' (как после data-миграции из этого PR): запись проходит."""
line_id = _add_satellite_line(client, admin_tokens, form2_id, sheet)
phase_code = f"spod_test_after_{sheet.lower()}"
response = client.post(
f"/api/v1/stages/form/{form2_id}",
json={
"budget_form_id": form2_id,
"sheet": sheet,
"phase_code": phase_code,
"role": "DFIP",
"column_keys": ["fact_q4.dec", "fact_q4.spod"],
"opens_at": "2020-01-01T00:00:00",
"closes_at": "2099-01-01T00:00:00",
"direction": None,
},
headers=_auth_headers(admin_tokens),
)
assert response.status_code == 200, response.text
@ -186,9 +218,3 @@ def test_satellite_spod_phase_gates_non_admin_user(
headers=_auth_headers(isp_tokens),
)
assert response.status_code == 200, response.text
response = client.delete(
f"/api/v1/stages/form/{form2_id}/{sheet}/{phase_code}",
headers=_auth_headers(admin_tokens),
)
assert response.status_code == 200, response.text