Files
kanbante/tests/test_palier20.py
Spark 28322c12cf
CI / test (push) Successful in 2m17s
feat(0.30.0): colonnes personnalisables par espace + mapping étiquette ↔ colonne
- Table board_columns par espace (clé immuable, nom/couleur/
  ordre libres, étiquette mappée + alias, drapeau Focus) ;
  définitions semées paresseusement depuis les valeurs
  historiques — sans personnalisation, comportement identique.
- Service app/board_columns.py : résolution, mapping
  étiquette→colonne (alias compris, ex. kanban_a_faire de
  Flowdeck), colonnes Focus/Wallboard.
- Board, API, adoption, miroirs d'étiquettes, déplacements,
  capture, santé, focus, wall, dashboard branchés sur les
  colonnes de l'espace ; validations dynamiques dans models
  (move_issue, set_column_order) et rules.
- Administration → Colonnes : ajouter (avant Terminé),
  renommer, réordonner, mapper, supprimer avec migration des
  cartes vers une colonne choisie, réinitialiser ; bouton
  🧱 Colonnes sur le board ; journal d'audit.
- Backlog et Terminé : colonnes système non supprimables.
- 254 tests verts (7 nouveaux, palier 20) ; smoke réel contre
  le vrai Gitea : création, affichage au board, suppression.
2026-10-23 08:38:02 -04:00

278 lines
9.1 KiB
Python

