sdlc-hub/tests/test_api.py
Andrey Epifancev 2558248dc9 feat(hub): HTTP API — маршруты агента и интерфейсов, токены, долгий claim, клиент
Соединение SQLite на запрос, сравнение токенов в постоянное время,
400 на битый JSON и нечисловые параметры.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
2026-10-11 22:15:59 +04:00

154 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import threading
import pytest
from jira_hub import api, db, settings
from jira_hub.client import HubClient, HubError
@pytest.fixture
def hub(tmp_path):
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
h.claim_wait = 0
conn = h.conn()
settings.set_(conn, "link_types", {"связанные": "ASUPR_Связанные"})
tok = settings.tokens(conn)
conn.close()
srv = api.make_server(h, "127.0.0.1", 0)
th = threading.Thread(target=srv.serve_forever, daemon=True)
th.start()
base = f"http://127.0.0.1:{srv.server_port}"
yield {"hub": h, "agent": HubClient(base, tok["agent"]), "client": HubClient(base, tok["client"]), "base": base}
srv.shutdown()
srv.server_close()
th.join(5)
def test_auth_scopes(hub):
with pytest.raises(HubError) as e:
hub["agent"].get("/issues")
assert e.value.status == 401
with pytest.raises(HubError) as e:
hub["client"].post("/agent/claim", {})
assert e.value.status == 401
with pytest.raises(HubError) as e:
HubClient(hub["base"], "wrong").get("/issues")
assert e.value.status == 401
with pytest.raises(HubError) as e:
hub["client"].get("/nope")
assert e.value.status == 404
def test_agent_flow(hub):
c = hub["client"]
b = c.post("/batches", {"title": "срок", "author": "claude",
"commands": [{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23"}}]})
assert b["count"] == 2
a = hub["agent"]
claim = a.post("/agent/claim", {"agent_version": 1, "settings_version": 1})
cmd = claim["command"]
assert cmd["type"] == "ensure_fields" and cmd["pause_seconds"] == 120 and claim["settings_version"] >= 1
assert a.get(f"/agent/commands/{cmd['id']}")["id"] == cmd["id"]
assert a.get("/agent/settings")["link_types"]["связанные"] == "ASUPR_Связанные"
assert a.post("/agent/claim", {})["command"] is None # одна в полёте, долгий запрос выключен
r = a.post(f"/agent/commands/{cmd['id']}/result", {"status": "done", "result": {"changed": ["duedate"]}, "jira_calls": 2})
assert r["status"] == "done"
assert a.post(f"/agent/commands/{cmd['id']}/result", {"status": "failed", "error": "x"})["status"] == "done"
tail = a.post("/agent/claim", {})["command"]
assert tail["type"] == "refresh"
out = a.post("/agent/state", {"command_id": tail["id"], "page": 1, "last_page": True,
"issues": [{"key": "A-1", "summary": "Т", "duedate": "2026-10-23", "labels": ["AWPCA"],
"created": "2026-10-10T10:00:00.000+0300"},
{"key": "B-1", "summary": "Сосед", "labels": []}],
"links": [{"source_key": "A-1", "target_key": "B-1", "link_type": "ASUPR_Связанные"}],
"neighbor_keys": ["B-1"]})
assert out == {"issues": 2, "links": 1}
assert a.get("/agent/known-keys")["keys"] == ["A-1", "B-1"]
a.post(f"/agent/commands/{tail['id']}/result", {"status": "done", "jira_calls": 1})
assert c.get("/batches/%d" % b["id"])["status"] == "done"
issue = c.get("/issues/A-1")
assert issue["links_out"] == [{"target_key": "B-1", "link_type": "ASUPR_Связанные"}]
assert c.get("/issues/B-1")["neighbor"] is True
runs = c.get("/sync-runs", limit=5)["runs"]
assert runs[0]["issues_count"] == 2 and runs[0]["finished_at"]
assert c.get("/agent")["online"] is True
def test_interface_routes(hub):
c = hub["client"]
with pytest.raises(HubError) as e:
c.get("/issues/A-404")
assert e.value.status == 404
hub["agent"].post("/agent/state", {"issues": [{"key": "A-1", "summary": "Т", "sprint": "Спринт 2", "labels": []},
{"key": "A-2", "summary": "Д", "sprint": "Спринт 1", "labels": []}], "links": []})
assert [i["key"] for i in c.get("/issues", sprint="Спринт 2")["issues"]] == ["A-1"]
note = c.put("/issues/A-1/note", {"fact_status": "В работе", "updated_by": "owner"})
assert note["fact_status"] == "В работе" and note["updated_by"] == "owner"
with pytest.raises(HubError) as e:
c.put("/issues/A-1/note", {"summary": "x"})
assert e.value.status == 422
with pytest.raises(HubError) as e:
c.post("/batches", {"title": "x", "commands": [{"type": "ensure_fields", "params": {"key": "A-1"}}]})
assert e.value.status == 422 and "без полей" in e.value.detail
b = c.post("/batches", {"title": "x", "commands": [{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23"}},
{"type": "ensure_fields", "params": {"key": "A-2", "duedate": "2026-10-23"}}]})
assert c.get("/batches", limit=5)["batches"][0]["id"] == b["id"]
cmd = hub["agent"].post("/agent/claim", {})["command"]
with pytest.raises(HubError) as e:
c.post(f"/commands/{cmd['id']}/cancel", {})
assert e.value.status == 409 and e.value.detail
assert c.post(f"/commands/{cmd['id']}/cancel", {"force": True})["status"] == "cancelled"
assert c.post(f"/batches/{b['id']}/cancel", {})["status"] == "cancelled"
retry = c.post(f"/commands/{cmd['id']}/retry", {"author": "claude"})
assert c.get(f"/batches/{retry['id']}")["title"] == f"Повтор команды {cmd['id']}"
full = c.post("/refresh", {"full": True})
assert c.get(f"/batches/{full['id']}")["commands"][0]["params"]["jql"] == "project = AURORA AND labels = AWPCA"
keys = c.post("/refresh", {"keys": ["A-1"]})
assert c.get(f"/batches/{keys['id']}")["commands"][0]["params"] == {"keys": ["A-1"]}
def test_settings_put_rejects_protected(hub):
c = hub["client"]
v0 = c.get("/settings")["settings_version"]
for key in ("tokens", "settings_version"):
with pytest.raises(HubError) as e:
c.put("/settings", {key: "x", "pacing": {"refresh": 1}})
assert e.value.status == 422
assert c.get("/settings")["settings_version"] == v0
assert "tokens" not in c.get("/settings")
v1 = c.put("/settings", {"pacing": {"refresh": 1}})["settings_version"]
assert v1 == v0 + 1 and c.get("/settings")["pacing"]["refresh"] == 1
def test_backup_created_on_first_write(hub, tmp_path):
c = hub["client"]
c.post("/batches", {"title": "x", "commands": [{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23"}}]})
hub["agent"].post("/agent/claim", {})
assert len(list((tmp_path / "backups").glob("*-batch-*.sqlite"))) == 1
def test_client_unavailable():
from jira_hub.client import HubUnavailable
with pytest.raises(HubUnavailable):
HubClient("http://127.0.0.1:9", "t", timeout=1).get("/issues")
def test_no_backup_for_refresh_batch(hub, tmp_path):
hub["client"].post("/refresh", {"keys": ["A-1"]})
assert hub["agent"].post("/agent/claim", {})["command"]["type"] == "refresh"
assert not list((tmp_path / "backups").glob("*.sqlite"))
def test_bad_requests(hub):
c = hub["client"]
with pytest.raises(HubError) as e:
c.get("/issues", limit="abc")
assert e.value.status == 400
import urllib.request, urllib.error
req = urllib.request.Request(hub["base"] + "/batches", data=b"{not json", method="POST",
headers={"Authorization": "Bearer " + c.token})
with pytest.raises(urllib.error.HTTPError) as e2:
urllib.request.urlopen(req)
assert e2.value.code == 400
e2.value.close()