mdns-termux/tests/test_stdlib_resolve.py

50 lines
1.5 KiB
Python
Raw Permalink Normal View History

feat: publisher-daemon (singleton + re-announce), sync-hosts, status/stop y tests Bloque de robustez operativa + integracion de sistema + calidad: Publisher como servicio (lib/mdns_tools/service.py): - `mdns publish --daemon`: daemon singleton con lock PID en ~/.cache/mdns/publisher.pid. Un segundo intento es rechazado -> evita el problema de "Termux SMB" duplicado (varios publishers compitiendo por el mismo nombre, que zeroconf renombra a "-2"). - Re-anuncio automatico al cambiar la IP local (WiFi<->datos, roaming): unregister + register con la nueva IP para que el registro no quede stale. - Apagado limpio: maneja SIGTERM ademas de SIGINT (loop en ticks de 1s) y envia goodbye packets para que los clientes borren la entrada al instante. - `mdns status` (running/host/ip/servicios/desde) y `mdns stop`. Integracion de sistema (lib/mdns_tools/hosts.py): - `mdns sync-hosts [--dry-run]`: escribe un bloque gestionado en $PREFIX/etc/hosts (o /etc/hosts) para que ping/curl/kubectl nativos resuelvan .local sin el wrapper `resolve`. Solo toca su propio bloque. Refactor publish.py: expone helpers reutilizables (local_ipv4, load_config, build_infos, register_all, unregister_all) que consume el daemon. Tests offline (tests/, pytest, 20 casos, sin red): - construccion/parseo de queries A, seleccion de IP ante multi-homing (rechazo de rangos virtuales), cache con TTL y familias, PTR reverse, render/strip del bloque de hosts. - pyproject.toml con metadata, deps, extras [test] y config de pytest. README: seccion de daemon, sync-hosts y tests; tabla RFC ampliada. bashrc.snippet: aliases mdns-up/status/down/hosts (consolidados). install.sh: fix de backticks; symlinks ya cubren mdns + lib via resolve(). Nota: publish.py (109) y service.py (110) exceden levemente las 100 lineas; se dejan como modulos cohesivos de responsabilidad unica, igual que el dispatcher CLI, para no fragmentar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-20 17:59:10 +00:00
"""Parser tests for the dependency-free resolver (no network)."""
import socket
import struct
from mdns_tools import _stdlib_resolve as sr
def _a_response(hostname: str, ips: list[str], ttl: int = 120) -> bytes:
"""Craft a minimal mDNS response with one question and N A answers."""
header = struct.pack(">HHHHHH", 0, 0x8400, 1, len(ips), 0, 0)
labels = b""
for part in hostname.rstrip(".").split("."):
labels += bytes([len(part)]) + part.encode()
labels += b"\x00"
question = labels + struct.pack(">HH", 1, 1)
answers = b""
for ip in ips:
answers += labels + struct.pack(">HHIH", 1, 1, ttl, 4)
answers += socket.inet_aton(ip)
return header + question + answers
def test_build_query_roundtrip():
q = sr.build_query("host.local", sr.QTYPE_A)
# header is 12 bytes; QDCOUNT must be 1
assert struct.unpack(">H", q[4:6])[0] == 1
assert q.endswith(struct.pack(">HH", 1, 1))
def test_skip_name_plain():
data = b"\x04host\x05local\x00rest"
off = sr.skip_name(data, 0)
assert data[off:off + 4] == b"rest"
def test_parse_all_a_single():
resp = _a_response("dell.local", ["192.168.1.17"])
recs = sr.parse_all_a(resp)
assert recs == [("192.168.1.17", 120)]
def test_parse_all_a_multiple():
resp = _a_response("multi.local", ["192.168.1.5", "192.168.122.1"])
ips = [ip for ip, _ in sr.parse_all_a(resp)]
assert ips == ["192.168.1.5", "192.168.122.1"]
def test_parse_all_a_empty_on_garbage():
assert sr.parse_all_a(b"\x00\x00") == []