159 lines
5.8 KiB
Python
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"]
|