fix: Tailscale detection reads DNSName from Self + suppress cryptography deprecation
Bug 1: detect_tailscale() looked for DNSName at top level of JSON but tailscale status --self --json nests it inside Self.DNSName. Fixed to read from Self first with top-level fallback. Bug 2: getattr(cert, 'not_valid_after_utc', cert.not_valid_after) eagerly evaluated the fallback, triggering CryptographyDeprecationWarning. Replaced with try/except AttributeError pattern in all three occurrences (generate_self_signed and get_cert_info).
This commit is contained in:
@@ -511,3 +511,67 @@ def test_generate_mkcert_includes_tailscale_sans(monkeypatch, tmp_path):
|
|||||||
assert "spark-1.tail8f3c4e.ts.net" in gen_calls[0], (
|
assert "spark-1.tail8f3c4e.ts.net" in gen_calls[0], (
|
||||||
f"'spark-1.tail8f3c4e.ts.net' must appear in mkcert command args, got: {gen_calls[0]!r}"
|
f"'spark-1.tail8f3c4e.ts.net' must appear in mkcert command args, got: {gen_calls[0]!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 24-25. Bug-fix regression tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_tailscale_reads_dns_name_from_self(monkeypatch):
|
||||||
|
"""detect_tailscale() must read DNSName from Self nested object (actual Tailscale JSON structure)."""
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import muxplex.tls as tls_mod
|
||||||
|
|
||||||
|
class MockResult:
|
||||||
|
returncode = 0
|
||||||
|
stdout = json.dumps(
|
||||||
|
{
|
||||||
|
"BackendState": "Running",
|
||||||
|
"TailscaleIPs": ["100.124.126.19"],
|
||||||
|
"CertDomains": ["spark-1.tail8f3c4e.ts.net"],
|
||||||
|
"Self": {
|
||||||
|
"DNSName": "spark-1.tail8f3c4e.ts.net.",
|
||||||
|
"HostName": "spark-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: MockResult())
|
||||||
|
monkeypatch.setattr(shutil, "which", lambda x: "/usr/bin/tailscale")
|
||||||
|
|
||||||
|
result = tls_mod.detect_tailscale()
|
||||||
|
assert result is not None, (
|
||||||
|
"detect_tailscale() must not return None when DNSName is in Self"
|
||||||
|
)
|
||||||
|
assert result["hostname"] == "spark-1.tail8f3c4e.ts.net", (
|
||||||
|
f"hostname must be 'spark-1.tail8f3c4e.ts.net' (stripped from Self.DNSName), got: {result['hostname']!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_self_signed_no_deprecation_warning(tmp_path):
|
||||||
|
"""generate_self_signed() and get_cert_info() must not emit CryptographyDeprecationWarning."""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from muxplex.tls import generate_self_signed, get_cert_info
|
||||||
|
|
||||||
|
cert_path = tmp_path / "cert.pem"
|
||||||
|
key_path = tmp_path / "key.pem"
|
||||||
|
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
generate_self_signed(cert_path, key_path, hostnames=["localhost"])
|
||||||
|
get_cert_info(cert_path)
|
||||||
|
|
||||||
|
deprecation_warnings = [
|
||||||
|
str(warning.message)
|
||||||
|
for warning in w
|
||||||
|
if issubclass(warning.category, (DeprecationWarning, PendingDeprecationWarning))
|
||||||
|
and "not_valid_after" in str(warning.message).lower()
|
||||||
|
]
|
||||||
|
assert deprecation_warnings == [], (
|
||||||
|
f"CryptographyDeprecationWarning must not be raised, got: {deprecation_warnings}"
|
||||||
|
)
|
||||||
|
|||||||
+18
-4
@@ -91,7 +91,10 @@ def generate_self_signed(
|
|||||||
.sign(private_key, hashes.SHA256())
|
.sign(private_key, hashes.SHA256())
|
||||||
)
|
)
|
||||||
|
|
||||||
expires = getattr(cert, "not_valid_after_utc", cert.not_valid_after)
|
try:
|
||||||
|
expires = cert.not_valid_after_utc # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
expires = cert.not_valid_after # type: ignore[attr-defined]
|
||||||
|
|
||||||
# Write key PEM — create file with restricted permissions before writing
|
# Write key PEM — create file with restricted permissions before writing
|
||||||
key_pem = private_key.private_bytes(
|
key_pem = private_key.private_bytes(
|
||||||
@@ -152,7 +155,8 @@ def detect_tailscale() -> dict | None:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dns_name = data.get("DNSName", "")
|
self_info = data.get("Self") or {}
|
||||||
|
dns_name = self_info.get("DNSName", "") or data.get("DNSName", "")
|
||||||
cert_domains = data.get("CertDomains") or []
|
cert_domains = data.get("CertDomains") or []
|
||||||
ips = data.get("TailscaleIPs") or []
|
ips = data.get("TailscaleIPs") or []
|
||||||
|
|
||||||
@@ -358,9 +362,19 @@ def get_cert_info(cert_path) -> dict | None:
|
|||||||
except ExtensionNotFound:
|
except ExtensionNotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
expires = cert.not_valid_after_utc # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
expires = cert.not_valid_after # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
try:
|
||||||
|
not_before = cert.not_valid_before_utc # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
not_before = cert.not_valid_before # type: ignore[attr-defined]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"expires": getattr(cert, "not_valid_after_utc", cert.not_valid_after),
|
"expires": expires,
|
||||||
"not_before": getattr(cert, "not_valid_before_utc", cert.not_valid_before),
|
"not_before": not_before,
|
||||||
"hostnames": hostnames,
|
"hostnames": hostnames,
|
||||||
"serial": cert.serial_number,
|
"serial": cert.serial_number,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user