"""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