test: A32 phase 1 — 4 routers à 0 test couverts (10 smokes) (v7.11.0)
tests/test_smoke_uncovered.py — un smoke par route des 4 routers qui n'avaient
AUCUN test :
- webhooks.py 3/3 : réception sans secret → {"status":"ok"} ; HMAC faux → 401
(secret piloté par monkeypatch, déterministe quel que soit le .env) ;
register sans secret → 400 AVANT tout appel réseau ; status avec
gitea.list_webhooks stubbé → {"registered": False} (zéro réseau réel)
- notes.py 2/2 : GET HTML + roundtrip POST→GET (upsert persisté en base,
échappement HTML vérifié : <b> et non <b>)
- sidebar_config.py 2/2 : GET défauts ; PUT persisté puis RELU depuis
users.sidebar_config ; 400 sans config ; remise en état en fin de test
- github_routes.py 2/2 : status {"linked": False} ; disconnect {"status": "ok"}
Reste (A32 phase 2) : quasi nuls — library 1/10, api 3/23, dashboard 17/63,
api_v2 50/115 → même recette, fixture client existante.
suite **1047/1047** · `ruff check app tests` OK · docs à jour
This commit is contained in:
@@ -1,5 +1,24 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `tests/test_smoke_uncovered.py` — **10 smoke tests**, un par route des 4
|
||||||
|
routers qui n'avaient AUCUN test :
|
||||||
|
· `webhooks.py` (3/3) : réception sans secret → `{"status":"ok"}` ;
|
||||||
|
HMAC faux → 401 (secret piloté par monkeypatch) ; register sans secret →
|
||||||
|
400 **avant** tout appel réseau ; status avec `gitea.list_webhooks` stubbé
|
||||||
|
(zéro accès réseau réel)
|
||||||
|
· `notes.py` (2/2) : GET HTML + roundtrip POST→GET (upsert persisté,
|
||||||
|
échappement HTML vérifié `<b>`)
|
||||||
|
· `sidebar_config.py` (2/2) : GET défauts, PUT persisté relu depuis
|
||||||
|
`users.sidebar_config`, 400 sans `config`, remise en état en fin de test
|
||||||
|
· `github_routes.py` (2/2) : status `{"linked": False}`, disconnect ok
|
||||||
|
- Reste (phase 2) : quasi nuls — `library.py` 1/10, `api.py` 3/23,
|
||||||
|
`dashboard.py` 17/63, `api_v2.py` 50/115
|
||||||
|
- Suite complète : **1047/1047**
|
||||||
|
|
||||||
## v7.10.0 (2026-10-01) — Audit : A21 phase 2b (api_v2 bouclé)
|
## v7.10.0 (2026-10-01) — Audit : A21 phase 2b (api_v2 bouclé)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.10.0 (audit — A21 phase 2b : api_v2 bouclé, 111/115 hors loop) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.11.0 (audit — A32 phase 1 : 4 routers à 0 test couverts) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.10.0",
|
version="7.11.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.10.0"
|
"version": "7.11.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""A32 — smoke tests : un par route des 4 routers à 0 test.
|
||||||
|
|
||||||
|
`webhooks.py` (3), `notes.py` (2), `sidebar_config.py` (2), `github_routes.py` (2).
|
||||||
|
Aucun accès réseau réel : le secret webhook est piloté par monkeypatch et
|
||||||
|
`gitea.list_webhooks` est stubbé.
|
||||||
|
"""
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
OWNER, REPO = "smoke-a32", "repo-x"
|
||||||
|
|
||||||
|
|
||||||
|
# ── webhooks.py (0/3) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_webhook_receive_ok_without_secret(client, monkeypatch):
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "")
|
||||||
|
r = client.post("/api/webhook", json={"zen": "smoke"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_receive_rejects_bad_signature(client, monkeypatch):
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "s3cret")
|
||||||
|
r = client.post("/api/webhook", json={}) # pas d'en-tête X-Gitea-Signature
|
||||||
|
assert r.status_code == 401
|
||||||
|
assert r.json()["detail"] == "Invalid signature"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_register_fails_fast_without_secret(client, monkeypatch):
|
||||||
|
# secret vide → 400 AVANT tout appel réseau (gitea.list_webhooks)
|
||||||
|
monkeypatch.setattr(settings, "gitea_webhook_secret", "")
|
||||||
|
r = client.post(f"/api/webhook/register/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "not configured" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_status_stubbed(client, monkeypatch):
|
||||||
|
from app.services import gitea_client
|
||||||
|
|
||||||
|
async def _no_hooks(*_a, **_k):
|
||||||
|
return []
|
||||||
|
|
||||||
|
monkeypatch.setattr(gitea_client.gitea, "list_webhooks", _no_hooks)
|
||||||
|
r = client.get(f"/api/webhook/status/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"registered": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ── notes.py (0/2) ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_notes_get_renders_html(client):
|
||||||
|
r = client.get(f"/notes/{OWNER}/{REPO}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "text/html" in r.headers["content-type"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_save_roundtrip(client):
|
||||||
|
payload = "note de smoke A32"
|
||||||
|
html_payload = "<b>&co</b>"
|
||||||
|
r = client.post(
|
||||||
|
f"/notes/{OWNER}/{REPO}", data={"content": f"{payload} {html_payload}"}
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert payload in r.text # contenu rendu
|
||||||
|
assert "<b>&co</b>" in r.text # échappé (A10 — pas d'HTML cru)
|
||||||
|
again = client.get(f"/notes/{OWNER}/{REPO}")
|
||||||
|
assert payload in again.text # persistée en base (upsert)
|
||||||
|
|
||||||
|
|
||||||
|
# ── sidebar_config.py (0/2) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_sidebar_config_get(client):
|
||||||
|
r = client.get("/api/sidebar/config")
|
||||||
|
assert r.status_code == 200
|
||||||
|
cfg = r.json()["config"]
|
||||||
|
assert isinstance(cfg, dict) and "meetings" in cfg
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidebar_config_put_persists_and_validates(client):
|
||||||
|
r = client.put(
|
||||||
|
"/api/sidebar/config",
|
||||||
|
json={"config": {"meetings": {"visible": False, "order": 2, "show_count": 5}}},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
back = client.get("/api/sidebar/config").json()["config"]
|
||||||
|
assert back["meetings"]["visible"] is False # relu depuis users.sidebar_config
|
||||||
|
|
||||||
|
bad = client.put("/api/sidebar/config", json={})
|
||||||
|
assert bad.status_code == 400 # config dict requis
|
||||||
|
|
||||||
|
# remise en état pour les autres tests
|
||||||
|
restore = client.put(
|
||||||
|
"/api/sidebar/config",
|
||||||
|
json={"config": {"meetings": {"visible": True, "order": 2, "show_count": 5}}},
|
||||||
|
)
|
||||||
|
assert restore.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
# ── github_routes.py (0/2) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_github_status_unlinked(client):
|
||||||
|
r = client.get("/api/github/status")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"linked": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_github_disconnect_ok(client):
|
||||||
|
r = client.delete("/api/github/disconnect")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok"}
|
||||||
Reference in New Issue
Block a user