227 lines
8.4 KiB
Rust
227 lines
8.4 KiB
Rust
mod renewal_regressions {
|
|
use super::*;
|
|
use crate::{
|
|
auth::Auth,
|
|
config::{self, Config, ValidationPolicy},
|
|
db::Store,
|
|
};
|
|
async fn configured(automatic: bool) -> (Arc<App>, 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>) {
|
|
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());
|
|
}
|
|
}
|