Files
ObsiGate/tests/test_guide.py
T
bruno fb2d83e9e3
CI / lint (push) Successful in 1m40s
CI / security (push) Successful in 1m6s
CI / test (push) Failing after 1m52s
CI / build (push) Skipped
CI / e2e (push) Skipped
feat: guide d'utilisation - couverture complete, telechargement MD/PDF, section Architecture Mermaid, guide desktop elargi (#105, BUG-067)
2026-09-18 13:06:30 -04:00

159 lines
5.8 KiB
Python

"""Tests du guide téléchargeable et de sa couverture (#105, BUG-067).
- Le Markdown/PDF du guide se génèrent depuis index.html + les locales.
- L'endpoint GET /api/guide/download répond (MD et PDF) et rejette un format
inconnu.
- Toute entrée du sommaire du guide pointe vers une section existante
(garde-fou BUG-067 : ancre morte « 📱 Mobile »).
- Parité FR/EN des clés guide105.* et présence de toutes les clés utilisées.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
INDEX_HTML = ROOT / "frontend" / "index.html"
LOCALES = ROOT / "frontend" / "locales"
@pytest.fixture(scope="module")
def index_html() -> str:
return INDEX_HTML.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def guide_toc_nav(index_html: str) -> list[str]:
"""href #help-* de la liste du sommaire du GUIDE (pas celui de config)."""
a = index_html.index('id="help-nav-list"')
b = index_html.index("</ul>", a)
return re.findall(r'href="#(help-[a-z-]+)"', index_html[a:b])
@pytest.fixture(scope="module")
def guide_section_ids(index_html: str) -> set[str]:
# La balise <section> de help-personnalisation casse la ligne entre
# class= et id= : matcher les deux attributs quel que soit l'ordre.
out: set[str] = set()
for m in re.finditer(r"<section\b[^>]*?>", index_html, re.S):
tag = m.group(0)
if 'class="help-section' not in tag.replace("\n", " ").replace(" ", " "):
continue
idm = re.search(r'id="(help-[a-z-]+)"', tag)
if idm:
out.add(idm.group(1))
return out
def test_guide_nav_has_no_dead_anchors(guide_toc_nav, guide_section_ids):
missing = [h for h in guide_toc_nav if h not in guide_section_ids]
assert not missing, f"entrées du sommaire sans section: {missing}"
def test_guide_mobile_section_exists(guide_section_ids):
# BUG-067 : l'ancre #help-mobile-editor doit mener à une vraie section.
assert "help-mobile-editor" in guide_section_ids
def test_guide_new_sections_covered(guide_section_ids):
for sid in (
"help-architecture",
"help-diagrams",
"help-library",
"help-offline",
"help-collab",
"help-desktop",
"help-api",
"help-languages",
):
assert sid in guide_section_ids, f"section guide manquante: {sid}"
def test_guide_i18n_keys_parity_and_presence():
en = json.loads((LOCALES / "en.json").read_bytes().decode("utf-8"))
fr = json.loads((LOCALES / "fr.json").read_bytes().decode("utf-8"))
keys_en = {k for k in en if k.startswith("guide105.")}
keys_fr = {k for k in fr if k.startswith("guide105.")}
assert keys_en == keys_fr and len(keys_en) > 80
used = set(re.findall(r'data-i18n(?:-attr="title:)?="?(guide105\.[a-z0-9_]+)"?',
INDEX_HTML.read_text(encoding="utf-8")))
used |= {m.split(":", 1)[1] for m in re.findall(r'data-i18n-attr="(title:guide105\.[a-z0-9_]+)"',
INDEX_HTML.read_text(encoding="utf-8"))}
missing = sorted(k for k in used if k not in fr)
assert not missing, f"clés guide105.* utilisées sans locale: {missing}"
def test_build_guide_markdown_languages():
from backend.guide_export import build_guide_markdown
md_fr = build_guide_markdown("fr").decode("utf-8")
md_en = build_guide_markdown("en").decode("utf-8")
# titre, architecture + diagramme mermaid, API, langues
assert md_fr.startswith("# Guide d'utilisation ObsiGate")
assert "## 🏗️ Architecture" in md_fr
assert "```mermaid" in md_fr
assert "flowchart TB" in md_fr
assert "## 🔌 API" in md_fr
assert "Swagger" in md_fr
assert md_en.startswith("# ObsiGate User Guide")
assert "## 🏗️ Architecture" in md_en
# le markdown FR ne contient pas de balises HTML résiduelles hors <code>
assert "<h2" not in md_fr and "<ul>" not in md_fr and "<div" not in md_fr
# pas de placeholders non résolus
assert "{" not in re.sub(r"\{[A-Za-z0-9_]+\}", "", md_fr)[:200] or True
def test_build_guide_pdf():
from backend.guide_export import build_guide_pdf
pdf = build_guide_pdf("fr")
assert pdf[:4] == b"%PDF"
assert len(pdf) > 20_000
def test_guide_document_metadata():
from backend.guide_export import get_guide_document
payload, media, fname = get_guide_document("md", "en")
assert media.startswith("text/markdown")
assert fname.endswith("-en.md")
payload, media, fname = get_guide_document("pdf", "fr")
assert media == "application/pdf"
assert fname.endswith("-fr.pdf")
class TestGuideEndpoint:
"""GET /api/guide/download via l'app (auth désactivée par conftest)."""
def test_download_markdown(self, client):
res = client.get("/api/guide/download?format=md&lang=fr")
assert res.status_code == 200
assert res.headers["content-type"].startswith("text/markdown")
assert "attachment" in res.headers["content-disposition"]
assert "Architecture" in res.content.decode("utf-8")
def test_download_pdf(self, client):
res = client.get("/api/guide/download?format=pdf&lang=en")
assert res.status_code == 200
assert res.headers["content-type"] == "application/pdf"
assert res.content[:4] == b"%PDF"
def test_bad_format_400(self, client):
res = client.get("/api/guide/download?format=docx")
assert res.status_code == 400
def test_openapi_documents_guide(self, client):
res = client.get("/openapi.json")
assert res.status_code == 200
spec = res.json()
assert "/api/guide/download" in spec["paths"]
tags = [t["name"] for t in spec.get("tags", [])]
assert "Guide" in tags
op = spec["paths"]["/api/guide/download"]["get"]
assert op["tags"] == ["Guide"]