Хаб: маршруты GET/POST /projects, создание проекта ставит полный refresh (#61)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
203c7e7b3c
commit
f60aecd889
@ -250,3 +250,9 @@
|
||||
раскладки AWPCA. Цена ошибки: схема ключей в `storage.ts`.
|
||||
21. 2026-10-19 — **`last_sync` после создания проекта — `null` до завершения его обновления, `sync_run.query` не
|
||||
переписывается.** Обоснование: прежний прогон новую метку не покрывал. Цена ошибки: временная надпись на доске.
|
||||
22. 2026-10-19 — **пустой `author` в `POST /projects` — как `api`, нестрока — 422 `bad_project`.** Обоснование: как у
|
||||
остальных маршрутов (`body.get("author") or "api"`); спека требует 422 для не-строки. Цена ошибки: пустая строка
|
||||
пишется автором `api`, а не отвергается.
|
||||
23. 2026-10-19 — **`CommandError` из постановки refresh в `POST /projects` — 422 `bad_batch`, а не `bad_project`.**
|
||||
Обоснование: ошибка относится к пакету, а не к полям проекта; спека называет только `project_exists` и
|
||||
`bad_project`. Цена ошибки: код ошибки в ответе.
|
||||
|
||||
@ -393,6 +393,30 @@ def h_settings_put(hub, conn, m, q, body):
|
||||
return 200, {**settings.get_all(conn), "base_jql": projects.base_jql(conn)}
|
||||
|
||||
|
||||
def h_projects_get(hub, conn, m, q, body):
|
||||
return 200, {"projects": projects.list_projects(conn)}
|
||||
|
||||
|
||||
def h_projects_post(hub, conn, m, q, body):
|
||||
"""Новый проект: проект и пакет «Новый проект <метка>» (один refresh по базовой JQL с новой меткой) — в одной
|
||||
транзакции; ошибка любого шага не оставляет ни проекта, ни пакета (hub-api, «Маршруты проектов»)."""
|
||||
author = body.get("author") or "api"
|
||||
if not isinstance(author, str):
|
||||
raise ApiError(422, "bad_project", "author — строка")
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
project = projects.create(conn, body.get("label"), body.get("name"), body.get("board_id"))
|
||||
batch = queue.create_batch(conn, f"Новый проект {project['label']}", author,
|
||||
[{"type": "refresh", "params": {"jql": projects.base_jql(conn)}}])
|
||||
except projects.ProjectExists as e:
|
||||
raise ApiError(409, "project_exists", str(e))
|
||||
except projects.ProjectError as e:
|
||||
raise ApiError(422, "bad_project", str(e))
|
||||
except CommandError as e:
|
||||
raise ApiError(422, "bad_batch", str(e))
|
||||
return 201, {"project": project, "batch_id": batch["id"]}
|
||||
|
||||
|
||||
ROUTES = [
|
||||
("GET", r"^/(links)?$", h_root, None),
|
||||
("GET", r"^/app(?:/(.*))?$", h_app, None),
|
||||
@ -420,6 +444,8 @@ ROUTES = [
|
||||
("GET", r"^/sync-runs$", h_sync_runs, "client"),
|
||||
("GET", r"^/settings$", h_settings_get, "client"),
|
||||
("PUT", r"^/settings$", h_settings_put, "client"),
|
||||
("GET", r"^/projects$", h_projects_get, "client"),
|
||||
("POST", r"^/projects$", h_projects_post, "client"),
|
||||
]
|
||||
_COMPILED = [(m, re.compile(p), fn, scope) for m, p, fn, scope in ROUTES]
|
||||
|
||||
|
||||
@ -780,3 +780,53 @@ def test_issues_hide_closed(hub):
|
||||
assert keys() == ["A-1", "A-2", "A-3", "A-4"]
|
||||
c.put("/settings", {"closed_statuses": ["Готово"]})
|
||||
assert keys(hide_closed="1") == ["A-1", "A-3", "A-4"]
|
||||
|
||||
|
||||
def test_create_project(hub): # сценарий «Новый проект»
|
||||
c = hub["client"]
|
||||
conn = hub["hub"].conn(); version = settings.get(conn, "settings_version"); conn.close()
|
||||
c.post("/projects", {"label": "AWPCA", "name": "АРМ КА", "board_id": 4216})
|
||||
out = c.post("/projects", {"label": "STOCKS_DRRK", "name": "Запасы ДРРК", "board_id": 5120, "author": "viewer"})
|
||||
assert out["project"]["label"] == "STOCKS_DRRK" and out["project"]["board_type"] is None
|
||||
got = c.get("/projects")["projects"]
|
||||
assert [p["label"] for p in got] == ["AWPCA", "STOCKS_DRRK"] and got[1]["issues_count"] == 0
|
||||
b = c.get(f"/batches/{out['batch_id']}")
|
||||
assert b["author"] == "viewer" and b["title"] == "Новый проект STOCKS_DRRK" and b["project"] is None
|
||||
assert b["commands"][0]["params"] == {"jql": "project = AURORA AND labels in (AWPCA, STOCKS_DRRK)"}
|
||||
assert c.get("/settings")["settings_version"] == version # «Создание проекта не трогает версию настроек»
|
||||
assert c.get("/settings")["base_jql"] == "project = AURORA AND labels in (AWPCA, STOCKS_DRRK)"
|
||||
|
||||
|
||||
def test_create_project_without_board(hub):
|
||||
c = hub["client"]
|
||||
out = c.post("/projects", {"label": "STOCKS_DRRK", "name": "Запасы ДРРК"})
|
||||
assert out["project"]["board_id"] is None and out["project"]["board_type"] is None
|
||||
b = c.get(f"/batches/{out['batch_id']}")
|
||||
assert b["author"] == "api"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("body, status, field", [
|
||||
({"label": "AWPCA", "name": "x"}, 409, "label"),
|
||||
({"label": "awpca", "name": "x"}, 409, "label"),
|
||||
({"label": "STOCKS DRRK", "name": "x"}, 422, "label"),
|
||||
({"label": "STOCKS_DRRK", "name": " "}, 422, "name"),
|
||||
({"label": "STOCKS_DRRK", "name": "x", "board_id": "5120"}, 422, "board_id"),
|
||||
({"label": "STOCKS_DRRK", "name": "x", "author": 5}, 422, None),
|
||||
])
|
||||
def test_create_project_errors(hub, body, status, field):
|
||||
c = hub["client"]
|
||||
c.post("/projects", {"label": "AWPCA", "name": "АРМ КА"})
|
||||
before = len(c.get("/batches")["batches"])
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post("/projects", body)
|
||||
assert e.value.status == status and (field is None or e.value.detail.startswith(field))
|
||||
assert len(c.get("/projects")["projects"]) == 1 and len(c.get("/batches")["batches"]) == before
|
||||
|
||||
|
||||
def test_projects_need_client_token(hub):
|
||||
with pytest.raises(HubError) as e:
|
||||
hub["agent"].get("/projects")
|
||||
assert e.value.status == 401
|
||||
with pytest.raises(HubError) as e:
|
||||
hub["agent"].post("/projects", {"label": "STOCKS_DRRK", "name": "x"})
|
||||
assert e.value.status == 401
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user