certs: async renewal via background task + status polling
- POST /acme/renew returns a request_id and spawns a background task (renew/deploy/refresh steps); dedups per-domain like issue - completes as "skipped" when acme.sh reports the renewal window has not been reached (no --force) - new GET /acme/renew/status endpoint (iface + handler + blueprint) - run acme.sh subprocesses off the event loop (asyncio.to_thread) in both issuance and renewal - ActionCell: busy/busyLabel props; certs page disables the Renew button and polls renewal status with toasts for success/skip/fail
This commit is contained in:
@@ -530,6 +530,64 @@ class TestCertsIssue:
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestCertsRenew:
|
||||
@_ce("post")
|
||||
def test_start_renew(self, mock_post, client):
|
||||
mock_post.return_value = {"request_id": "abc123", "domain": "example.com"}
|
||||
resp = client.post("/api/certs/example.com/renew")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
assert data["data"]["request_id"] == "abc123"
|
||||
args = mock_post.call_args.args
|
||||
assert args[0] == ("POST", "/acme/renew")
|
||||
assert args[1] == {"domain": "example.com"}
|
||||
|
||||
@_ce("post")
|
||||
def test_start_renew_rejected(self, mock_post, client):
|
||||
from daemon.client import BadRequest
|
||||
|
||||
mock_post.side_effect = BadRequest("'domain' is required")
|
||||
resp = client.post("/api/certs/example.com/renew")
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()["ok"] is False
|
||||
|
||||
@_ce("post")
|
||||
def test_start_renew_runtime_error(self, mock_post, client):
|
||||
mock_post.side_effect = RuntimeError("daemon unreachable")
|
||||
resp = client.post("/api/certs/example.com/renew")
|
||||
assert resp.status_code == 500
|
||||
assert resp.get_json()["ok"] is False
|
||||
|
||||
|
||||
class TestCertsRenewStatus:
|
||||
@_ce("get")
|
||||
def test_success(self, mock_get, client):
|
||||
mock_get.return_value = {
|
||||
"request_id": "abc123",
|
||||
"domain": "example.com",
|
||||
"status": "completed",
|
||||
"steps": [],
|
||||
}
|
||||
resp = client.get("/api/certs/renew/abc123")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
assert data["data"]["status"] == "completed"
|
||||
args = mock_get.call_args.args
|
||||
assert args[0] == ("GET", "/acme/renew/status")
|
||||
assert args[1] == {"id": "abc123"}
|
||||
|
||||
@_ce("get")
|
||||
def test_not_found(self, mock_get, client):
|
||||
from daemon.client import NotFound
|
||||
|
||||
mock_get.side_effect = NotFound("renewal request not found")
|
||||
resp = client.get("/api/certs/renew/unknown")
|
||||
assert resp.status_code == 404
|
||||
assert resp.get_json()["ok"] is False
|
||||
|
||||
|
||||
class TestCertsEmail:
|
||||
def test_missing_email(self, client):
|
||||
resp = client.post("/api/certs/email", json={})
|
||||
|
||||
Reference in New Issue
Block a user