diff --git a/src/roborock_local_server/certs.py b/src/roborock_local_server/certs.py index 964076c..e22ab5c 100644 --- a/src/roborock_local_server/certs.py +++ b/src/roborock_local_server/certs.py @@ -17,6 +17,7 @@ LOG = logging.getLogger("roborock_local_server.certs") ACME_SH_PATH = Path("/opt/acme.sh/acme.sh") +ACME_RENEW_SKIP_RETURN_CODE = 2 @dataclass(frozen=True) @@ -155,6 +156,9 @@ def _run_acme(self, args: Iterable[str]) -> None: ) if result.stdout.strip(): LOG.info("ACME output:\n%s", self._redact_text(result.stdout.strip(), sensitive_values)) + if result.returncode == ACME_RENEW_SKIP_RETURN_CODE: + LOG.info("ACME command skipped because certificate renewal is not due: %s", display_command) + return if result.returncode != 0: raise RuntimeError(f"ACME command failed ({result.returncode}): {display_command}") diff --git a/tests/test_certs.py b/tests/test_certs.py index 127d599..e1a3131 100644 --- a/tests/test_certs.py +++ b/tests/test_certs.py @@ -282,6 +282,59 @@ def fake_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str assert "hmac-456" not in str(excinfo.value) +def test_run_acme_treats_renew_skip_as_success( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + config_file = tmp_path / "config.toml" + config_file.write_text( + """ +[network] +stack_fqdn = "api-roborock.example.com" + +[broker] +mode = "embedded" + +[storage] +data_dir = "data" + +[tls] +mode = "cloudflare_acme" +base_domain = "example.com" +email = "acme@example.com" +cloudflare_token_file = "secrets/cloudflare_token" +acme_server = "zerossl" + +[admin] +password_hash = "pbkdf2_sha256$600000$abc$def" +session_secret = "abcdefghijklmnopqrstuvwxyz123456" +protocol_login_email = "user@example.com" +protocol_login_pin_hash = "pbkdf2_sha256$600000$ghi$jkl" + """.strip(), + encoding="utf-8", + ) + config = load_config(config_file) + paths = resolve_paths(config_file, config) + paths.cloudflare_token_file.parent.mkdir(parents=True, exist_ok=True) + paths.cloudflare_token_file.write_text("cloudflare-token", encoding="utf-8") + manager = CertificateManager(config=config, paths=paths) + + def fake_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess( + args=args[0], + returncode=2, + stdout="Skipping. Next renewal time is: 2026-08-13T08:35:28Z", + ) + + monkeypatch.setattr("roborock_local_server.certs.ACME_SH_PATH", tmp_path / "acme.sh") + (tmp_path / "acme.sh").write_text("#!/bin/sh\n", encoding="utf-8") + monkeypatch.setattr("roborock_local_server.certs.subprocess.run", fake_run) + + with caplog.at_level(logging.INFO, logger="roborock_local_server.certs"): + manager._run_acme(["--issue", "-d", "example.com"]) + + assert "ACME command skipped because certificate renewal is not due" in caplog.text + + def test_certificate_manager_refreshes_when_cert_domains_do_not_match_config(tmp_path: Path) -> None: config_file = tmp_path / "config.toml" config_file.write_text(