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], (
|
||||
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())
|
||||
)
|
||||
|
||||
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
|
||||
key_pem = private_key.private_bytes(
|
||||
@@ -152,7 +155,8 @@ def detect_tailscale() -> dict | None:
|
||||
except json.JSONDecodeError:
|
||||
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 []
|
||||
ips = data.get("TailscaleIPs") or []
|
||||
|
||||
@@ -358,9 +362,19 @@ def get_cert_info(cert_path) -> dict | None:
|
||||
except ExtensionNotFound:
|
||||
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 {
|
||||
"expires": getattr(cert, "not_valid_after_utc", cert.not_valid_after),
|
||||
"not_before": getattr(cert, "not_valid_before_utc", cert.not_valid_before),
|
||||
"expires": expires,
|
||||
"not_before": not_before,
|
||||
"hostnames": hostnames,
|
||||
"serial": cert.serial_number,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user