Étape 2 de la mission « QA Engineer d’une flotte GenAI multi-tenant ».« Sécurise ton accès et explore l’admin. »
Ce notebook est la couche pédagogique du module 02. Il raconte le module, lit le banc de tests réel (02-navigation-auth.spec.ts), en orchestre l’inventaire et propose des exercices exécutables. Le fichier .spec.ts reste le moteur de test (backend) : on ne le remplace pas, on l’enrobe.
Portabilité. Toutes les cellules de ce notebook s’exécutent sans navigateur ni instance Open WebUI en ligne : elles lisent le code des tests et en dressent l’inventaire. Lancer réellement les tests E2E (contre une vraie plateforme) est documenté au §4 et fait l’objet du capstone — ici on apprend à lire, cartographier et raisonner sur la suite.
1. Objectifs & place dans la mission
La mission compte cinq étapes. Tu es à la deuxième : la prise en main de l’outillage est faite, il faut maintenant sécuriser l’accès et prouver que la navigation et les permissions fonctionnent — porte d’entrée de tout ce qui suit (on ne certifie pas un chat si on ne sait pas certifier un login).
À la fin de ce module, tu sais :
comprendre le pattern storageState : s’authentifier une fois, réutiliser la session ;
tester le RBAC (accès admin réservé) et la navigation entre sections ;
lire les routes SvelteKit d’Open WebUI (/admin, /workspace/*, /channels) ;
gérer les libellés multilingues (FR/EN) qui dérivent entre versions ;
distinguer un échec d’authentification d’un rate-limiting (HTTP 429).
Étape
Module
Ce que tu certifies
1
01 — Découverte
Mon outillage fonctionne, je sais lire un test.
2
02 — Navigation & Auth (ici)
L’accès et l’admin.
3
03 — Chat & Streaming
Le cœur métier (chat IA).
4
04 — RAG, outils & canaux
Les fonctions avancées.
5
05 — Multi-tenant & CI/CD
L’isolation + l’industrialisation.
# --- Setup : localiser la serie et le module, SANS exposer de chemin absolu ---from pathlib import Pathimport re, shutil, subprocessdef _redact(texte: str) ->str:# Anti-fuite : ne jamais afficher de chemin absolu (machine de l'auteur, home...). out = textefor absolu in {str(Path.cwd().resolve()), str(Path.home())}: out = out.replace(absolu, ".") out = out.replace(absolu.replace("/", "\\"), ".")return outdef trouver_racine_serie(depart=None) -> Path:# La racine de la serie = le dossier qui contient playwright.config.ts. p = Path(depart or Path.cwd()).resolve()for cand in [p, *p.parents]:if (cand /"playwright.config.ts").exists():return candfor sub in Path.cwd().resolve().rglob("playwright.config.ts"):return sub.parentraiseFileNotFoundError("playwright.config.ts introuvable depuis le dossier courant")SERIE = trouver_racine_serie()MODULE = SERIE /"02-navigation-authentification"SPEC = MODULE /"02-navigation-auth.spec.ts"print("Serie :", SERIE.name)print("Module :", MODULE.name)print("Banc de test:", SPEC.name, "(present)"if SPEC.exists() else"(ABSENT)")
Serie : Playwright-OWUI
Module : 02-navigation-authentification
Banc de test: 02-navigation-auth.spec.ts (present)
2. Le pattern storageState — s’authentifier une seule fois
L’écueil n°1 du test d’authentification, c’est le rate-limiting : l’endpoint /api/v1/auths/signin d’Open WebUI tolère peu de connexions rapprochées (~2 min entre appels). Si chaque test refaisait un login, on atteindrait le 429 avant la fin du module.
La solution Playwright : un setup dédié se connecte une fois, puis sauvegarde l’état du navigateur (cookies, localStorage, tokens) dans un fichier JSON. Tous les tests chargent ensuite cet état — ils démarrent déjà connectés.
1. auth.setup.ts → Login via formulaire → Sauvegarde .auth/owui.json
2. *.spec.ts → Charge .auth/owui.json → Déjà connecte !
C’est plus rapide (un seul login) et plus robuste (on ne frappe pas le rate-limiter). Le storageState est déclaré une fois dans playwright.config.ts (projet authenticated) et propagé à tous les specs qui en dépendent. Retiens le principe : l’authentification est un fixture, pas une étape de test.
# --- Lire le banc de tests et en extraire la structure (sans le lancer) ---texte = SPEC.read_text(encoding="utf-8")# Titres des tests : test('....', ...) — le spec utilise des guillemets simplestitres = re.findall(r"test\('([^']+)'", texte)# Le bloc describe (le theme du module)describe = re.search(r"describe\('([^']+)'", texte)print("Theme du module :", describe.group(1) if describe else"(non trouve)")print()print(f"{len(titres)} test(s) defini(s) dans {SPEC.name} :")for i, t inenumerate(titres, 1):print(f" {i}. {t}")# Compter les exercices embarques (commentaires // EXERCICE)nb_ex =len(re.findall(r"//\s*EXERCICE", texte))print()print(f"{nb_ex} exercice(s) embarque(s) dans les commentaires du spec.")
Theme du module : 02 — Navigation & Authentification
8 test(s) defini(s) dans 02-navigation-auth.spec.ts :
1. la session authentifiee est valide et persistante
2. acceder au panneau admin — liste des utilisateurs
3. acceder aux reglages admin
4. workspace — lister les bases de connaissances
5. workspace — lister les modeles personnalises
6. workspace — lister les prompts
7. workspace — lister les fonctions
8. acceder a la section Channels
3 exercice(s) embarque(s) dans les commentaires du spec.
3. RBAC et routes SvelteKit — qui peut aller où
Open WebUI est une application SvelteKit : chaque section est une route. Le module 02 en parcourt quatre familles, chacune avec son niveau d’accès (le RBAC — Rôle-Based Access Control) :
Route
Section
Accès
/ , /c/{id}
Chat (nouvelle / existante conversation)
Utilisateur connecté
/admin
Panneau admin — liste des utilisateurs
Admin uniquement
/admin/settings
Réglages — v0.11 : redirige vers /, les réglages vivent dans une fenêtre à onglets (page distincte jusqu’à la v0.10)
Admin uniquement
/workspace/*
Bases de connaissances, modèles, prompts
Utilisateur connecté
/admin/functions
Fonctions (plugins : filtres, actions, outils) — gérées côté admin
Admin uniquement
/channels
Canaux de discussion collaboratifs
Utilisateur connecté
Regardons le test d’accès admin (code réel du spec, annoté) — il illustre la navigation (goto), l’assertion localisée (le libellé dépend de la langue de l’interface) et l’assertion de donnée (la table est rendue) :
test('acceder au panneau admin — liste des utilisateurs',async ({ page }) => {await page.goto('/admin');// 1. naviguer vers la route admin// 2. assertion localisée : « Utilisateurs » (FR) ou « Users » (EN) —// .or() accepte les deux, robuste au drift de traductionawaitexpect( page.getByText('Utilisateurs').or(page.getByText('Users')).first() ).toBeVisible({ timeout:15_000 });// 3. assertion de donnée : la table des utilisateurs est bien rendueawaitexpect(page.locator('table')).toBeVisible({ timeout:15_000 });});
Deux leçons : (1) goto('/admin') suffit — si le storageState est valide et le rôle admin, on arrive ; sinon, SvelteKit redirige (le test échoue proprement). (2) .or() accepte les deux langues : un sélecteur multilingue résiste au drift de libellé entre versions — c’est exactement le réflexe anti-casse qu’on cherche. Et l’assertion ne s’arrête pas au titre : vérifier la table confirme que la page a réellement chargé ses données, pas seulement son en-tête.
# --- Inventaire REEL via Playwright : `npx playwright test --grep '02' --list` ---# Liste les tests SANS les executer (--list) et SANS navigateur.# Ne tourne que si npx + node_modules sont presents ; sinon repli propre.def lister_tests_module(serie: Path, grep: str="02"):if shutil.which("npx") isNone:returnNone, "npx introuvable — `npm install` requis (repli : inventaire statique ci-dessus)."ifnot (serie /"node_modules").exists():returnNone, "node_modules absent — `npm install` requis (repli : inventaire statique ci-dessus)."try:# NB: pas de guillemets autour de {grep} — cmd.exe (Windows) les traite# comme litteraux. grep est un token simple (ex. "02"), sans espace. r = subprocess.run(f"npx playwright test --grep {grep} --list", cwd=str(serie), shell=True, capture_output=True, text=True, timeout=180, ) sortie = (r.stdout or"") + (r.stderr or"")return r.returncode, _redact(sortie.strip())exceptExceptionas e:returnNone, f"{type(e).__name__}: {e}"code_retour, sortie = lister_tests_module(SERIE, "02")print("returncode:", code_retour)print(sortie if sortie else"(aucune sortie)")
L’inventaire ci-dessus se fait hors-ligne. Pour exécuter les tests contre une vraie instance Open WebUI, il faut un .env configuré (OWUI_URL, OWUI_EMAIL, OWUI_PASSWORD) — jamais commité — puis :
npm install # dependancesnpx playwright install chromium # navigateurnpm run test:module2 # ce module uniquementnpx playwright test --grep"02"--headed# navigateur VISIBLE (debug)PWDEBUG=1 npx playwright test --grep"02"--headed# Playwright Inspectornpm run report # rapport HTML
L’exécution live dépend d’une plateforme et de secrets : elle est différée ici (et constitue le cœur du capstone). Ce notebook reste reproductible partout, sans secret.
# --- Demonstration executable : classer une route par sa zone d'accès ---# Pas de navigateur : on raisonne sur la *forme* de la route (le concept RBAC du para. 3).def zone_acces(route: str) ->str: r = route.strip()if r.startswith("/admin"):return"admin"# RBAC : admin uniquementif r.startswith("/workspace"):return"workspace"# utilisateur connectéif r.startswith("/channels"):return"channels"# utilisateur connectéif r =="/"or r.startswith("/c/"):return"chat"# utilisateur connecté (défaut)return"inconnue"exemples = ["/", "/c/abc-123", "/admin", "/admin/settings","/workspace/models", "/workspace/knowledge", "/channels", "/workspace/prompts"]for r insorted(exemples, key=zone_acces):print(f" zone {zone_acces(r):10} : {r}")
zone admin : /admin
zone admin : /admin/settings
zone channels : /channels
zone chat : /
zone chat : /c/abc-123
zone workspace : /workspace/models
zone workspace : /workspace/knowledge
zone workspace : /workspace/prompts
5. Interpréter les résultats — et le piège du 429
Un run renvoie, pour chaque test, un statut. Sur le module 02, un statut mérite une attention particulière :
passed — le test a vérifié ce qu’il devait. ✅
failed — une assertion a échoué ou un sélecteur n’a rien trouvé → drift d’interface (libellé FR/EN) ou redirection inattendue (RBAC), à investiguer.
skipped — volontairement ignoré (section non configurée, ex. Channels désactivé). Un skip n’est pas une régression.
flaky — passe au retry après un premier échec (souvent : plateforme lente).
Piège spécifique au module 02 — le 429 (rate-limiting). Si le storageState est absent ou expiré, un test peut tenter un login de repli et recevoir un 429 Too Many Requests. Ce n’est pas un échec fonctionnel de l’application : c’est le rate-limiter de l’endpoint d’auth qui protège contre le brute-force. Un 429 dans le rapport = problème de setup de test (régénérer le .auth/owui.json), pas une régression d’Open WebUI. Savoir l’identifier évite un no-go injustifié le jour d’une certification.
# --- Qualifier un rapport (echantillon module 02) ---rapport_exemple = [ {"test": "session authentifiee persistante", "status": "passed"}, {"test": "acceder au panneau admin", "status": "passed"}, {"test": "reglages admin", "status": "failed"}, # ex. : drift de libelle FR/EN {"test": "workspace — bases de connaissances", "status": "skipped"}, # KB non configuree {"test": "workspace — modeles personnalises", "status": "passed"}, {"test": "section Channels", "status": "skipped"}, # channels non actives]def qualifier(rapport): n = {"passed": 0, "failed": 0, "skipped": 0, "flaky": 0}for t in rapport: n[t["status"]] = n.get(t["status"], 0) +1return nbilan = qualifier(rapport_exemple)print("Bilan module 02 (echantillon) :", bilan)verdict ="GO"if bilan["failed"] ==0else"NO-GO (investiguer les failed — ici : drift de libelle admin)"print("Verdict provisoire :", verdict)
Bilan module 02 (echantillon) : {'passed': 3, 'failed': 1, 'skipped': 2, 'flaky': 0}
Verdict provisoire : NO-GO (investiguer les failed — ici : drift de libelle admin)
Exercices
Trois exercices, tous exécutables tels quels (ils tournent sans erreur et renvoient un placeholder). Remplace le corps de chaque fonction, ré-exécute, et compare au comportement attendu décrit plus haut.
Exercice 1 — Zone d’accès d’une route
Complète zone_d_acces pour qu’elle renvoie "admin", "workspace", "channels", "chat" ou "inconnue" selon le préfixe de la route, sans réutiliser zone_acces du §3. Appuie-toi sur le tableau RBAC du §3.
def zone_d_acces(route: str) ->str:# TODO : renvoyer "admin" / "workspace" / "channels" / "chat" / "inconnue"# selon le prefixe de la route. Indice : inspecte le debut de la chaîne.return"inconnue"# placeholder — a remplacer# Test rapide (doit afficher "admin" une fois complete) :print("zone de '/admin/settings' :", zone_d_acces("/admin/settings"))
zone de '/admin/settings' : inconnue
Exercice 2 — Relier un test à son concept
Complète concept_du_test : à partir du titre d’un test du module 02, renvoie le concept principal qu’il illustre (ex. "storageState", "admin", "workspace", "channels"). Inspire-toi de l’inventaire du §2 et du tableau des routes du §3.
def concept_du_test(titre: str) ->str:# TODO : mapper un titre de test -> son concept principal# Ex : "acceder au panneau admin..." -> "admin"return"a determiner"# placeholderfor t in ["la session authentifiee est valide et persistante","acceder au panneau admin — liste des utilisateurs","workspace — lister les prompts","acceder a la section Channels"]:print(f" {t!r} -> {concept_du_test(t)}")
'la session authentifiee est valide et persistante' -> a determiner
'acceder au panneau admin — liste des utilisateurs' -> a determiner
'workspace — lister les prompts' -> a determiner
'acceder a la section Channels' -> a determiner
Exercice 3 — L’assertion manquante
Le spec laisse un exercice en commentaire (ex. « comptez le nombre d’utilisateurs dans la table admin »). Une assertion Playwright typique sur la route admin vérifie l’URL atteinte. En TypeScript ce serait await expect(page).toHaveURL(/\/admin/);. Ici, renvoie cette ligne sous forme de chaîne depuis assertion_url_admin() (on valide la forme, pas l’exécution navigateur).
def assertion_url_admin() ->str:# TODO : renvoyer la ligne d'assertion Playwright verifiant l'URL admin# Forme attendue : await expect(page).toHaveURL(/\/admin/);return"a completer"# placeholderprint(assertion_url_admin())
a completer
Conclusion & étape suivante
Tu sais désormais sécuriser l’accès (storageState), raisonner sur le RBAC et les routes SvelteKit, gérer les libellés multilingues et — crucial — distinguer un 429 (rate-limiting) d’une vraie régression. La porte d’entrée de la flotte est certifiée.
➡️ Étape 3 — Module 03 : Chat & Streaming. Tu y attaqueras le cœur métier : le chat IA en streaming, son non-déterminisme, et les assertions tolérantes qui vont avec.
Note de reproductibilité (C.3). Les cellules de ce notebook ont été exécutées hors-ligne : lecture du .spec.ts, inventaire --list (sans navigateur) et démonstrations pure-Python. Aucune ne requiert d’instance Open WebUI ni de secret. L’exécution live des tests E2E (navigateur + plateforme) est volontairement différée au capstone ; la suite a depuis été revalidée sur la flotte v0.11.0 (campagne #9854, 2026-08-10), dont les pièges mesurés (redirection /admin/settings, payload chat) sont documentés dans WHATS-NEW-v0.11.md. Aucun claim de compatibilité au-delà de v0.11.0 n’est porté par ce module.