fix(dashboard): DETTE-024 — download fleet, fallback legacy rendu visible
_resolve_lea_zip_template() reste résolu à la volée (full buildé après démarrage OK) ; ajout d'un WARNING explicite quand le full est absent et qu'on retombe sur le ZIP léger non autoportant (plus de fallback silencieux). Fonction injectable pour tests. 4 tests + 32 non-régression verts. refs DETTE-024 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
tests/unit/test_resolve_lea_zip_template.py
Normal file
75
tests/unit/test_resolve_lea_zip_template.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Tests unitaires pour _resolve_lea_zip_template (DETTE-024).
|
||||
|
||||
La fonction est injectable (full_path, legacy_path en paramètres)
|
||||
→ testable sans instancier Flask ni lire le vrai deploy/.
|
||||
|
||||
Pattern anti-DETTE-013 : os.environ.setdefault avant l'import du module.
|
||||
"""
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DASHBOARD_AUTH_DISABLED", "true")
|
||||
|
||||
import pytest # noqa: E402
|
||||
from web_dashboard.app import _resolve_lea_zip_template # noqa: E402
|
||||
|
||||
|
||||
class TestResolveLéaZipTemplate:
|
||||
"""DETTE-024 — sélection du ZIP template pour le download fleet."""
|
||||
|
||||
def test_full_present_retourne_full(self, tmp_path):
|
||||
"""Si le ZIP complet autoportant est présent, il est retourné."""
|
||||
full = tmp_path / "Lea_full_v1.0.1.zip"
|
||||
legacy = tmp_path / "Lea_v1.0.0.zip"
|
||||
full.write_bytes(b"full-stub")
|
||||
legacy.write_bytes(b"legacy-stub")
|
||||
|
||||
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
|
||||
|
||||
assert result == full, f"Attendu full ({full}), obtenu {result}"
|
||||
|
||||
def test_full_absent_retourne_legacy_avec_warning(self, tmp_path, caplog):
|
||||
"""Si le ZIP complet est absent, le legacy est retourné + WARNING loggué.
|
||||
|
||||
Le WARNING est le signal observable en production (DETTE-024) :
|
||||
sans lui, le fallback silencieux rendait le problème invisible.
|
||||
"""
|
||||
import logging
|
||||
|
||||
full = tmp_path / "Lea_full_v1.0.1.zip"
|
||||
legacy = tmp_path / "Lea_v1.0.0.zip"
|
||||
# full intentionnellement absent
|
||||
legacy.write_bytes(b"legacy-stub")
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
|
||||
|
||||
assert result == legacy, f"Attendu legacy ({legacy}), obtenu {result}"
|
||||
# Le WARNING DETTE-024 doit apparaître dans les logs
|
||||
assert any(
|
||||
"DETTE-024" in record.message for record in caplog.records
|
||||
), (
|
||||
"Un WARNING DETTE-024 doit être émis quand le ZIP complet est absent "
|
||||
f"(logs: {[r.message for r in caplog.records]})"
|
||||
)
|
||||
|
||||
def test_full_et_legacy_absents_retourne_none(self, tmp_path):
|
||||
"""Si aucun ZIP n'existe, retourne None (la route renvoie 500)."""
|
||||
full = tmp_path / "Lea_full_v1.0.1.zip"
|
||||
legacy = tmp_path / "Lea_v1.0.0.zip"
|
||||
# aucun des deux créés
|
||||
|
||||
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
|
||||
|
||||
assert result is None, f"Attendu None, obtenu {result}"
|
||||
|
||||
def test_full_prime_sur_legacy(self, tmp_path):
|
||||
"""Le full est retourné même si le legacy existe aussi (priorité correcte)."""
|
||||
full = tmp_path / "Lea_full_v1.0.1.zip"
|
||||
legacy = tmp_path / "Lea_v1.0.0.zip"
|
||||
full.write_bytes(b"full-stub")
|
||||
legacy.write_bytes(b"legacy-stub")
|
||||
|
||||
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
|
||||
|
||||
assert result == full
|
||||
assert result != legacy
|
||||
@@ -2231,16 +2231,37 @@ _LEA_ZIP_TEMPLATE_FULL = BASE_PATH / "deploy" / "build" / "Lea_full_v1.0.1.zip"
|
||||
_LEA_ZIP_TEMPLATE_LEGACY = BASE_PATH / "deploy" / "Lea_v1.0.0.zip"
|
||||
|
||||
|
||||
def _resolve_lea_zip_template():
|
||||
def _resolve_lea_zip_template(
|
||||
full_path: Path = _LEA_ZIP_TEMPLATE_FULL,
|
||||
legacy_path: Path = _LEA_ZIP_TEMPLATE_LEGACY,
|
||||
) -> "Path | None":
|
||||
"""Résout le ZIP à servir, à la volée (le complet peut être buildé
|
||||
après le démarrage du dashboard). Préfère le ZIP complet autoportant ;
|
||||
retombe sur l'ancien ZIP léger uniquement s'il existe.
|
||||
Retourne None si aucun template n'est présent.
|
||||
|
||||
Les paramètres full_path/legacy_path sont injectables pour les tests
|
||||
(évite de démarrer Flask — DETTE-013).
|
||||
|
||||
⚠️ DETTE-024 : si le ZIP complet est absent, un avertissement est loggué
|
||||
explicitement pour ne pas masquer silencieusement l'absence du full.
|
||||
"""
|
||||
if _LEA_ZIP_TEMPLATE_FULL.exists():
|
||||
return _LEA_ZIP_TEMPLATE_FULL
|
||||
if _LEA_ZIP_TEMPLATE_LEGACY.exists():
|
||||
return _LEA_ZIP_TEMPLATE_LEGACY
|
||||
if full_path.exists():
|
||||
return full_path
|
||||
# Full absent → fallback sur le legacy, mais log d'avertissement obligatoire.
|
||||
if legacy_path.exists():
|
||||
try:
|
||||
api_logger.warning(
|
||||
"DETTE-024 — ZIP Léa complet autoportant ABSENT (%s) ; "
|
||||
"fallback sur ZIP léger NON autoportant (%s). "
|
||||
"Le poste recevra un ZIP sans Python embarqué → non installable "
|
||||
"sans Python système. Exécuter deploy/build_package_full.sh.",
|
||||
full_path,
|
||||
legacy_path,
|
||||
)
|
||||
except Exception:
|
||||
pass # api_logger pas encore initialisé au module load (import tardif ok)
|
||||
return legacy_path
|
||||
return None
|
||||
|
||||
|
||||
@@ -2389,8 +2410,14 @@ def download_agent_package(machine_id):
|
||||
# Sécurité : l'auth Basic est déjà gérée par before_request
|
||||
|
||||
# 1. Résoudre + vérifier que le ZIP template existe (à la volée)
|
||||
# _resolve_lea_zip_template() logue un WARNING si le full est absent (DETTE-024).
|
||||
zip_template = _resolve_lea_zip_template()
|
||||
if zip_template is None:
|
||||
api_logger.error(
|
||||
"download_agent_package(%s) — aucun ZIP template présent. "
|
||||
"full=%s legacy=%s",
|
||||
machine_id, _LEA_ZIP_TEMPLATE_FULL, _LEA_ZIP_TEMPLATE_LEGACY,
|
||||
)
|
||||
return jsonify({
|
||||
'error': 'ZIP template introuvable',
|
||||
'detail': (
|
||||
@@ -2399,6 +2426,13 @@ def download_agent_package(machine_id):
|
||||
'autoportant) ou deploy/build_package.sh (ZIP léger).'
|
||||
),
|
||||
}), 500
|
||||
is_full = (zip_template == _LEA_ZIP_TEMPLATE_FULL)
|
||||
zip_kind = "full-autoportant" if is_full else "legacy-léger⚠️"
|
||||
api_logger.info(
|
||||
"download_agent_package(%s) — ZIP sélectionné : %s (%s, %d Ko)",
|
||||
machine_id, zip_template.name, zip_kind,
|
||||
zip_template.stat().st_size // 1024,
|
||||
)
|
||||
|
||||
# 2. Vérifier que le machine_id est enregistré
|
||||
agent = _fetch_fleet_agent(machine_id)
|
||||
|
||||
Reference in New Issue
Block a user