"""Palier 20 (0.30.0): colonnes personnalisables par
espace et mapping étiquette ↔ colonne configurable."""
import pytest
from fastapi.testclient import TestClient
import app.main as main
from app import board_columns as bcols
from app import models
from tests.conftest import csrf_from
@pytest.fixture
def auth_client(tmp_path, monkeypatch, fake_gitea):
monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "cols.db"))
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
with TestClient(main.app) as client:
client.get("/login")
token = client.cookies.get("kanbante_csrf") or ""
client.post(
"/login",
data={
"username": "bruno",
"password": "motdepasse",
"next": "/",
"csrf_token": token,
},
follow_redirects=False,
)
yield client
@pytest.fixture
def ws_cols():
"""Un espace dédié rattaché à la connexion héritée."""
ws_id = models.create_workspace("cols-test", "ColonnesTest", "#123456")
models.set_workspace_connections(ws_id, [1])
bcols.invalidate(ws_id)
yield ws_id
models.clear_board_columns(ws_id)
bcols.invalidate(ws_id)
models.delete_workspace(ws_id)
def _issue(number, title, labels=None):
return {
"number": number,
"title": title,
"body": "",
"state": "open",
"labels": labels or [],
"assignee": None,
"assignees": [],
"milestone": None,
"due_date": None,
"created_at": "2026-09-01T09:00:00Z",
"updated_at": "2026-09-02T09:00:00Z",
"html_url": f"http://gitea.test/Projets/demo/issues/{number}",
"pull_request": None,
}
# ── Défauts et service ────────────────────────────────────
def test_defaults_match_historical_columns(ws_cols):
columns = bcols.get_columns(ws_cols)
assert [c["key"] for c in columns] == [
"backlog", "todo", "in_progress", "qa", "review", "done",
]
assert bcols.names(ws_cols)["todo"] == "📋 À faire"
assert bcols.label_for(ws_cols, "review") == "kanban:review"
assert bcols.label_for(ws_cols, "backlog") is None
assert bcols.focus_keys(ws_cols) == ["in_progress", "qa", "review"]
assert bcols.wall_keys(ws_cols) == [
"todo", "in_progress", "qa", "review",
]
def test_custom_column_at_model_level(ws_cols):
columns = bcols.get_columns(ws_cols)
models.upsert_board_column(
ws_cols,
{
"key": "dev", "name": "🛠️ Dev", "color": "#123123",
"position": 4, "label": "etape:dev", "aliases": "",
"kind": "flow", "in_focus": 1,
},
)
bcols.invalidate(ws_cols)
assert "dev" in models.column_keys(ws_cols)
models.move_issue(
"Projets/demo", 5, "dev", workspace_id=ws_cols, connection_id=1
)
state = models.get_board_state(
"Projets/demo", workspace_id=ws_cols, connection_id=1
)
assert [c["issue_number"] for c in state["dev"]] == [5]
assert bcols.label_for(ws_cols, "dev") == "etape:dev"
assert "dev" in bcols.focus_keys(ws_cols)
# Un autre espace n'est pas affecté.
other = models.create_workspace("cols-temoin", "Témoin", "#000000")
try:
assert "dev" not in models.column_keys(other)
finally:
models.delete_workspace(other)
# ── Panneau d'administration ──────────────────────────────
def test_admin_columns_crud(auth_client, ws_cols):
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
page = auth_client.get("/admin/columns")
assert page.status_code == 200
assert "Colonnes du board" in page.text
token = csrf_from(auth_client)
r = auth_client.post(
"/admin/columns",
data={
"name": "🛠️ En développement",
"color": "#224466",
"label": "etape:dev",
"aliases": "en-dev",
"csrf_token": token,
},
follow_redirects=False,
)
assert r.status_code == 303
keys = bcols.keys(ws_cols)
assert "en_developpement" in keys
# Insérée avant « done ».
assert keys.index("en_developpement") == keys.index("done") - 1
# Renommage + mapping via le formulaire de la colonne.
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns/en_developpement",
data={
"name": "🚀 Dev",
"color": "#224466",
"label": "etape:dev",
"aliases": "en-dev, dev",
"in_focus": "1",
"csrf_token": token,
},
follow_redirects=False,
)
assert bcols.names(ws_cols)["en_developpement"] == "🚀 Dev"
lookup = bcols.label_lookup(ws_cols)
assert lookup["etape:dev"] == "en_developpement"
assert lookup["en-dev"] == "en_developpement"
# Le board affiche la nouvelle colonne.
board = auth_client.get("/board/Projets/demo")
assert 'data-column="en_developpement"' in board.text
assert "🚀 Dev" in board.text
def test_admin_column_delete_migrates_cards(auth_client, ws_cols):
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns",
data={
"name": "Attente client", "color": "#445566", "label": "",
"aliases": "", "csrf_token": token,
},
follow_redirects=False,
)
assert "attente_client" in bcols.keys(ws_cols)
models.move_issue(
"Projets/demo", 5, "attente_client",
workspace_id=ws_cols, connection_id=1,
)
token = csrf_from(auth_client)
r = auth_client.post(
"/admin/columns/attente_client/delete",
data={"target": "backlog", "csrf_token": token},
follow_redirects=False,
)
assert r.status_code == 303
assert "attente_client" not in bcols.keys(ws_cols)
state = models.get_board_state(
"Projets/demo", workspace_id=ws_cols, connection_id=1
)
assert any(c["issue_number"] == 5 for c in state["backlog"])
# Les colonnes système ne se suppriment pas.
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns/backlog/delete",
data={"target": "todo", "csrf_token": token},
follow_redirects=False,
)
assert "backlog" in bcols.keys(ws_cols)
def test_admin_columns_reset(auth_client, ws_cols):
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns",
data={
"name": "Temporaire", "color": "#445566", "label": "",
"aliases": "", "csrf_token": token,
},
follow_redirects=False,
)
assert "temporaire" in bcols.keys(ws_cols)
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns/reset",
data={"csrf_token": token},
follow_redirects=False,
)
assert bcols.keys(ws_cols) == [
"backlog", "todo", "in_progress", "qa", "review", "done",
]
# ── Mapping : adoption par alias et miroir personnalisé ──
def test_label_alias_adoption(auth_client, ws_cols, fake_gitea):
"""L'étiquette Flowdeck kanban_a_faire, déclarée alias de la
colonne todo, fait adopter À faire par le board."""
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns/todo",
data={
"name": "📋 À faire", "color": "#3b82f6",
"label": "kanban:todo", "aliases": "kanban_a_faire",
"csrf_token": token,
},
follow_redirects=False,
)
fake_gitea.issues["Projets/demo"].append(
_issue(42, "Carte Flowdeck", labels=[
{"id": 950, "name": "kanban_a_faire", "color": "aaaaaa"},
])
)
payload = auth_client.get("/api/board/Projets/demo").json()
assert "42" in payload["columns"]["todo"]["html"]
state = models.get_board_state(
"Projets/demo", workspace_id=ws_cols, connection_id=1
)
assert any(c["issue_number"] == 42 for c in state["todo"])
def test_custom_label_mirror_on_move(auth_client, ws_cols, fake_gitea):
"""Déplacer une carte pose l'étiquette mappée de SA colonne."""
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
token = csrf_from(auth_client)
auth_client.post(
"/admin/columns",
data={
"name": "En développement", "color": "#224466",
"label": "etape:dev", "aliases": "", "csrf_token": token,
},
follow_redirects=False,
)
token = csrf_from(auth_client)
r = auth_client.post(
"/api/move",
data={
"repo_full_name": "Projets/demo",
"issue_number": "5",
"to_column": "en_developpement",
"csrf_token": token,
},
)
assert r.status_code == 200
issue = next(
i for i in fake_gitea.issues["Projets/demo"] if i["number"] == 5
)
names = [l["name"] for l in issue.get("labels") or []]
assert "etape:dev" in names