From f1dae5a8114efc9e966be279f653425873b06143 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 26 Oct 2026 02:32:39 +0400 Subject: [PATCH] =?UTF-8?q?API:=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B2=D0=B8=D0=BA=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5=D1=80=D0=BD=D0=BE=D0=B9=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=8B=20=E2=80=94=20422,=20=D0=B0=20=D0=BD=D0=B5=20500?= =?UTF-8?q?=20(#129,=20#42=20T2.1,=20#94=20T8.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mirror.check_issues проверяет форму issues до записи; /wiki/sync отвергает project не строку. Co-Authored-By: Claude Opus 5.5 --- src/sdlc_hub/modules/jira/api.py | 1 + src/sdlc_hub/modules/jira/mirror.py | 9 +++++++++ src/sdlc_hub/modules/wiki/api.py | 2 ++ tests/test_api.py | 14 ++++++++++++++ tests/test_wiki_api.py | 9 +++++++++ 5 files changed, 35 insertions(+) diff --git a/src/sdlc_hub/modules/jira/api.py b/src/sdlc_hub/modules/jira/api.py index 7e372c5..be5bbeb 100644 --- a/src/sdlc_hub/modules/jira/api.py +++ b/src/sdlc_hub/modules/jira/api.py @@ -24,6 +24,7 @@ def h_agent_state(hub, conn, m, q, body): issues = body.get("issues") or [] boards = body.get("boards") try: + mirror.check_issues(issues) mirror.check_comments(issues) # до записи: неверная пачка не применяется целиком mirror.check_attachments(issues) mirror.check_agile_fields(issues) diff --git a/src/sdlc_hub/modules/jira/mirror.py b/src/sdlc_hub/modules/jira/mirror.py index 59f7b2a..1f238a8 100644 --- a/src/sdlc_hub/modules/jira/mirror.py +++ b/src/sdlc_hub/modules/jira/mirror.py @@ -32,6 +32,15 @@ def check_comments(issues) -> None: if c.get(f) is not None and not isinstance(c[f], str): raise ValueError(f"{where}.{f} — строка или null") +def check_issues(issues) -> None: + """Форма списка задач состояния: список объектов с непустым строковым key. Нарушение — ValueError.""" + if not isinstance(issues, list): + raise ValueError("issues — список задач") + for n, it in enumerate(issues): + if not isinstance(it, dict) or not isinstance(it.get("key"), str) or not it["key"]: + raise ValueError(f"issues[{n}] — объект задачи с key") + + def check_attachments(issues) -> None: """Форма поля attachments у задач состояния (агент v14+); поле необязательно. Нарушение — ValueError с ключом и полем.""" for it in issues: diff --git a/src/sdlc_hub/modules/wiki/api.py b/src/sdlc_hub/modules/wiki/api.py index aa7e9a7..d9eac34 100644 --- a/src/sdlc_hub/modules/wiki/api.py +++ b/src/sdlc_hub/modules/wiki/api.py @@ -94,6 +94,8 @@ def h_search(hub, conn, m, q, body): def h_sync(hub, conn, m, q, body): + if body.get("project") is not None and not isinstance(body["project"], str): + raise ApiError(422, "bad_request", "project — строка") q = dict(q, project=body.get("project")) author = body.get("author") or "api" if not isinstance(author, str): diff --git a/tests/test_api.py b/tests/test_api.py index 74cf6e5..0338d96 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1299,3 +1299,17 @@ def test_rank_before_trimmed_on_claim(hub): {"type": "ensure_rank", "params": {"key": "X-1", "before": " X-2 "}}]}) cmd = hub["agent"].post("/agent/claim", {})["command"] assert cmd["type"] == "ensure_rank" and cmd["params"]["before"] == "X-2" + + +@pytest.mark.parametrize("issues, detail", [ + (["X-1"], "issues[0] — объект задачи с key"), + ([{"summary": "без ключа"}], "issues[0] — объект задачи с key"), + ([{"key": 5}], "issues[0] — объект задачи с key"), + ([{"key": ""}], "issues[0] — объект задачи с key"), + ({"key": "X-1"}, "issues — список задач"), + ("X-1", "issues — список задач"), +]) +def test_agent_state_bad_issue_shape_422(hub, issues, detail): + with pytest.raises(HubError) as e: + hub["agent"].post("/agent/state", {"issues": issues, "links": []}) + assert e.value.status == 422 and e.value.error == "bad_state" and detail in e.value.detail diff --git a/tests/test_wiki_api.py b/tests/test_wiki_api.py index 055061d..743ac74 100644 --- a/tests/test_wiki_api.py +++ b/tests/test_wiki_api.py @@ -228,3 +228,12 @@ def test_page_attachment_failed_error_visible(api): c.close() att = api.client.get("/wiki/pages/DOC-3", project="AWPCA")["attachments"][0] assert att["state"] == "failed" and att["error"] == "Eva: 404" + + +@pytest.mark.parametrize("project", [5, ["AWPCA"], {"a": 1}, True]) +def test_sync_project_not_string_422(api, project): + first_sync(api) + before = api.rows("SELECT id FROM batch") + e = err(api.client.post, "/wiki/sync", {"project": project}) + assert e.status == 422 and e.error == "bad_request" and e.detail == "project — строка" + assert api.rows("SELECT id FROM batch") == before # пакета не создано