Refactor ACME module and add cert issuance conflict handling
- Move acme.sh utilities (_run_acme, _find_acme, etc.) from lib/state to lib/acme - Rewrite _parse_list_output to support pipe, tab, and column-separated formats - Add ConflictError (409) to block issuing when cert already exists - Move _find_issuance helper to detect in-progress issuance per domain - Update issue_cert to check existing certs and return issuance status - Fix start_polling to accept event loop explicitly - Add sudoers entry for chown on vacuum-wall.conf - Extend systemd ReadWritePaths for /run/nginx.pid and /var/log/nginx - Update frontend to handle 'existing' issuance status
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Tests for daemon/handlers/acme.py — handler endpoint logic."""
|
||||
|
||||
import asyncio
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -24,8 +25,10 @@ from daemon.handlers.acme import (
|
||||
deactivate_account,
|
||||
generate_self_signed,
|
||||
get_account,
|
||||
issue_cert,
|
||||
register_account,
|
||||
)
|
||||
from daemon.server import ConflictError
|
||||
|
||||
|
||||
class TestGenerateSelfSigned:
|
||||
@@ -1088,3 +1091,64 @@ class TestDeactivateAccount:
|
||||
|
||||
assert not (acme_dir / ".account.conf").is_file()
|
||||
assert not (acme_dir / "account.conf").is_file()
|
||||
|
||||
|
||||
class TestIssueCertExistingCerts:
|
||||
"""Phase 3: issue_cert blocks when cert expires today (days == 0) or tomorrow (days == 1)."""
|
||||
|
||||
def test_days_zero_blocks(self):
|
||||
"""days_until_expiry returns 0 (expires today) — should block."""
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.acme._validate",
|
||||
return_value={"ready": True, "checks": []},
|
||||
),
|
||||
patch(
|
||||
"lib.acme.list_certs",
|
||||
return_value=[{"domain": "example.com", "days_until_expiry": 0}],
|
||||
),
|
||||
pytest.raises(ConflictError, match="0 days remaining"),
|
||||
):
|
||||
asyncio.run(issue_cert(None, {"domain": "example.com"}))
|
||||
|
||||
def test_days_one_blocks(self):
|
||||
"""days_until_expiry returns 1 (expires tomorrow) — should still block."""
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.acme._validate",
|
||||
return_value={"ready": True, "checks": []},
|
||||
),
|
||||
patch(
|
||||
"lib.acme.list_certs",
|
||||
return_value=[{"domain": "example.com", "days_until_expiry": 1}],
|
||||
),
|
||||
pytest.raises(ConflictError, match="1 day remaining"),
|
||||
):
|
||||
asyncio.run(issue_cert(None, {"domain": "example.com"}))
|
||||
|
||||
def test_days_negative_one_allows(self):
|
||||
"""days_until_expiry returns -1 (already expired) — should not block."""
|
||||
|
||||
async def _fake_run_issue(req):
|
||||
pass
|
||||
|
||||
with (
|
||||
patch(
|
||||
"daemon.handlers.acme._validate",
|
||||
return_value={"ready": True, "checks": []},
|
||||
),
|
||||
patch(
|
||||
"lib.acme.list_certs",
|
||||
return_value=[{"domain": "example.com", "days_until_expiry": -1}],
|
||||
),
|
||||
patch.dict("daemon.handlers.acme._ISSUANCES", clear=True),
|
||||
patch(
|
||||
"daemon.handlers.acme._run_issue",
|
||||
new=MagicMock(side_effect=_fake_run_issue),
|
||||
) as mock_run_issue,
|
||||
):
|
||||
result = asyncio.run(issue_cert(None, {"domain": "example.com"}))
|
||||
|
||||
assert result["domain"] == "example.com"
|
||||
assert "request_id" in result
|
||||
mock_run_issue.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user