164 lines
8.4 KiB
Python
164 lines
8.4 KiB
Python
from datetime import datetime, timezone
|
||
|
||
import pytest
|
||
|
||
from sdlc_hub.core import settings
|
||
from sdlc_hub.core.client import HubError
|
||
from sdlc_hub.modules.jira import agile, mirror
|
||
|
||
COLS = [{"name": "К выполнению", "statuses": ["Backlog", "Ожидает анализа"]}, {"name": "В работе", "statuses": ["В работе"]},
|
||
{"name": "Готово", "statuses": ["Готово", "Протестировано", "Снято"]}]
|
||
|
||
|
||
@pytest.fixture
|
||
def conn(open_db, add_project):
|
||
c = open_db()
|
||
settings.ensure_defaults(c)
|
||
add_project(c)
|
||
return c
|
||
|
||
|
||
def issue(key, status, **over):
|
||
base = {"key": key, "id": "1", "issue_type": "Задача", "summary": f"Тема {key}", "description": "", "status": status,
|
||
"priority": "Средний", "assignee": None, "reporter": "me", "duedate": None, "planned_start": None,
|
||
"planned_end": None, "epic_key": None, "labels": ["AWPCA"], "sprint": None,
|
||
"created": "2026-10-10T12:00:00.000+0300", "jira_updated": "2026-10-11T12:00:00.000+0300"}
|
||
return base | over
|
||
|
||
|
||
def kissue(key, status, **over):
|
||
return issue(key, status, labels=["K"], **over)
|
||
|
||
|
||
def board(id=4216, type="scrum", sprints=(), **over):
|
||
return {"id": id, "type": type, "columns": COLS, "kanban_backlog": None, "sprints": list(sprints), **over}
|
||
|
||
|
||
def sprint(id, state, name=None, **over):
|
||
return {"id": id, "name": name or f"S{id}", "state": state, "start_date": None, "end_date": None,
|
||
"complete_date": None, "goal": None, **over}
|
||
|
||
|
||
def load(conn, issues, boards):
|
||
mirror.apply_state(conn, issues, [], boards=boards)
|
||
|
||
|
||
def keys(rows):
|
||
return [r["key"] for r in rows]
|
||
|
||
|
||
def test_sprint_and_backlog_sums(conn): # сценарий «Спринт и бэклог»
|
||
load(conn, [issue("A", "Готово", story_points=3, sprint_ids=[102], rank="0|a"), issue("B", "В работе", sprint_ids=[102], rank="0|b"),
|
||
issue("C", "Backlog", sprint_ids=[]), issue("D", "Готово", sprint_ids=[])],
|
||
[board(sprints=[sprint(102, "active")])])
|
||
v = agile.view(conn, "AWPCA")
|
||
s = v["sprints"][0]
|
||
assert (s["count"], s["count_done"], s["points"], s["points_done"], s["unestimated"]) == (2, 1, 3, 3, 1)
|
||
assert keys(s["issues"]) == ["A", "B"] and keys(v["backlog"]) == ["C"] and v["board_issues"] is None
|
||
|
||
|
||
def test_done_is_last_column(conn): # сценарий «Колонки доски»
|
||
load(conn, [issue("P", "Протестировано", sprint_ids=[102]), issue("Q", "Backlog", sprint_ids=[102])], [board(sprints=[sprint(102, "active")])])
|
||
assert {i["key"]: i["done"] for i in agile.view(conn, "AWPCA")["sprints"][0]["issues"]} == {"P": True, "Q": False}
|
||
|
||
|
||
def test_project_without_board(conn, add_project): # сценарий «Проект без доски»
|
||
add_project(conn, "STOCKS_DRRK", board_id=None, board_type=None)
|
||
assert agile.view(conn, "STOCKS_DRRK") == {"board": None, "sprints": [], "backlog": None, "board_issues": None}
|
||
|
||
|
||
def test_board_without_agent_data(conn): # design #23, состояние 1
|
||
load(conn, [issue("A", "Готово")], [])
|
||
v = agile.view(conn, "AWPCA")
|
||
assert v["board"] == {"id": 4216, "type": "scrum", "columns": None, "kanban_backlog": None, "fetched_at": None}
|
||
assert [i["done"] for i in v["backlog"]] == [False] # без колонок выполненных нет
|
||
|
||
|
||
def test_sprint_order_mixed_offsets(conn): # порядок спринтов; Review Focus 1
|
||
load(conn, [], [board(sprints=[
|
||
sprint(3, "active", start_date="2026-10-14T09:00:00.000+0300"), sprint(5, "active", start_date="2026-10-01T09:00:00.000+03:00"),
|
||
sprint(9, "future"), sprint(7, "future"),
|
||
sprint(1, "closed", complete_date="2026-10-01T02:00:00.000+0300"), sprint(2, "closed", complete_date="2026-09-30T23:30:00.000Z")])])
|
||
assert [s["id"] for s in agile.view(conn, "AWPCA")["sprints"]] == [5, 3, 7, 9, 2, 1] # 2 закрыт позже 1 по UTC
|
||
|
||
|
||
def test_rank_order_nulls_last_then_key(conn):
|
||
load(conn, [issue("Z-1", "Backlog", rank="0|i0002:"), issue("Z-2", "Backlog", rank="0|i0001:"),
|
||
issue("B-1", "Backlog", rank=None), issue("A-1", "Backlog")], [board()])
|
||
assert keys(agile.view(conn, "AWPCA")["backlog"]) == ["Z-2", "Z-1", "A-1", "B-1"]
|
||
|
||
|
||
def test_only_own_issues_and_empty_sprint(conn): # design #8
|
||
load(conn, [issue("N", "Backlog", labels=["OTHER"], sprint_ids=[102])], [board(sprints=[sprint(102, "active")])])
|
||
s = agile.view(conn, "AWPCA")["sprints"][0]
|
||
assert s["issues"] == [] and s["count"] == 0
|
||
|
||
|
||
def test_shared_board_own_issues_only(conn, add_project): # Review Focus 3
|
||
add_project(conn, "C", board_id=4216)
|
||
load(conn, [issue("A", "Backlog", sprint_ids=[102]), issue("K", "Backlog", labels=["C"], sprint_ids=[102])], [board(sprints=[sprint(102, "active")])])
|
||
assert keys(agile.view(conn, "C")["sprints"][0]["issues"]) == ["K"] and keys(agile.view(conn, "AWPCA")["sprints"][0]["issues"]) == ["A"]
|
||
|
||
|
||
def test_closed_sprint_done_by_current_status(conn): # design #7
|
||
load(conn, [issue("X", "Готово", sprint_ids=[101])], [board(sprints=[sprint(101, "closed")])])
|
||
assert agile.view(conn, "AWPCA")["sprints"][0]["count_done"] == 1
|
||
|
||
|
||
def test_backlog_keeps_issue_of_closed_sprint(conn):
|
||
load(conn, [issue("X", "Backlog", sprint_ids=[101]), issue("Y", "Backlog", sprint_ids=[103])],
|
||
[board(sprints=[sprint(101, "closed"), sprint(103, "future")])])
|
||
assert keys(agile.view(conn, "AWPCA")["backlog"]) == ["X"]
|
||
|
||
|
||
KCOLS = [{"name": "Бэклог", "statuses": ["Backlog"]}, {"name": "В работе", "statuses": ["В работе"]},
|
||
{"name": "Готово", "statuses": ["Готово"]}]
|
||
NOW = datetime(2026, 10, 24, 12, tzinfo=timezone.utc)
|
||
|
||
|
||
def test_kanban_with_backlog(conn, add_project): # design #3, «Контракт данных»
|
||
add_project(conn, "K", board_id=5120, board_type="kanban")
|
||
now = datetime(2026, 10, 24, 12, tzinfo=timezone.utc)
|
||
load(conn, [kissue("K1", "Backlog"), kissue("K2", "В работе"), kissue("K3", "Готово", jira_updated="2026-10-21T10:00:00.000+0300"),
|
||
kissue("K4", "Готово", jira_updated="2026-10-01T10:00:00.000+0300"), kissue("K5", "Странный"),
|
||
kissue("K6", "Готово", jira_updated=None)],
|
||
[board(id=5120, type="kanban", kanban_backlog=True, columns=[{"name": "Бэклог", "statuses": ["Backlog"]},
|
||
{"name": "В работе", "statuses": ["В работе"]}, {"name": "Готово", "statuses": ["Готово"]}])])
|
||
v = agile.view(conn, "K", now=now)
|
||
assert v["sprints"] == [] and keys(v["backlog"]) == ["K1"] and sorted(keys(v["board_issues"])) == ["K2", "K3", "K5"]
|
||
assert v["board"]["kanban_backlog"] is True
|
||
|
||
|
||
def test_kanban_without_backlog(conn, add_project): # design #17
|
||
add_project(conn, "K", board_id=5120, board_type="kanban")
|
||
load(conn, [kissue("K1", "Backlog"), kissue("K2", "В работе")],
|
||
[board(id=5120, type="kanban", kanban_backlog=False, columns=KCOLS)])
|
||
v = agile.view(conn, "K", now=NOW)
|
||
assert v["backlog"] is None and sorted(keys(v["board_issues"])) == ["K1", "K2"]
|
||
assert v["board"]["kanban_backlog"] is False
|
||
|
||
|
||
def test_kanban_done_cutoff_with_offsets(conn, add_project): # Review Focus 1: граница 14 дней по UTC
|
||
add_project(conn, "K", board_id=5120, board_type="kanban")
|
||
load(conn, [kissue("OLD", "Готово", jira_updated="2026-10-10T14:30:00.000+0300"), # 11:30Z — старше 14 дней
|
||
kissue("NEW", "Готово", jira_updated="2026-10-10T15:30:00.000+0300")], # 12:30Z — в пределах
|
||
[board(id=5120, type="kanban", kanban_backlog=False, columns=KCOLS)])
|
||
assert keys(agile.view(conn, "K", now=NOW)["board_issues"]) == ["NEW"]
|
||
|
||
|
||
def test_agile_route(hub_server, add_project):
|
||
h = hub_server["hub"]
|
||
c = h.conn()
|
||
try:
|
||
add_project(c)
|
||
finally:
|
||
c.close()
|
||
client = hub_server["client"]
|
||
with pytest.raises(HubError) as e:
|
||
client.get("/agile")
|
||
assert e.value.status == 422 and e.value.error == "bad_request"
|
||
with pytest.raises(HubError) as e:
|
||
client.get("/agile", project="NOPE")
|
||
assert e.value.status == 404
|
||
assert set(client.get("/agile", project="AWPCA")) == {"board", "sprints", "backlog", "board_issues"}
|