#!/usr/bin/env python3 """Tests de hreflang_watch.py y de la separación pendiente/roto. Lo que se protege aquí no es «que no dé error»: es la disciplina de aviso. Un vigilante que canta a diario cinco rojos que nadie puede arreglar se convierte en ruido, y entonces el rojo de verdad tampoco se ve. Eso ya pasó con los og_*/twitter_* de las páginas. python3 -m pytest test_hreflang_watch.py -q python3 test_hreflang_watch.py # sin pytest """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import seo_hreflang as H import hreflang_watch as W def _con_posts(en, es): """Sustituye la lectura de Ghost por corpus de mentira, por sitio.""" orig = H.trae H.trae = lambda site, slugs: (en if site == "en" else es) return orig P = [("caso-en", "caso-es")] def _post(slug, status, head=""): return {"id": "i-" + slug, "slug": slug, "status": status, "codeinjection_head": head} # ─────────────── pendiente NO es roto (la cola de agosto) ─────────────── def test_un_espejo_programado_sale_como_pendiente_no_como_fallo(): orig = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "scheduled")}) try: esp = H.pendientes(P) finally: H.trae = orig assert esp == {("caso-en", "caso-es"): "published/scheduled"}, esp def test_los_dos_publicados_no_estan_pendientes(): orig = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "published")}) try: assert H.pendientes(P) == {} finally: H.trae = orig def test_un_slug_que_no_existe_en_ghost_tambien_es_pendiente(): """Si se renombra un slug en hreflang-parejas.md y no en Ghost, no hay que tratarlo como una página rota: no hay nada que escribir todavía.""" orig = _con_posts({}, {"caso-es": _post("caso-es", "published")}) try: assert H.pendientes(P) == {("caso-en", "caso-es"): "no encontrado en Ghost"} finally: H.trae = orig # ─────────────── que un programado NUNCA se escriba ─────────────── def test_no_se_planifica_nada_mientras_el_espejo_este_programado(): """Es la salvaguarda de fondo: un hreflang a una URL futura es un 404, y un 404 declarado como versión alternativa es peor que no declarar nada.""" orig_t = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "scheduled")}) orig_p = H.lee_parejas H.lee_parejas = lambda: P try: plan, avisos, _ = H.cambios() finally: H.trae, H.lee_parejas = orig_t, orig_p assert plan == [], plan assert any("404" in w for w in avisos), avisos def test_en_cuanto_se_publica_el_espejo_aparece_el_plan(): """La otra mitad: el vigilante no sirve si es tan prudente que nunca actúa.""" orig_t = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "published")}) orig_p = H.lee_parejas H.lee_parejas = lambda: P try: plan, _, _ = H.cambios() finally: H.trae, H.lee_parejas = orig_t, orig_p assert len(plan) == 2, plan # escribe en los DOS lados assert {s for s, _, _ in plan} == {"en", "es"}, plan for _, _, nuevo in plan: assert H.INICIO in nuevo and H.FIN in nuevo assert 'hreflang="x-default"' in nuevo # sin él Google descarta el grupo # ─────────────── silencio cuando no hay nada que contar ─────────────── def test_sin_nada_que_abrir_no_avisa_por_telegram(): """26 días de cada 31 no habrá nada. Si esos días mandara mensaje, el día que importa se leería igual que los otros.""" orig_t = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "scheduled")}) orig_p, orig_tg = H.lee_parejas, W.telegram H.lee_parejas = lambda: P avisado = [] W.telegram = lambda t: avisado.append(t) or True try: rc = W.main() finally: H.trae, H.lee_parejas, W.telegram = orig_t, orig_p, orig_tg assert rc == 0, rc assert avisado == [], avisado def test_el_aviso_dice_que_pareja_y_que_queda_pendiente(): orig_t = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "published")}) orig_p, orig_tg, orig_a, orig_v = (H.lee_parejas, W.telegram, H.aplica, W.verifica) H.lee_parejas = lambda: P avisado = [] W.telegram = lambda t: avisado.append(t) or True H.aplica = lambda plan, log=print: ([(s, p["slug"]) for s, p, _ in plan], [], "/tmp/copia.json") W.verifica = lambda abiertas: [] try: rc = W.main() finally: (H.trae, H.lee_parejas, W.telegram, H.aplica, W.verifica) = ( orig_t, orig_p, orig_tg, orig_a, orig_v) assert rc == 0, rc assert len(avisado) == 1, avisado assert "caso-en" in avisado[0] and "caso-es" in avisado[0] assert "verificado en la web pública" in avisado[0] def test_si_ghost_guarda_pero_la_web_no_lo_emite_grita_y_sale_con_error(): """El caso peligroso: Ghost dice OK y la página no lo tiene. Sin esto, el vigilante daría por hecho algo que no está.""" orig_t = _con_posts({"caso-en": _post("caso-en", "published")}, {"caso-es": _post("caso-es", "published")}) orig_p, orig_tg, orig_a, orig_v = (H.lee_parejas, W.telegram, H.aplica, W.verifica) H.lee_parejas = lambda: P avisado = [] W.telegram = lambda t: avisado.append(t) or True H.aplica = lambda plan, log=print: ([(s, p["slug"]) for s, p, _ in plan], [], "/tmp/copia.json") W.verifica = lambda abiertas: ["caso-es: en dice None, debería ser X"] try: rc = W.main() finally: (H.trae, H.lee_parejas, W.telegram, H.aplica, W.verifica) = ( orig_t, orig_p, orig_tg, orig_a, orig_v) assert rc == 1, "un fallo silencioso deja exit 0 y systemd lo da por bueno" assert "🚨" in avisado[0] and "MAL en la web pública" in avisado[0], avisado if __name__ == "__main__": fallos = 0 for nombre, fn in sorted(globals().items()): if not nombre.startswith("test_"): continue try: fn() print(f" ✓ {nombre}") except AssertionError as exc: fallos += 1 print(f" ✗ {nombre}: {exc}") print(f"\n{'✓ todo pasa' if not fallos else f'✗ {fallos} fallo(s)'}") sys.exit(1 if fallos else 0)