From 8798f092c4ff8bbb279502e4583ebeb624927565 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 11 Nov 2026 16:53:42 +0400 Subject: [PATCH] =?UTF-8?q?fix(stages):=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20dollar-=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=20=D0=B2=20DO-=D0=B1=D0=BB=D0=BE=D0=BA=D0=B5=200059,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D1=8D=D1=82=D0=B0=D0=BF?= =?UTF-8?q?=D0=BE=D0=B2=20(AURORA-1879)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sql_migration._split_statements узнаёт только $tag$, пустой $$ ломал разбиение: DO $$ … $$ → DO $do$ … $do$. - test_form_phases_delete_removes_only_named_phase: ?sheet= без direction фильтрует direction IS NULL — читаем весь список и отбираем AHR в тесте. - test_form_phases_duplicate_without_direction_rejected: после ошибки сессия клиента откатывается, число строк не проверяем; защиту БД проверяет новый test_form_phase_nodir_unique_index_blocks_duplicate. Co-Authored-By: Claude Opus 5.5 --- .../sql/0059_form_phase_direction_fix.sql | 4 +-- .../integration/test_form_phases_api_smoke.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/api/alembic/versions/sql/0059_form_phase_direction_fix.sql b/api/alembic/versions/sql/0059_form_phase_direction_fix.sql index 28d9db5..0f0455b 100644 --- a/api/alembic/versions/sql/0059_form_phase_direction_fix.sql +++ b/api/alembic/versions/sql/0059_form_phase_direction_fix.sql @@ -3,7 +3,7 @@ -- 2. direction в идентификаторе этапа в аудите (add/upd/del), direction в payload upd. -- 3. del_form_phase удаляет только этап с указанным phase_code. -DO $$ +DO $do$ DECLARE v_deleted INT; BEGIN @@ -20,7 +20,7 @@ BEGIN GET DIAGNOSTICS v_deleted = ROW_COUNT; RAISE NOTICE 'form_phase: удалено дублей этапов без направления: %', v_deleted; END -$$; +$do$; CREATE UNIQUE INDEX IF NOT EXISTS form_phase_nodir_uidx ON v3.form_phase (budget_form_id, sheet, phase_code) diff --git a/api/tests/integration/test_form_phases_api_smoke.py b/api/tests/integration/test_form_phases_api_smoke.py index c736f50..d17ef42 100644 --- a/api/tests/integration/test_form_phases_api_smoke.py +++ b/api/tests/integration/test_form_phases_api_smoke.py @@ -493,8 +493,13 @@ def test_form_phases_delete_removes_only_named_phase(client, admin_tokens): response = client.delete("/api/v1/stages/form/1/AHR/T_same?direction=Support", headers=headers) assert response.status_code == 200 - result = client.get("/api/v1/stages/form/1?sheet=AHR", headers=headers).json()["result"] - left = {(p["phase_code"], p["direction"]) for p in result if p["phase_code"] in ("T_same", "T_other")} + # ?sheet= без direction фильтрует direction IS NULL — берём весь список и отбираем AHR здесь. + result = client.get("/api/v1/stages/form/1", headers=headers).json()["result"] + left = { + (p["phase_code"], p["direction"]) + for p in result + if p["sheet"] == "AHR" and p["phase_code"] in ("T_same", "T_other") + } assert left == {("T_other", "Support"), ("T_same", "Development")} @@ -517,9 +522,23 @@ def test_form_phases_duplicate_without_direction_rejected(client, admin_tokens): second = client.post("/api/v1/stages/form/1", json=body, headers=headers) assert 400 <= second.status_code < 500 assert "phase_code" in second.text + # После ошибки клиентская сессия откатывается целиком (tests/integration/conftest.py), + # поэтому число строк здесь не проверяем — защиту БД проверяет следующий тест. - result = client.get("/api/v1/stages/form/1?sheet=OPER&phase_code=T_dup", headers=headers).json()["result"] - assert len(result) == 1 + +def test_form_phase_nodir_unique_index_blocks_duplicate(client, svod2_sql): + rows = svod2_sql( + "SELECT budget_form_id, sheet, phase_code FROM v3.form_phase WHERE direction IS NULL LIMIT 1" + ) + if not rows: + pytest.skip("Нет этапа без направления в текущей БД") + form_id, sheet, phase_code = rows[0] + with pytest.raises(Exception, match="form_phase_nodir_uidx"): + svod2_sql( + "INSERT INTO v3.form_phase SELECT * FROM v3.form_phase " + "WHERE budget_form_id = :f AND sheet = :s AND phase_code = :p AND direction IS NULL", + {"f": form_id, "s": sheet, "p": phase_code}, + ) def test_form_phases_patch_audit_has_direction(client, admin_tokens, svod2_sql):