From 600163a9ee880c757ad5594a7cb2cf15d4405802 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 22:23:00 +0400 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=D1=82=D0=B8=D0=BF=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=B5=D0=B9=20=D1=82=D0=B5=D0=BB=D0=B0=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B4=D0=B0=D1=8E=D1=82?= =?UTF-8?q?=20422,=20=D0=B0=20=D0=BD=D0=B5=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- src/jira_hub/api.py | 28 ++++++++++++++++++++++------ tests/test_api.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/jira_hub/api.py b/src/jira_hub/api.py index dd71448..a66b193 100644 --- a/src/jira_hub/api.py +++ b/src/jira_hub/api.py @@ -142,8 +142,12 @@ def h_note_put(hub, conn, m, q, body): def h_batches_post(hub, conn, m, q, body): - if not body.get("title"): - raise ApiError(422, "bad_batch", "нет title") + if not body.get("title") or not isinstance(body["title"], str): + raise ApiError(422, "bad_batch", "title — непустая строка") + if not isinstance(body.get("author") or "api", str): + raise ApiError(422, "bad_batch", "author — строка") + if not isinstance(body.get("commands"), list): + raise ApiError(422, "bad_batch", "commands — список команд") try: return 201, queue.create_batch(conn, body["title"], body.get("author") or "api", body.get("commands")) except CommandError as e: @@ -166,6 +170,8 @@ def h_batch_cancel(hub, conn, m, q, body): def h_command_retry(hub, conn, m, q, body): + if not isinstance(body.get("author") or "api", str): + raise ApiError(422, "bad_retry", "author — строка") try: return 201, queue.retry_command(conn, _int(m), body.get("author") or "api") except KeyError: @@ -184,12 +190,19 @@ def h_command_cancel(hub, conn, m, q, body): def h_refresh(hub, conn, m, q, body): + if not isinstance(body.get("author") or "api", str): + raise ApiError(422, "bad_refresh", "author — строка") + keys = body.get("keys") + if keys is not None and (not isinstance(keys, list) or not all(isinstance(k, str) and k for k in keys)): + raise ApiError(422, "bad_refresh", "keys — список строк") + if body.get("jql") is not None and not isinstance(body["jql"], str): + raise ApiError(422, "bad_refresh", "jql — строка") if body.get("full"): params = {"jql": settings.get(conn, "base_jql")} elif body.get("jql"): params = {"jql": body["jql"]} elif body.get("keys"): - params = {"keys": list(body["keys"])} + params = {"keys": keys} else: raise ApiError(422, "bad_refresh", "нужно full, jql или keys") return 201, queue.create_batch(conn, "Обновление", body.get("author") or "api", [{"type": "refresh", "params": params}]) @@ -213,9 +226,12 @@ def h_settings_put(hub, conn, m, q, body): if bad: raise ApiError(422, "protected", ", ".join(bad)) version = settings.get(conn, "settings_version") - with db.transaction(conn): - for key, value in body.items(): - version = settings.set_(conn, key, value) + try: + with db.transaction(conn): + for key, value in body.items(): + version = settings.set_(conn, key, value) + except ValueError as e: # запрет на защищённые ключи из settings.set_ + raise ApiError(422, "protected", str(e)) return 200, {"settings_version": version} diff --git a/tests/test_api.py b/tests/test_api.py index e404d1d..00c55d7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -221,3 +221,23 @@ def test_claim_long_poll_times_out_with_null(hub): t0 = time.monotonic() assert hub["agent"].post("/agent/claim", {})["command"] is None assert 1.0 <= time.monotonic() - t0 < 4 + + +@pytest.mark.parametrize("path,body", [ + ("/refresh", {"keys": 5}), ("/refresh", {"keys": "A-1"}), ("/refresh", {"keys": [1]}), ("/refresh", {"jql": 5}), + ("/batches", {"title": "x", "commands": 5}), ("/batches", {"title": "x", "commands": "x"}), + ("/batches", {"title": ["x"], "commands": []}), ("/batches", {"title": "x", "author": 5, "commands": []}), +]) +def test_bad_body_shapes_are_422(hub, path, body): + with pytest.raises(HubError) as e: + hub["client"].post(path, body) + assert e.value.status == 422 + + +def test_settings_put_invalid_is_4xx_and_atomic(hub): + c = hub["client"] + v0 = c.get("/settings")["settings_version"] + with pytest.raises(HubError) as e: + c.put("/settings", {"pacing": {"refresh": 3}, "tokens": {}}) + assert 400 <= e.value.status < 500 + assert c.get("/settings")["settings_version"] == v0