1+ from datetime import datetime , timedelta , timezone
12from pathlib import Path
23import logging
34import subprocess
45
6+ from cryptography import x509
7+ from cryptography .hazmat .primitives import hashes , serialization
8+ from cryptography .hazmat .primitives .asymmetric import rsa
9+ from cryptography .x509 .oid import NameOID
510import pytest
611from roborock_local_server .certs import CertificateManager
712from roborock_local_server .config import load_config , resolve_paths
813
914
15+ def _write_certificate (
16+ cert_path : Path ,
17+ * ,
18+ common_name : str ,
19+ san_names : list [str ],
20+ ) -> None :
21+ private_key = rsa .generate_private_key (public_exponent = 65537 , key_size = 2048 )
22+ now = datetime .now (timezone .utc )
23+ certificate = (
24+ x509 .CertificateBuilder ()
25+ .subject_name (x509 .Name ([x509 .NameAttribute (NameOID .COMMON_NAME , common_name )]))
26+ .issuer_name (x509 .Name ([x509 .NameAttribute (NameOID .COMMON_NAME , common_name )]))
27+ .public_key (private_key .public_key ())
28+ .serial_number (x509 .random_serial_number ())
29+ .not_valid_before (now - timedelta (days = 1 ))
30+ .not_valid_after (now + timedelta (days = 30 ))
31+ .add_extension (x509 .SubjectAlternativeName ([x509 .DNSName (name ) for name in san_names ]), critical = False )
32+ .sign (private_key = private_key , algorithm = hashes .SHA256 ())
33+ )
34+ cert_path .write_bytes (certificate .public_bytes (encoding = serialization .Encoding .PEM ))
35+
36+
1037def test_certificate_manager_passes_actalis_eab_to_register_account (tmp_path : Path ) -> None :
1138 config_file = tmp_path / "config.toml"
1239 config_file .write_text (
@@ -222,7 +249,11 @@ def test_run_acme_redacts_eab_credentials_in_logs_and_errors(
222249 manager = CertificateManager (config = config , paths = paths )
223250
224251 def fake_run (* args : object , ** kwargs : object ) -> subprocess .CompletedProcess [str ]:
225- return subprocess .CompletedProcess (args = args [0 ], returncode = 1 , stdout = "failed" )
252+ return subprocess .CompletedProcess (
253+ args = args [0 ],
254+ returncode = 1 ,
255+ stdout = "failed with kid-123 hmac-456 and cloudflare-token" ,
256+ )
226257
227258 monkeypatch .setattr ("roborock_local_server.certs.ACME_SH_PATH" , tmp_path / "acme.sh" )
228259 (tmp_path / "acme.sh" ).write_text ("#!/bin/sh\n " , encoding = "utf-8" )
@@ -245,6 +276,55 @@ def fake_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str
245276 combined_logs = "\n " .join (caplog .messages )
246277 assert "kid-123" not in combined_logs
247278 assert "hmac-456" not in combined_logs
279+ assert "cloudflare-token" not in combined_logs
248280 assert "<redacted>" in combined_logs
249281 assert "kid-123" not in str (excinfo .value )
250282 assert "hmac-456" not in str (excinfo .value )
283+
284+
285+ def test_certificate_manager_refreshes_when_cert_domains_do_not_match_config (tmp_path : Path ) -> None :
286+ config_file = tmp_path / "config.toml"
287+ config_file .write_text (
288+ """
289+ [network]
290+ stack_fqdn = "api-roborock.example.com"
291+
292+ [broker]
293+ mode = "embedded"
294+
295+ [storage]
296+ data_dir = "data"
297+
298+ [tls]
299+ mode = "cloudflare_acme"
300+ base_domain = "example.com"
301+ email = "acme@example.com"
302+ cloudflare_token_file = "secrets/cloudflare_token"
303+ acme_server = "actalis"
304+ acme_eab_kid = "kid-123"
305+ acme_eab_hmac_key = "hmac-456"
306+
307+ [admin]
308+ password_hash = "pbkdf2_sha256$600000$abc$def"
309+ session_secret = "abcdefghijklmnopqrstuvwxyz123456"
310+ protocol_login_email = "user@example.com"
311+ protocol_login_pin_hash = "pbkdf2_sha256$600000$ghi$jkl"
312+ """ .strip (),
313+ encoding = "utf-8" ,
314+ )
315+ config = load_config (config_file )
316+ paths = resolve_paths (config_file , config )
317+ paths .cert_file .parent .mkdir (parents = True , exist_ok = True )
318+ paths .key_file .parent .mkdir (parents = True , exist_ok = True )
319+ _write_certificate (paths .cert_file , common_name = "example.com" , san_names = ["example.com" , "*.example.com" ])
320+ paths .key_file .write_text ("key" , encoding = "utf-8" )
321+ manager = CertificateManager (config = config , paths = paths )
322+ called = {"value" : False }
323+
324+ def fake_provision () -> None :
325+ called ["value" ] = True
326+
327+ manager ._provision_or_renew = fake_provision # type: ignore[method-assign]
328+
329+ assert manager .ensure_certificate () is True
330+ assert called ["value" ] is True
0 commit comments