netsleuth/tests/test_models.py
google-labs-jules[bot] ddfbb1adbf Add tests for _os_timezone function
This commit adds a new test file, `tests/test_cli.py`, containing
unit tests for the `_os_timezone` function in `netsleuth.cli`.

The tests cover:
- Happy paths (TZ variable, /etc/localtime symlink, /etc/timezone file)
- The edge case of an `OSError` exception being thrown when reading the
  /etc/timezone file.

Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
2026-08-19 11:32:02 +00:00

375 lines
11 KiB
Python

from __future__ import annotations
import json
import pytest
from netsleuth.models import (
AdapterLeakResult,
AnycastHop,
BgpEvent,
BgpIntel,
Capabilities,
CfL4Stats,
CfTrace,
DnsAdvanced,
DnsblHit,
DnsLeak,
DpiCheckResult,
Finding,
InternetDbResult,
IpGeo,
IxpPresence,
LocalNet,
ModuleResult,
PathDiversity,
PingResult,
PortProbe,
PrefixBenchmark,
PrefixProbe,
ProbeError,
Reputation,
ResolverProbe,
Signal,
SpeedResult,
TierAttempt,
TlsResult,
TraceHop,
TraceResult,
VpnAssessment,
to_jsonable,
)
def test_probe_error_rejects_unknown_kind():
with pytest.raises(ValueError, match="unknown ProbeError kind"):
ProbeError(source="ip-api", kind="exploded", message="boom")
@pytest.mark.parametrize(
"kind",
[
"timeout",
"http_error",
"rate_limited",
"blocked",
"parse_error",
"unavailable",
"no_privilege",
"not_applicable",
],
)
def test_probe_error_accepts_documented_kinds(kind):
assert ProbeError(source="s", kind=kind, message="m").kind == kind
def test_module_result_rejects_unknown_status():
with pytest.raises(ValueError, match="unknown ModuleResult status"):
ModuleResult(name="bgp", status="borked")
def test_module_result_serializes_to_strict_json():
result = ModuleResult(
name="reputation",
status="partial",
data={"firehol_hits": ["firehol_level1"]},
errors=[ProbeError(source="internetdb", kind="timeout", message="8s", retryable=True)],
warnings=["abuseipdb key missing"],
started_at="2026-08-08T19:12:00Z",
duration_ms=1234,
)
text = json.dumps(to_jsonable(result), allow_nan=False)
back = json.loads(text)
assert back["name"] == "reputation"
assert back["status"] == "partial"
assert back["data"]["firehol_hits"] == ["firehol_level1"]
assert back["errors"][0] == {
"source": "internetdb",
"kind": "timeout",
"message": "8s",
"retryable": True,
}
assert back["warnings"] == ["abuseipdb key missing"]
assert back["duration_ms"] == 1234
def test_to_jsonable_coerces_non_finite_numbers_to_null():
payload = {"a": float("inf"), "b": float("-inf"), "c": float("nan"), "d": 1.5}
out = to_jsonable(payload)
assert out == {"a": None, "b": None, "c": None, "d": 1.5}
json.dumps(out, allow_nan=False)
def test_to_jsonable_handles_nested_dataclasses_and_sets():
finding = Finding(
id="latency.high",
severity="warn",
title="Latency above target",
detail="avg 130 ms to 1.1.1.1",
metric="avg_ms",
value=130.0,
threshold=100.0,
advice="Check for a saturated uplink.",
)
signal = Signal(name="tunnel_iface", observed=True, weight=0.35, direction="vpn", note="wg0")
out = to_jsonable({"findings": [finding], "signals": (signal,), "tags": {"a"}})
assert out["findings"][0]["severity"] == "warn"
assert out["signals"][0]["weight"] == 0.35
assert out["tags"] == ["a"]
json.dumps(out, allow_nan=False)
def test_finding_rejects_unknown_severity():
with pytest.raises(ValueError, match="unknown Finding severity"):
Finding(
id="x",
severity="apocalyptic",
title="t",
detail="d",
metric=None,
value=None,
threshold=None,
advice=None,
)
def test_every_domain_shape_defaults_to_constructible_with_no_arguments():
for cls in (
Capabilities,
LocalNet,
IpGeo,
CfTrace,
VpnAssessment,
DnsLeak,
BgpIntel,
Reputation,
SpeedResult,
TraceResult,
):
instance = cls()
json.dumps(to_jsonable(instance), allow_nan=False)
def test_ping_result_round_trips_through_json():
ping = PingResult(
label="cloudflare-dns",
host="1.1.1.1",
resolved_ip="1.1.1.1",
method="icmp_win",
sent=20,
received=20,
loss_pct=0.0,
min_ms=11.0,
avg_ms=12.4,
max_ms=15.1,
mdev_ms=0.9,
jitter_ms=1.9,
samples=[11.0, 12.0, None, 15.1],
)
out = json.loads(json.dumps(to_jsonable(ping), allow_nan=False))
assert out["method"] == "icmp_win"
assert out["samples"][2] is None
def test_trace_result_nests_hops():
trace = TraceResult(
target="1.1.1.1",
resolved_ip="1.1.1.1",
backend="icmp_win",
hops=[TraceHop(ttl=1, ip="192.168.1.1", probes=[1.2, 1.1, None], annotations=["!H"])],
cycles=1,
completed=True,
)
out = to_jsonable(trace)
assert out["hops"][0]["ttl"] == 1
assert out["hops"][0]["annotations"] == ["!H"]
assert out["backend"] == "icmp_win"
def test_reputation_composes_optional_sub_results():
rep = Reputation(
internetdb=InternetDbResult(ip="203.0.113.44", ports=[80, 443], tags=["cdn"]),
firehol_hits=["firehol_level1"],
dnsbl_hits=[DnsblHit(zone="zen.spamhaus.org", codes=["127.0.0.2"], meaning="listed")],
dnsbl_query_blocked=False,
captcha_risk="medium",
rationale="Listed on one blocklist.",
)
out = to_jsonable(rep)
assert out["internetdb"]["ports"] == [80, 443]
assert out["dnsbl_hits"][0]["zone"] == "zen.spamhaus.org"
def test_bgp_intel_nests_events_and_ixps():
bgp = BgpIntel(
asn="AS64500",
holder="Example Telecom",
flaps=[BgpEvent(timestamp="2026-08-01T00:00:00Z", type="A", prefix="203.0.113.0/24")],
ixps=[IxpPresence(name="AMS-IX", city="Amsterdam", country="NL", speed_mbps=100000)],
stability="stable",
)
out = to_jsonable(bgp)
assert out["flaps"][0]["prefix"] == "203.0.113.0/24"
assert out["ixps"][0]["speed_mbps"] == 100000
def test_speed_result_carries_tier_attempts_and_cfl4():
speed = SpeedResult(
method="cloudflare",
tier_attempts=[
TierAttempt(tier="ookla_bin", ok=False, reason="binary not on PATH"),
TierAttempt(tier="cloudflare", ok=True, reason=None),
],
download_mbps=284.3,
upload_mbps=41.7,
cfL4_stats=CfL4Stats(rtt_ms=12.0, min_rtt_ms=11.0, rtt_var_ms=1.5, delivery_rate_bps=35000000, cwnd=42, unsent_bytes=0, recv_bytes=1048576),
)
out = to_jsonable(speed)
assert out["tier_attempts"][0]["ok"] is False
assert out["cfL4_stats"]["rtt_ms"] == 12.0
def test_adapter_leak_result_and_dns_leak_compose():
leak = DnsLeak(
per_adapter=[
AdapterLeakResult(
adapter="Wi-Fi",
configured_resolvers=["192.168.1.1"],
echoed_ip="203.0.113.9",
echoed_asn="AS64501",
matches_egress_asn=False,
)
],
ecs_leaked=True,
note="ISP resolver still active on Wi-Fi adapter.",
)
out = to_jsonable(leak)
assert out["per_adapter"][0]["matches_egress_asn"] is False
assert out["ecs_leaked"] is True
def test_ip_geo_and_vpn_assessment_defaults_are_json_safe():
geo = IpGeo(ip="203.0.113.44", ip_version=4, asn="AS64500", sources={"asn": "ip-api"})
vpn = VpnAssessment(verdict="likely", confidence=0.55, signals=[Signal("warp", True, 0.5, "vpn")])
out = to_jsonable({"geo": geo, "vpn": vpn})
assert out["geo"]["ip_type"] == "unknown"
assert out["vpn"]["signals"][0]["name"] == "warp"
def test_every_l7_domain_shape_defaults_to_constructible_with_no_arguments():
for cls in (TlsResult, PrefixBenchmark, DpiCheckResult, DnsAdvanced, PathDiversity):
instance = cls()
json.dumps(to_jsonable(instance), allow_nan=False)
def test_tls_result_round_trips_through_json():
tls = TlsResult(
label="cloudflare",
host="cloudflare.com",
port=443,
resolved_ip="104.16.132.229",
tcp_rtt_ms=12.0,
tls_handshake_ms=45.0,
ttfb_ms=60.0,
tls_version="TLSv1.3",
cipher="TLS_AES_256_GCM_SHA384",
alpn="h2",
cert_verified=True,
)
out = json.loads(json.dumps(to_jsonable(tls), allow_nan=False))
assert out["host"] == "cloudflare.com"
assert out["tls_handshake_ms"] == 45.0
def test_port_probe_rejects_unknown_state():
with pytest.raises(ValueError, match="unknown PortProbe state"):
PortProbe(port=443, state="bogus")
@pytest.mark.parametrize("state", ["open", "closed", "filtered", "reset", "error"])
def test_port_probe_accepts_documented_states(state):
assert PortProbe(port=443, state=state).state == state
def test_dpi_check_result_rejects_unknown_verdict():
with pytest.raises(ValueError, match="unknown DpiCheckResult verdict"):
DpiCheckResult(verdict="bogus")
@pytest.mark.parametrize(
"verdict", ["clean", "partial_filtering", "reset_injection", "unreachable", "unknown"]
)
def test_dpi_check_result_accepts_documented_verdicts(verdict):
assert DpiCheckResult(verdict=verdict).verdict == verdict
def test_dpi_check_result_nests_port_probes():
result = DpiCheckResult(
target="203.0.113.9",
resolved_ip="203.0.113.9",
consented=True,
ports=[PortProbe(port=443, state="open", rtt_ms=20.0)],
verdict="clean",
rationale="all probed ports behave normally",
)
out = to_jsonable(result)
assert out["ports"][0]["state"] == "open"
assert out["consented"] is True
def test_resolver_probe_rejects_unknown_kind():
with pytest.raises(ValueError, match="unknown ResolverProbe kind"):
ResolverProbe(name="system", kind="bogus", query_name="example.com")
@pytest.mark.parametrize("kind", ["system", "doh"])
def test_resolver_probe_accepts_documented_kinds(kind):
assert ResolverProbe(name="n", kind=kind, query_name="example.com").kind == kind
def test_dns_advanced_nests_resolver_probes():
adv = DnsAdvanced(
probes=[ResolverProbe(name="system", kind="system", query_name="example.com", answers=["1.2.3.4"])],
system_avg_ms=12.0,
doh_avg_ms=30.0,
transparent_proxy=False,
note="system resolver answered directly",
)
out = to_jsonable(adv)
assert out["probes"][0]["answers"] == ["1.2.3.4"]
assert out["transparent_proxy"] is False
def test_anycast_hop_rejects_unknown_source():
with pytest.raises(ValueError, match="unknown AnycastHop source"):
AnycastHop(target="example.com", source="bogus")
@pytest.mark.parametrize("source", ["cf_ray", "cf_trace", "server_timing", "none"])
def test_anycast_hop_accepts_documented_sources(source):
assert AnycastHop(target="example.com", source=source).source == source
def test_path_diversity_nests_anycast_hops():
pd = PathDiversity(
client_country="RU",
hops=[AnycastHop(target="cloudflare.com", edge_colo="DME", source="cf_ray")],
international_loop=False,
)
out = to_jsonable(pd)
assert out["hops"][0]["edge_colo"] == "DME"
assert out["international_loop"] is False
def test_prefix_benchmark_nests_prefix_probes_and_ranks():
bench = PrefixBenchmark(
asn="AS64500",
prefixes_announced=120,
prefixes_probed=32,
method="icmp",
results=[PrefixProbe(prefix="203.0.113.0/24", probe_ip="203.0.113.1", avg_ms=15.0, reachable=True)],
best="203.0.113.0/24",
)
out = to_jsonable(bench)
assert out["results"][0]["avg_ms"] == 15.0
assert out["best"] == "203.0.113.0/24"