Ctrl K
ComunidadGuías…

test_serve_app_js_attente_relance.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""La page se recharge toute seule quand blink2video revient après une mise à
jour, Appliquer ou Redémarrer (issue #35 : « Instead of an Error page and a
manual refresh »). Le vrai code de serve_app.js, exécuté par node avec un
faux fetch et un faux location.

Deux façons de rater le retour existaient : un serveur relancé plus vite que
le sondage de 2 s (jamais vu absent, alors que son nouveau jeton fait
répondre 403 à l'ancienne page), et un mandataire (nginx...) qui répond 502
au lieu de refuser la connexion pendant l'arrêt."""

from __future__ import annotations

import json
import re
import shutil
import subprocess
import unittest
from pathlib import Path

RACINE = Path(__file__).parent


class TestsSondeDeRelance(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.node = shutil.which("node")
        if cls.node is None:
            raise unittest.SkipTest("node introuvable")
        cls.source = (RACINE / "serve_app.js").read_text(encoding="utf-8")
        fonctions = []
        for nom in ("etatServeur", "sonderRelance"):
            trouve = re.search(rf"^async function {nom}\(.*?^\}}", cls.source,
                               re.DOTALL | re.MULTILINE)
            if trouve is None:
                raise AssertionError(f"fonction {nom}() introuvable")
            fonctions.append(trouve.group(0))
        cls.code = "\n".join(fonctions)

    def executer(self, reponses: list) -> dict:
        """Enchaîne un sondage par réponse : « erreur » fait lever fetch(),
        un entier est un code HTTP. Rend l'état vu et les rechargements."""
        script = (
            f"const reponses = {json.dumps(reponses)};\n"
            "let rechargements = 0;\n"
            "globalThis.location = {reload: () => { rechargements += 1; }};\n"
            "globalThis.fetch = async () => {\n"
            "  const r = reponses.shift();\n"
            "  if (r === 'erreur') throw new TypeError('Connexion refusée');\n"
            "  return {status: r};\n"
            "};\n" + self.code + "\n"
            "(async () => {\n"
            "  const suivi = {parti: false}, apres = [];\n"
            f"  for (let i = 0; i < {len(reponses)}; i++) {{\n"
            "    await sonderRelance(suivi); apres.push([suivi.parti, rechargements]);\n"
            "  }\n"
            "  console.log(JSON.stringify(apres));\n"
            "})();\n"
        )
        resultat = subprocess.run([self.node, "-e", script], capture_output=True,
                                  text=True, encoding="utf-8", check=True)
        return json.loads(resultat.stdout)

    def test_le_serveur_est_vu_absent_puis_revient(self):
        # Connexion refusée, puis le serveur répond : rechargement au retour.
        self.assertEqual(self.executer(["erreur", "erreur", 200]),
                         [[True, 0], [True, 0], [True, 1]])

    def test_un_mandataire_qui_repond_502_503_504_compte_comme_absence(self):
        for code in (502, 503, 504):
            with self.subTest(code=code):
                self.assertEqual(self.executer([code, code, 200]),
                                 [[True, 0], [True, 0], [True, 1]])

    def test_403_recharge_sans_avoir_vu_l_absence(self):
        # Le serveur relancé (nouveau jeton) répond 403 à l'ancienne page.
        self.assertEqual(self.executer([403]), [[False, 1]])

    def test_rien_a_faire_tant_que_le_serveur_n_a_pas_disparu(self):
        self.assertEqual(self.executer([200, 200, 200]),
                         [[False, 0], [False, 0], [False, 0]])

    def test_une_erreur_500_est_un_serveur_vivant_pas_une_absence(self):
        self.assertEqual(self.executer([500]), [[False, 0]])


class TestsMiseAJourEtRedemarrage(unittest.TestCase):
    def test_les_trois_attentes_passent_par_la_meme_sonde(self):
        source = (RACINE / "serve_app.js").read_text(encoding="utf-8")
        self.assertIn("miseAJourAttente = setInterval(() => sonderRelance(suivi), 2000);", source)
        self.assertIn("const attente = setInterval(() => sonderRelance(suivi), 2000);", source)
        # Plus de sonde écrite à la main, qui ne reconnaissait ni le 403 du
        # serveur relancé ni le 502 d'un mandataire.
        self.assertNotIn('await fetch("/api/status", { cache: "no-store" });\n'
                         "      if (parti) location.reload();", source)


if __name__ == "__main__":
    unittest.main()