mod renewal_regressions { use super::*; use crate::{ auth::Auth, config::{self, Config, ValidationPolicy}, db::Store, }; async fn configured(automatic: bool) -> (Arc, tempfile::TempDir, Certificate, Value) { let directory = tempfile::tempdir().unwrap(); let cert_dir = directory.path().join("certs"); crate::tls::initialize_default(&cert_dir).unwrap(); for name in ["acme-old", "acme-next"] { crate::tls::generate(&cert_dir, name, vec!["example.org".into()]).unwrap(); } let cert = Certificate { id: "site".into(), covered_hosts: Vec::new(), hosts: vec!["example.org".into()], cert_file: "acme-old.pem".into(), key_file: "acme-old.key".into(), issuer: Some(crate::tls::Issuer { provider: "cloudflare".into(), environment: "staging".into(), auto_renew: automatic, }), }; let mut config = Config::default(); config.tls.certificates.push(cert.clone()); config.tls.default_certificate = Some(cert.id.clone()); let policy = ValidationPolicy { proxy_addr: "127.0.0.1:8080".parse().unwrap(), admin_addr: "127.0.0.1:9090".parse().unwrap(), tls_addr: None, cert_dir: Some(cert_dir), data_dir: None, }; let store = Store::open(&directory.path().join("db"), &config) .await .unwrap(); let auth = Auth::new(store.clone(), directory.path().to_owned()) .await .unwrap(); let snapshot = config::compile(config, 0, &policy).unwrap(); let app = App::new( store, snapshot, policy, auth, 8, Duration::from_secs(2), Duration::from_secs(2), Duration::from_secs(30), ); let output = serde_json::json!({"provider":"cloudflare", "environment":"staging", "certificate_id":"site", "domains":["example.org"], "cert_file":"acme-next.pem", "key_file":"acme-next.key", "changed":true}); (app, directory, cert, output) } async fn stop(app: Arc) { app.shutdown.cancel(); app.tunnels.close(); app.tunnels.wait().await; app.store.close().await; } #[tokio::test] async fn renewed_pair_publishes_one_revision_and_preserves_bindings_and_old_files() { let (app, _directory, original, output) = configured(true).await; deploy_renewal(&app, &original, &output, true) .await .unwrap(); let snapshot = app.config.load_full(); assert_eq!(snapshot.envelope.revision, 1); let applied = &snapshot.envelope.config.tls.certificates[0]; assert_eq!(applied.cert_file, "acme-next.pem"); assert_eq!(applied.hosts, original.hosts); assert!(applied.issuer.as_ref().unwrap().auto_renew); assert_eq!( app.store.load().await.unwrap().config.tls.certificates[0].cert_file, "acme-next.pem" ); assert!(app .policy .cert_dir .as_ref() .unwrap() .join("acme-old.key") .exists()); stop(app).await; } #[tokio::test] async fn invalid_renewed_domain_never_changes_active_configuration() { let (app, _directory, original, mut output) = configured(true).await; output["domains"] = serde_json::json!(["unrelated.example.org"]); assert!(deploy_renewal(&app, &original, &output, true) .await .is_err()); assert_eq!(app.config.load().envelope.revision, 0); assert_eq!( app.store.load().await.unwrap().config.tls.certificates[0].cert_file, original.cert_file ); stop(app).await; } #[tokio::test] async fn operator_opt_out_wins_over_an_older_automatic_job() { let (app, _directory, mut original, output) = configured(false).await; original.issuer.as_mut().unwrap().auto_renew = true; assert_eq!( deploy_renewal(&app, &original, &output, true) .await .unwrap_err(), "acme.configuration_changed" ); assert_eq!(app.config.load().envelope.revision, 0); deploy_renewal(&app, &original, &output, false) .await .unwrap(); assert!( !app.config.load().envelope.config.tls.certificates[0] .issuer .as_ref() .unwrap() .auto_renew ); stop(app).await; } #[tokio::test] async fn saved_renewal_observations_are_persisted_in_sqlite_without_provider_credentials() { let (app, _directory, _original, _output) = configured(true).await; app.acme.renewals.lock().unwrap().insert( "site".into(), RenewalState { last_checked: 100, next_check: 43200, status: "complete".into(), message: "acme.not_due".into(), ..Default::default() }, ); app.acme.persist(&app.store).await; let saved = app.store.load_certificate_renewals().await.unwrap(); let state = saved.get("site").unwrap(); assert_eq!(state.next_check, 43200); assert_eq!(state.status, "complete"); assert_eq!(state.message, "acme.not_due"); stop(app).await; } } mod runtime_metadata_tests { use super::*; fn write_package(site: &Path, package: &str, version: &str, metadata: &str) { let folder = site.join(format!("{package}-{version}.dist-info")); std::fs::create_dir_all(&folder).unwrap(); std::fs::write(folder.join("METADATA"), metadata).unwrap(); } #[test] fn managed_python_pip_and_certbot_versions_use_metadata_only() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); let python = venv_python(root); let certbot = venv_certbot(root); std::fs::create_dir_all(venv_bin(root)).unwrap(); std::fs::write(&python, "").unwrap(); std::fs::write(&certbot, "").unwrap(); std::fs::write( root.join("pyvenv.cfg"), "home = /usr/bin\nversion = 3.13.5\n", ) .unwrap(); let site = if cfg!(windows) { root.join("Lib/site-packages") } else { root.join("lib/python3.13/site-packages") }; write_package( &site, "pip", "25.2", "Metadata-Version: 2.3\nName: pip\nVersion: 25.2\n", ); write_package(&site, "certbot", "5.8.0", "Name: certbot\nVersion: 5.8.0\n"); let versions = runtime_versions(&python, true, Some(&certbot)); assert_eq!(versions.python.as_deref(), Some("3.13.5")); assert_eq!(versions.pip.as_deref(), Some("25.2")); assert_eq!(versions.certbot.as_deref(), Some("5.8.0")); } #[test] fn absent_or_unverified_versions_are_not_invented() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); let executable = root.join("bin/python3"); std::fs::create_dir_all(root.join("bin")).unwrap(); std::fs::write(&executable, "").unwrap(); let site = root.join("lib/python3/dist-packages"); write_package(&site, "pip", "99.0", "Name: other-package\nVersion: 99.0\n"); let versions = runtime_versions(&executable, true, None); assert!(versions.python.is_none()); assert!(versions.pip.is_none()); assert!(versions.certbot.is_none()); } #[test] fn conflicting_site_distributions_do_not_return_stale_versions() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); let executable = root.join("bin/python3"); std::fs::create_dir_all(root.join("bin")).unwrap(); std::fs::write(&executable, "").unwrap(); write_package( &root.join("lib/python3/dist-packages"), "pip", "23.0", "Name: pip\nVersion: 23.0\n", ); write_package( &root.join("lib/python3.13/site-packages"), "pip", "25.0", "Name: pip\nVersion: 25.0\n", ); assert!(runtime_versions(&executable, true, None).pip.is_none()); } }