147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""Palier 4 (0.6.0): QA column, label reconciliation, wallboard."""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.main as main
|
|
from app import models
|
|
from tests.conftest import _issue, csrf_from
|
|
|
|
|
|
def _column_block(html: str, column: str) -> str:
|
|
blocks = html.split('<div class="kanban-column"')
|
|
for block in blocks[1:]:
|
|
if f'data-column="{column}"' in block.split(">", 1)[0]:
|
|
return block
|
|
raise AssertionError(f"column {column} not found")
|
|
|
|
|
|
def _labelled(number: int, title: str, label_names: list[str]) -> dict:
|
|
issue = _issue(number, title)
|
|
issue["labels"] = [
|
|
{"id": 900 + i, "name": name, "color": "8b5cf6"}
|
|
for i, name in enumerate(label_names)
|
|
]
|
|
return issue
|
|
|
|
|
|
# ── QA column ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_board_has_six_columns_including_qa(client):
|
|
r = client.get("/board/Projets/demo")
|
|
assert r.status_code == 200
|
|
assert "🧪 QA" in r.text
|
|
positions = [
|
|
r.text.index(f'data-column="{col}"')
|
|
for col in ("backlog", "todo", "in_progress", "qa", "review", "done")
|
|
]
|
|
assert positions == sorted(positions) # QA between in_progress and review
|
|
|
|
|
|
def test_move_to_qa_syncs_kanban_qa_label(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
"/api/move",
|
|
data={
|
|
"repo_full_name": "Projets/demo", "issue_number": 1,
|
|
"to_column": "qa", "from_column": "backlog",
|
|
"to_order": "1", "from_order": "5", "csrf_token": token,
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["label_sync"] == "ok"
|
|
assert ("add", 1, "kanban:qa") in fake_gitea.label_ops
|
|
state = models.get_board_state("Projets/demo")
|
|
assert 1 in [c["issue_number"] for c in state["qa"]]
|
|
|
|
|
|
def test_qa_cards_appear_in_focus(client, fake_gitea):
|
|
models.move_issue("Projets/demo", 5, "qa")
|
|
r = client.get("/focus")
|
|
assert r.status_code == 200
|
|
assert "Tâche focus" in r.text
|
|
assert "🧪 QA" in r.text
|
|
|
|
|
|
# ── Label reconciliation (adoption) ───────────────────────────────
|
|
|
|
|
|
def test_board_adopts_diverging_kanban_label(client, fake_gitea):
|
|
fake_gitea.issues["Projets/demo"].append(
|
|
_labelled(9, "Déplacée via Gitea", ["kanban:review"])
|
|
)
|
|
r = client.get("/board/Projets/demo")
|
|
assert r.status_code == 200
|
|
assert "Déplacée via Gitea" in _column_block(r.text, "review")
|
|
state = models.get_board_state("Projets/demo")
|
|
assert 9 in [c["issue_number"] for c in state["review"]]
|
|
events = [
|
|
e for e in models.get_events("Projets/demo") if e["issue_number"] == 9
|
|
]
|
|
assert any(
|
|
e["event_type"] == "moved" and e["to_column"] == "review"
|
|
and e["source"] == "gitea"
|
|
for e in events
|
|
)
|
|
|
|
|
|
def test_board_ignores_conflicting_kanban_labels(client, fake_gitea):
|
|
fake_gitea.issues["Projets/demo"].append(
|
|
_labelled(9, "Étiquettes en conflit", ["kanban:todo", "kanban:review"])
|
|
)
|
|
r = client.get("/board/Projets/demo")
|
|
assert r.status_code == 200
|
|
assert "Étiquettes en conflit" in _column_block(r.text, "backlog")
|
|
state = models.get_board_state("Projets/demo")
|
|
elsewhere = [
|
|
c["issue_number"]
|
|
for col, cards in state.items() if col != "backlog"
|
|
for c in cards
|
|
]
|
|
assert 9 not in elsewhere
|
|
|
|
|
|
def test_adoption_is_idempotent(client, fake_gitea):
|
|
fake_gitea.issues["Projets/demo"].append(
|
|
_labelled(9, "Stable", ["kanban:qa"])
|
|
)
|
|
client.get("/board/Projets/demo")
|
|
client.get("/board/Projets/demo")
|
|
events = [
|
|
e for e in models.get_events("Projets/demo")
|
|
if e["issue_number"] == 9 and e["event_type"] == "moved"
|
|
]
|
|
assert len(events) == 1 # adopted once, then label and state agree
|
|
|
|
|
|
# ── Wallboard ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_wall_renders_active_projects(client):
|
|
models.move_issue("Projets/demo", 5, "in_progress")
|
|
r = client.get("/wall")
|
|
assert r.status_code == 200
|
|
assert "Projets/demo" in r.text
|
|
assert "Tâche focus" in r.text # the in-progress card is on the wall
|
|
assert 'class="wall-slide active"' in r.text
|
|
assert "🧪 QA" in r.text # the six column counters include QA
|
|
|
|
|
|
def test_wall_single_repo_mode(client):
|
|
r = client.get("/wall", params={"repo": "Projets/demo"})
|
|
assert r.status_code == 200
|
|
assert "Projets/demo" in r.text
|
|
|
|
|
|
def test_wall_unknown_repo_shows_empty_state(client):
|
|
r = client.get("/wall", params={"repo": "Inconnu/xyz"})
|
|
assert r.status_code == 200
|
|
assert "Aucun projet actif" in r.text
|
|
|
|
|
|
def test_wall_is_behind_auth(monkeypatch, fake_gitea):
|
|
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
|
|
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
|
|
with TestClient(main.app) as secured:
|
|
assert secured.get("/wall").status_code == 401
|