seo-watch: vigila la indexación en Google (ventana 5-60 días, huella sin la antigüedad)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests del chequeo de INDEXACIÓN de seo_watch.py.
|
||||
|
||||
Cada uno es la cicatriz de algo que ya salió mal en producción una vez:
|
||||
|
||||
1. avisar demasiado pronto convierte el vigilante en ruido y se deja de leer
|
||||
2. una huella que cambia sola reenvía hallazgos viejos como si fueran nuevos
|
||||
(ver la memoria «orden no determinista y huellas»)
|
||||
3. un chequeo que NO puede correr tiene que verse; si se traga la excepción,
|
||||
el informe sale limpio justo el día en que estaba ciego
|
||||
|
||||
python3 -m pytest test_seo_watch.py -q
|
||||
python3 test_seo_watch.py
|
||||
for h in 1 2 3; do PYTHONHASHSEED=$h python3 test_seo_watch.py; done
|
||||
"""
|
||||
import datetime as dt
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import seo_watch as W
|
||||
|
||||
|
||||
def _post(slug, dias, status="published"):
|
||||
cuando = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=dias, hours=1)
|
||||
return {"slug": slug, "status": status,
|
||||
"published_at": cuando.strftime("%Y-%m-%dT%H:%M:%S.000Z")}
|
||||
|
||||
|
||||
def _con_gsc(respuestas, credencial_rota=False):
|
||||
"""Inyecta un seo_gsc de mentira. `respuestas` es {slug: (icono, texto)}."""
|
||||
fake = types.ModuleType("seo_gsc")
|
||||
fake.consultados = []
|
||||
|
||||
def carga_credencial():
|
||||
if credencial_rota:
|
||||
raise FileNotFoundError("no hay service-account.json")
|
||||
return {"client_email": "x"}
|
||||
|
||||
fake.carga_credencial = carga_credencial
|
||||
fake.token = lambda sa: "tok"
|
||||
|
||||
def inspecciona_url(tok, site, slug):
|
||||
fake.consultados.append(slug)
|
||||
return {"slug": slug}
|
||||
|
||||
fake.inspecciona_url = inspecciona_url
|
||||
fake.etiqueta_indexacion = lambda d: respuestas.get(d["slug"], ("✅", "indexado"))
|
||||
sys.modules["seo_gsc"] = fake
|
||||
return fake
|
||||
|
||||
|
||||
def test_no_avisa_de_un_post_recien_publicado():
|
||||
"""Google tarda días aunque todo vaya bien: avisar el día 1 es ruido."""
|
||||
g = _con_gsc({"nuevo": ("⏳", "Google no lo conoce")})
|
||||
fuera = W.check_indexacion([_post("nuevo", 1)], "en")
|
||||
assert fuera == [], fuera
|
||||
assert g.consultados == [], "ni siquiera debe gastar cuota de API"
|
||||
|
||||
|
||||
def test_avisa_pasado_el_periodo_de_gracia():
|
||||
_con_gsc({"viejo": ("⏳", "Google no lo conoce")})
|
||||
fuera = W.check_indexacion([_post("viejo", W.DIAS_GRACIA + 3)], "en")
|
||||
assert len(fuera) == 1, fuera
|
||||
assert fuera[0][0] == "viejo" and "no lo conoce" in fuera[0][2], fuera
|
||||
|
||||
|
||||
def test_calla_lo_que_si_esta_indexado():
|
||||
"""Solo habla de lo roto: un post sano no debe aparecer en el informe."""
|
||||
_con_gsc({}) # todo devuelve ✅
|
||||
assert W.check_indexacion([_post("sano", 20)], "en") == []
|
||||
|
||||
|
||||
def test_ignora_lo_muy_viejo_y_lo_no_publicado():
|
||||
"""Fuera de ventana ya no es un incidente que se arregle pidiendo indexación."""
|
||||
g = _con_gsc({"antiguo": ("⏳", "x"), "borrador": ("⏳", "x")})
|
||||
posts = [_post("antiguo", W.DIAS_VENTANA + 10),
|
||||
_post("borrador", 30, status="draft")]
|
||||
assert W.check_indexacion(posts, "en") == []
|
||||
assert g.consultados == []
|
||||
|
||||
|
||||
def test_la_huella_NO_cambia_cuando_solo_pasa_un_dia():
|
||||
"""El fallo que mataría al vigilante: la antigüedad sube cada mañana, así
|
||||
que si entrase en la huella repetiría el mismo hallazgo a diario."""
|
||||
ayer = [("release-5", 21, "Google no lo conoce")]
|
||||
hoy = [("release-5", 22, "Google no lo conoce")]
|
||||
vacio = ([], [], [], [], [], {}, [], [], {})
|
||||
a = W.fingerprint(*vacio, ayer)
|
||||
b = W.fingerprint(*vacio, hoy)
|
||||
assert a == b, "un día más no puede contar como hallazgo nuevo"
|
||||
|
||||
|
||||
def test_la_huella_SI_cambia_cuando_cambia_el_estado():
|
||||
"""Pasar de «no lo conoce» a «descubierto» es noticia: hubo movimiento."""
|
||||
vacio = ([], [], [], [], [], {}, [], [], {})
|
||||
a = W.fingerprint(*vacio, [("release-5", 21, "Google no lo conoce")])
|
||||
b = W.fingerprint(*vacio, [("release-5", 21, "descubierto, sin rastrear")])
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_orden_estable_con_empate_de_dias():
|
||||
"""Dos posts del mismo día: sin desempate por slug el orden lo decide el
|
||||
corpus y la huella cambia sola de una ejecución a otra."""
|
||||
_con_gsc({"bbb": ("⏳", "x"), "aaa": ("⏳", "x")})
|
||||
posts = [_post("bbb", 10), _post("aaa", 10)]
|
||||
ida = [s for s, _, _ in W.check_indexacion(posts, "en")]
|
||||
_con_gsc({"bbb": ("⏳", "x"), "aaa": ("⏳", "x")})
|
||||
vuelta = [s for s, _, _ in W.check_indexacion(list(reversed(posts)), "en")]
|
||||
assert ida == vuelta == ["aaa", "bbb"], (ida, vuelta)
|
||||
|
||||
|
||||
def test_el_mas_viejo_va_primero():
|
||||
_con_gsc({"a": ("⏳", "x"), "b": ("⏳", "x")})
|
||||
fuera = W.check_indexacion([_post("a", 8), _post("b", 30)], "en")
|
||||
assert [s for s, _, _ in fuera] == ["b", "a"], fuera
|
||||
|
||||
|
||||
def test_si_no_hay_credencial_se_ve_en_el_informe():
|
||||
"""Un chequeo que no corre NO puede dejar el informe limpio."""
|
||||
_con_gsc({}, credencial_rota=True)
|
||||
fuera = W.check_indexacion([_post("x", 10)], "en")
|
||||
assert len(fuera) == 1 and "no se pudo comprobar" in fuera[0][2], fuera
|
||||
linea = W.build_report([], [], [], [], 1, [], {}, [], [], {}, fuera)
|
||||
assert "no se pudo comprobar" in linea, linea
|
||||
|
||||
|
||||
def test_un_error_de_la_api_no_tumba_el_resto():
|
||||
"""Si una URL revienta, las demás se siguen comprobando."""
|
||||
fake = _con_gsc({"malo": ("❓", "error 429"), "bueno": ("⏳", "no lo conoce")})
|
||||
|
||||
def revienta(tok, site, slug):
|
||||
fake.consultados.append(slug)
|
||||
if slug == "malo":
|
||||
raise TimeoutError("timeout")
|
||||
return {"slug": slug}
|
||||
|
||||
fake.inspecciona_url = revienta
|
||||
fuera = W.check_indexacion([_post("malo", 12), _post("bueno", 11)], "en")
|
||||
assert len(fuera) == 2, fuera
|
||||
assert any("TimeoutError" in e for _, _, e in fuera), fuera
|
||||
|
||||
|
||||
def test_el_informe_saca_los_hallazgos_de_indexacion():
|
||||
txt = W.build_report([], [], [], [], 1, [], {}, [], [], {},
|
||||
[("release-5", 22, "Google no lo conoce")])
|
||||
assert "NO ha indexado" in txt and "release-5" in txt and "22 días" in txt, txt
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user