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 # пакета не создано