fix(api): PUT /settings проверяет значения до записи, 422 (I-5); убран мёртвый except, честное имя теста
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
0dba6b7672
commit
4f055a80d0
@ -223,16 +223,20 @@ def h_settings_get(hub, conn, m, q, body):
|
|||||||
|
|
||||||
|
|
||||||
def h_settings_put(hub, conn, m, q, body):
|
def h_settings_put(hub, conn, m, q, body):
|
||||||
|
"""Записать настройки пакетом (всё или ничего). До записи проверяются защищённые ключи и значения
|
||||||
|
(settings.validate): ошибка — 422, версия настроек не меняется."""
|
||||||
bad = [k for k in body if k in settings.PROTECTED]
|
bad = [k for k in body if k in settings.PROTECTED]
|
||||||
if bad:
|
if bad:
|
||||||
raise ApiError(422, "protected", ", ".join(bad))
|
raise ApiError(422, "protected", ", ".join(bad))
|
||||||
|
for key, value in body.items():
|
||||||
|
try:
|
||||||
|
settings.validate(key, value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ApiError(422, "bad_setting", str(e))
|
||||||
version = settings.get(conn, "settings_version")
|
version = settings.get(conn, "settings_version")
|
||||||
try:
|
with db.transaction(conn):
|
||||||
with db.transaction(conn):
|
for key, value in body.items():
|
||||||
for key, value in body.items():
|
version = settings.set_(conn, key, value)
|
||||||
version = settings.set_(conn, key, value)
|
|
||||||
except ValueError as e: # запрет на защищённые ключи из settings.set_
|
|
||||||
raise ApiError(422, "protected", str(e))
|
|
||||||
return 200, {"settings_version": version}
|
return 200, {"settings_version": version}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,39 @@ AGENT_KEYS = ["base_jql", "project_key", "issue_types", "epic_name_field_id", "f
|
|||||||
PROTECTED = {"settings_version", "schema_version", "tokens"}
|
PROTECTED = {"settings_version", "schema_version", "tokens"}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_int(v) -> bool:
|
||||||
|
return isinstance(v, int) and not isinstance(v, bool)
|
||||||
|
|
||||||
|
|
||||||
|
def validate(key, value) -> None:
|
||||||
|
"""Проверить значение настройки до записи; ValueError — на 422 в API.
|
||||||
|
pacing — объект {тип команды: целое >= 0} только с известными типами команд;
|
||||||
|
backup_keep, auto_refresh_minutes, min_agent_version — целые >= 0; port — целое 1..65535;
|
||||||
|
остальные ключи — по типу значения по умолчанию (объект, список, строка, флаг), если оно не None;
|
||||||
|
неизвестные ключи и ключи с умолчанием None принимаются как есть."""
|
||||||
|
if key == "pacing":
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError("pacing — объект {тип команды: пауза в секундах}")
|
||||||
|
for ctype, pause in value.items():
|
||||||
|
if ctype not in DEFAULT_PACING:
|
||||||
|
raise ValueError(f"pacing: неизвестный тип команды «{ctype}»")
|
||||||
|
if not _is_int(pause) or pause < 0:
|
||||||
|
raise ValueError(f"pacing.{ctype}: ожидается целое >= 0")
|
||||||
|
return
|
||||||
|
if key == "port":
|
||||||
|
if not _is_int(value) or not 1 <= value <= 65535:
|
||||||
|
raise ValueError("port — целое 1..65535")
|
||||||
|
return
|
||||||
|
if key in ("backup_keep", "auto_refresh_minutes", "min_agent_version"):
|
||||||
|
if not _is_int(value) or value < 0:
|
||||||
|
raise ValueError(f"{key} — целое >= 0")
|
||||||
|
return
|
||||||
|
default = DEFAULTS.get(key)
|
||||||
|
if default is not None and type(value) is not type(default):
|
||||||
|
names = {dict: "объект", list: "список", str: "строка", bool: "true/false", int: "целое"}
|
||||||
|
raise ValueError(f"{key} — {names.get(type(default), type(default).__name__)}")
|
||||||
|
|
||||||
|
|
||||||
def ensure_defaults(conn) -> None:
|
def ensure_defaults(conn) -> None:
|
||||||
for key, value in DEFAULTS.items():
|
for key, value in DEFAULTS.items():
|
||||||
conn.execute("INSERT OR IGNORE INTO setting(key, value) VALUES (?, ?)", (key, _dump(value)))
|
conn.execute("INSERT OR IGNORE INTO setting(key, value) VALUES (?, ?)", (key, _dump(value)))
|
||||||
|
|||||||
@ -234,7 +234,7 @@ def test_bad_body_shapes_are_422(hub, path, body):
|
|||||||
assert e.value.status == 422
|
assert e.value.status == 422
|
||||||
|
|
||||||
|
|
||||||
def test_settings_put_invalid_is_4xx_and_atomic(hub):
|
def test_settings_put_protected_key_in_batch_rejects_whole_batch(hub):
|
||||||
c = hub["client"]
|
c = hub["client"]
|
||||||
v0 = c.get("/settings")["settings_version"]
|
v0 = c.get("/settings")["settings_version"]
|
||||||
with pytest.raises(HubError) as e:
|
with pytest.raises(HubError) as e:
|
||||||
@ -243,6 +243,33 @@ def test_settings_put_invalid_is_4xx_and_atomic(hub):
|
|||||||
assert c.get("/settings")["settings_version"] == v0
|
assert c.get("/settings")["settings_version"] == v0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("body", [
|
||||||
|
{"pacing": 5}, {"pacing": {"ensure_fields": "abc"}}, {"pacing": {"ensure_fields": -1}},
|
||||||
|
{"pacing": {"неизвестная": 1}}, {"port": "x"}, {"port": 0}, {"port": 70000}, {"backup_keep": -1},
|
||||||
|
{"auto_refresh_minutes": 1.5}, {"min_agent_version": "2"}, {"link_types": []}, {"base_jql": 5},
|
||||||
|
{"labels": {}}, {"initialized": 1},
|
||||||
|
{"pacing": {"refresh": 3}, "backup_keep": -1}, # хорошее рядом с плохим не записывается
|
||||||
|
])
|
||||||
|
def test_settings_put_invalid_value_is_422(hub, body):
|
||||||
|
c = hub["client"]
|
||||||
|
before = c.get("/settings")
|
||||||
|
with pytest.raises(HubError) as e:
|
||||||
|
c.put("/settings", body)
|
||||||
|
assert e.value.status == 422
|
||||||
|
assert c.get("/settings") == before
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_put_valid_values_pass(hub):
|
||||||
|
c = hub["client"]
|
||||||
|
v0 = c.get("/settings")["settings_version"]
|
||||||
|
out = c.put("/settings", {"pacing": {"refresh": 0, "ensure_fields": 90}, "backup_keep": 0, "port": 8800,
|
||||||
|
"auto_refresh_minutes": 15, "min_agent_version": 3, "link_types": {"x": "Y"},
|
||||||
|
"labels": ["AWPCA"], "board_id": 12})
|
||||||
|
assert out["settings_version"] == v0 + 8
|
||||||
|
got = c.get("/settings")
|
||||||
|
assert got["pacing"]["ensure_fields"] == 90 and got["port"] == 8800 and got["board_id"] == 12
|
||||||
|
|
||||||
|
|
||||||
def _run_refresh(hub, body, issues, neighbor_keys=()):
|
def _run_refresh(hub, body, issues, neighbor_keys=()):
|
||||||
"""Поставить refresh, взять его агентом, загрузить состояние, отчитаться done."""
|
"""Поставить refresh, взять его агентом, загрузить состояние, отчитаться done."""
|
||||||
hub["client"].post("/refresh", body)
|
hub["client"].post("/refresh", body)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user