fix: ECC cert support, ACME deploy hook path, NAT detection, and account config fallback

- Add find_cert_dir() to resolve both RSA and ECC (domain_ecc/) cert dirs
- Copy acme deploy hook to /deploy/ where acme.sh resolves it
- _parse_account_conf checks both legacy .account.conf and declarative config
- Skip public DNS check when all local IPs are private (NAT)
- Improve check message strings for validity and expiry status
- Support timezone-aware date formats in _days_until parsing
- Filter out "no" SAN domains in cert listing
- Bump frontend asset version cache keys
- Fix DOMContentLoaded race condition in app.js boot
- Fix spread operator in certs.js modal template
This commit is contained in:
2026-06-27 14:23:40 +00:00
parent 398831b6e2
commit 8feb56faf6
12 changed files with 250 additions and 105 deletions
+45
View File
@@ -157,6 +157,39 @@ class TestGetEmail:
assert result == "test@example.com"
class TestFindCertDir:
def test_rsa_dir(self, tmp_path):
rsa_dir = tmp_path / "example.com"
rsa_dir.mkdir()
result = acme.find_cert_dir("example.com", tmp_path)
assert result == rsa_dir
def test_ecc_dir(self, tmp_path):
ecc_dir = tmp_path / "example.com_ecc"
ecc_dir.mkdir()
result = acme.find_cert_dir("example.com", tmp_path)
assert result == ecc_dir
def test_eccPreferred(self, tmp_path):
rsa_dir = tmp_path / "example.com"
rsa_dir.mkdir()
ecc_dir = tmp_path / "example.com_ecc"
ecc_dir.mkdir()
result = acme.find_cert_dir("example.com", tmp_path)
assert result == ecc_dir
def test_fallback_when_neither(self, tmp_path):
result = acme.find_cert_dir("example.com", tmp_path)
assert result == tmp_path / "example.com"
def test_resolves_ecc_only(self, tmp_path):
"""Only _ecc dir exists, no RSA dir — should resolve to _ecc."""
ecc_dir = tmp_path / "example.com_ecc"
ecc_dir.mkdir()
result = acme.find_cert_dir("example.com", tmp_path)
assert result == ecc_dir
class TestGetCertPaths:
def test_returns_paths(self, tmp_path):
with patch.object(acme, "_ACME_HOME", tmp_path / "data" / "acme"):
@@ -166,6 +199,18 @@ class TestGetCertPaths:
assert paths["ca"].endswith("example.com/ca.cer")
assert paths["fullchain"].endswith("example.com/fullchain.cer")
def test_resolves_ecc_dir(self, tmp_path):
acme_dir = tmp_path / "data" / "acme"
ecc_dir = acme_dir / "example.com_ecc"
ecc_dir.mkdir(parents=True)
with patch.object(acme, "_ACME_HOME", acme_dir):
paths = acme.get_cert_paths("example.com")
assert paths["cert"].endswith("example.com_ecc/example.com.cert")
assert paths["key"].endswith("example.com_ecc/example.com.key")
assert paths["ca"].endswith("example.com_ecc/ca.cer")
assert paths["fullchain"].endswith("example.com_ecc/fullchain.cer")
class TestDeployHook:
@patch("lib.acme._run_acme")
+14 -3
View File
@@ -348,10 +348,10 @@ class TestCheckDnsPublic:
from unittest.mock import patch
mock_result = MagicMock(
returncode=0, stdout="example.com has address 192.168.1.1"
returncode=0, stdout="example.com has address 52.14.150.110"
)
with (
patch("socket.gethostbyname", return_value="192.168.1.1"),
patch("daemon.handlers.acme._get_local_ips", return_value={"52.14.150.110"}),
patch("subprocess.run", return_value=mock_result),
):
passed, _ = _check_dns_public("example.com")
@@ -362,12 +362,23 @@ class TestCheckDnsPublic:
mock_result = MagicMock(returncode=1, stdout="NXDOMAIN")
with (
patch("socket.gethostbyname", return_value="192.168.1.1"),
patch("daemon.handlers.acme._get_local_ips", return_value={"8.8.8.8"}),
patch("subprocess.run", return_value=mock_result),
):
passed, _ = _check_dns_public("example.com")
assert passed is False
def test_nat_detected_skips_check(self):
from unittest.mock import patch
with patch(
"daemon.handlers.acme._get_local_ips",
return_value={"192.168.1.1"},
):
passed, msg = _check_dns_public("example.com")
assert passed is True
assert "NAT" in msg
class TestCheckDomainFormat:
def test_valid(self):