223 lines
9.2 KiB
Python
223 lines
9.2 KiB
Python
"""Поля проекта от модулей; доски — в модуле jira (wiki-module, решение 8, спеки modules/data-model/hub-api)."""
|
||
import pytest
|
||
|
||
from sdlc_hub.core import db, mcp as core_mcp, registry
|
||
from sdlc_hub.core.client import HubError
|
||
from sdlc_hub.core.modules import Module, ProjectField
|
||
from sdlc_hub.modules.jira import mirror
|
||
from v6db import make_v6_db
|
||
|
||
|
||
def _ok(v):
|
||
return v
|
||
|
||
|
||
def test_v6_boards_moved(tmp_path):
|
||
make_v6_db(tmp_path / "hub.sqlite")
|
||
conn = db.connect(tmp_path / "hub.sqlite")
|
||
try:
|
||
cols = {r["name"] for r in conn.execute("PRAGMA table_info(project)")}
|
||
assert cols == {"label", "name", "created_at"}
|
||
rows = conn.execute("SELECT label, board_id, board_type FROM jira_project ORDER BY label").fetchall()
|
||
assert [tuple(r) for r in rows] == [("AWPCA", 4216, "scrum"), ("sfm_kedr", None, None)]
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def test_projects_get_shape(hub_server, add_project):
|
||
conn = hub_server["hub"].conn()
|
||
try:
|
||
add_project(conn)
|
||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
|
||
finally:
|
||
conn.close()
|
||
got = hub_server["client"].get("/projects")["projects"]
|
||
assert got == [{"label": "AWPCA", "name": "АРМ КА", "created_at": got[0]["created_at"],
|
||
"board_id": 4216, "board_type": "scrum", "issues_count": 1, "wiki_root": None,
|
||
"wiki_root_title": None, "wiki_pages": None, "wiki_synced_at": None, "wiki_sync_error": None}]
|
||
|
||
|
||
def test_post_unknown_field(hub_server):
|
||
c = hub_server["client"]
|
||
with pytest.raises(HubError) as e:
|
||
c.post("/projects", {"label": "X", "name": "X", "board": 5})
|
||
assert e.value.status == 422 and e.value.error == "bad_project" and "board" in e.value.detail
|
||
assert c.get("/projects")["projects"] == []
|
||
|
||
|
||
def test_post_response_shape(hub_server):
|
||
out = hub_server["client"].post("/projects", {"label": "X", "name": "X", "board_id": 7})
|
||
assert set(out) == {"project", "batch_id"} and out["project"]["board_id"] == 7
|
||
assert hub_server["client"].get(f"/batches/{out['batch_id']}")["title"] == "Новый проект X"
|
||
|
||
|
||
def test_patch_author_ok(hub_server, add_project):
|
||
conn = hub_server["hub"].conn()
|
||
try:
|
||
add_project(conn)
|
||
finally:
|
||
conn.close()
|
||
out = hub_server["client"].patch("/projects/AWPCA", {"author": "viewer"})
|
||
assert out["batch_ids"] == []
|
||
|
||
|
||
def test_patch_unknown_field(hub_server, add_project):
|
||
conn = hub_server["hub"].conn()
|
||
try:
|
||
add_project(conn)
|
||
finally:
|
||
conn.close()
|
||
with pytest.raises(HubError) as e:
|
||
hub_server["client"].patch("/projects/AWPCA", {"nope": 1})
|
||
assert e.value.status == 422 and e.value.detail == "nope: неизвестное поле"
|
||
|
||
|
||
def test_editable_field_needs_updater():
|
||
m = Module(name="a", project_fields=(ProjectField("root", _ok, editable=True),))
|
||
with pytest.raises(RuntimeError) as e:
|
||
registry.check([m], [], {})
|
||
assert "root" in str(e.value) and "on_project_updated" in str(e.value)
|
||
|
||
|
||
def test_migrate_v6_idempotent(tmp_path):
|
||
from sdlc_hub.modules.jira import projects as jira_projects
|
||
make_v6_db(tmp_path / "hub.sqlite")
|
||
conn = db.connect(tmp_path / "hub.sqlite")
|
||
try:
|
||
before = [tuple(r) for r in conn.execute("SELECT * FROM jira_project ORDER BY label")]
|
||
jira_projects.migrate_v6(conn)
|
||
assert [tuple(r) for r in conn.execute("SELECT * FROM jira_project ORDER BY label")] == before
|
||
assert conn.execute("PRAGMA foreign_key_check").fetchall() == []
|
||
assert [r["name"] for r in conn.execute("SELECT name FROM project ORDER BY label")] == ["АРМ КА", "sfm_kedr"]
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def test_create_project_text_without_batch(monkeypatch):
|
||
class C:
|
||
def post(self, path, body):
|
||
return {"project": {"label": "P", "name": "p"}, "batch_id": None}
|
||
monkeypatch.setattr(core_mcp, "client", lambda: C())
|
||
assert "#None" not in core_mcp.create_project("P", "p", {}, "wiki") and "пакет" not in core_mcp.create_project("P", "p", {}, "wiki").splitlines()[0]
|
||
|
||
|
||
def test_patch_board_forbidden(hub_server, add_project):
|
||
conn = hub_server["hub"].conn()
|
||
try:
|
||
add_project(conn)
|
||
finally:
|
||
conn.close()
|
||
c = hub_server["client"]
|
||
with pytest.raises(HubError) as e:
|
||
c.patch("/projects/AWPCA", {"board_id": 1})
|
||
assert e.value.status == 422 and e.value.error == "bad_project"
|
||
assert "board_id после создания не меняется" in e.value.detail
|
||
assert c.get("/projects")["projects"][0]["board_id"] == 4216
|
||
|
||
|
||
def test_patch_unknown_project(hub_server):
|
||
c = hub_server["client"]
|
||
with pytest.raises(HubError) as e:
|
||
c.patch("/projects/NOPE", {})
|
||
assert e.value.status == 404
|
||
|
||
|
||
def test_patch_empty_ok(hub_server, add_project):
|
||
conn = hub_server["hub"].conn()
|
||
try:
|
||
add_project(conn)
|
||
finally:
|
||
conn.close()
|
||
c = hub_server["client"]
|
||
out = c.patch("/projects/AWPCA", {})
|
||
assert out["batch_ids"] == [] and out["project"]["label"] == "AWPCA"
|
||
|
||
|
||
def test_field_conflict():
|
||
a = Module(name="a", project_fields=(ProjectField("root", _ok),))
|
||
b = Module(name="b", project_fields=(ProjectField("root", _ok),))
|
||
with pytest.raises(RuntimeError) as e:
|
||
registry.check([a, b], [], {})
|
||
text = str(e.value)
|
||
assert "root" in text and "a" in text and "b" in text
|
||
|
||
|
||
@pytest.mark.parametrize("name", ["label", "name", "created_at", "author"])
|
||
def test_field_clashes_with_core(name):
|
||
with pytest.raises(RuntimeError) as e:
|
||
registry.check([Module(name="a", project_fields=(ProjectField(name, _ok),))], [], {})
|
||
assert name in str(e.value)
|
||
|
||
|
||
def test_mcp_json_per_module():
|
||
first = core_mcp.mcp_json("AWPCA", "jira")["mcpServers"]
|
||
assert list(first) == ["sdlc-jira"] and first["sdlc-jira"]["args"][-4:] == ["mcp", "jira", "--project", "AWPCA"]
|
||
assert first["sdlc-jira"]["args"][3] == "sdlc-hub"
|
||
other = core_mcp.mcp_json("AWPCA", "wiki")["mcpServers"]
|
||
assert list(other) == ["sdlc-wiki"]
|
||
assert other["sdlc-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
|
||
|
||
|
||
def test_fmt_projects_columns():
|
||
items = [{"label": "A", "name": "a", "x": 1}, {"label": "B", "name": "b", "x": None}]
|
||
text = core_mcp.fmt_projects(items, "B", [("икс", lambda p: core_mcp.value(p.get("x")))])
|
||
assert text == "метка | название | икс\nA | a | 1\nB | b | — (текущий)"
|
||
|
||
|
||
@pytest.fixture
|
||
def two_modules(monkeypatch, open_db):
|
||
"""Реестр [jira, stub]: stub объявляет изменяемое поле root, хранит его в setting и ставит пакеты-пустышки."""
|
||
import json
|
||
import re
|
||
from sdlc_hub.core import api, queue
|
||
from sdlc_hub.core.http import ApiError
|
||
from sdlc_hub.modules.jira import MODULE as JIRA
|
||
|
||
def check_root(v):
|
||
if not isinstance(v, str) or not v:
|
||
raise ValueError("ожидается непустая строка")
|
||
return v.upper()
|
||
|
||
def created(conn, project, fields, author):
|
||
if "root" in fields:
|
||
conn.execute("INSERT OR REPLACE INTO setting(key, value) VALUES (?, ?)",
|
||
(f"root:{project['label']}", json.dumps(fields["root"])))
|
||
|
||
def updated(conn, project, fields, author):
|
||
created(conn, project, fields, author)
|
||
return [queue.create_batch(conn, "stub", author, [{"type": "refresh", "params": {"keys": ["A-1"]}}])]
|
||
|
||
def view(conn, labels):
|
||
out = {}
|
||
for label in labels:
|
||
row = conn.execute("SELECT value FROM setting WHERE key = ?", (f"root:{label}",)).fetchone()
|
||
out[label] = {"root": json.loads(row["value"]) if row else None}
|
||
return out
|
||
|
||
stub = Module(name="stub", project_fields=(ProjectField("root", check_root, editable=True),),
|
||
project_view=view, on_project_created=created, on_project_updated=updated)
|
||
monkeypatch.setattr(registry, "_cache", [JIRA, stub])
|
||
conn = open_db()
|
||
return api, ApiError, conn, re
|
||
|
||
|
||
def test_module_fields_post_and_patch(two_modules):
|
||
api, ApiError, conn, re = two_modules
|
||
code, out = api.h_projects_post(None, conn, None, {}, {"label": "P", "name": "p", "board_id": 3, "root": "doc-1"})
|
||
assert code == 201 and out["project"]["root"] == "DOC-1" and out["project"]["board_id"] == 3
|
||
assert set(out) == {"project", "batch_id"} and out["batch_id"] is not None # пакет только у jira
|
||
code, out = api.h_project_patch(None, conn, re.match(r"(.*)", "P"), {}, {"root": "doc-2"})
|
||
assert code == 200 and out["project"]["root"] == "DOC-2" and len(out["batch_ids"]) == 1
|
||
with pytest.raises(ApiError) as e:
|
||
api.h_project_patch(None, conn, re.match(r"(.*)", "P"), {}, {"root": ""})
|
||
assert e.value.status == 422 and e.value.detail.startswith("root")
|
||
assert api.project_rows(conn)[0]["root"] == "DOC-2"
|
||
|
||
|
||
def test_module_field_error_writes_nothing(two_modules):
|
||
api, ApiError, conn, re = two_modules
|
||
with pytest.raises(ApiError) as e:
|
||
api.h_projects_post(None, conn, None, {}, {"label": "P", "name": "p", "root": 5})
|
||
assert e.value.status == 422 and e.value.detail.startswith("root")
|
||
assert api.project_rows(conn) == []
|