use super::*;
#[test]
fn generated_names_keep_the_configured_alias_and_add_loopback() {
let names = generated_subject_names("hive-mind-router");
assert!(names.contains(&"hive-mind-router".to_string()), "{names:?}");
assert!(names.contains(&"localhost".to_string()), "{names:?}");
assert!(names.contains(&"127.0.0.1".to_string()), "{names:?}");
}
#[test]
fn configured_names_are_taken_verbatim_without_duplicates() {
let names = generated_subject_names("alpha, beta ,localhost");
assert_eq!(
names,
vec![
"alpha".to_string(),
"beta".to_string(),
"localhost".to_string(),
"127.0.0.1".to_string(),
]
);
}
#[test]
fn generation_is_stable_across_calls() {
let data_dir = tempfile::tempdir().expect("data dir");
let names = generated_subject_names("localhost");
let (cert, key) = ensure_generated(data_dir.path(), &names).expect("generate");
let first = std::fs::read_to_string(&cert).expect("read the certificate");
let (again, _) = ensure_generated(data_dir.path(), &names).expect("reuse");
assert_eq!(cert, again);
assert_eq!(
first,
std::fs::read_to_string(&again).expect("read again"),
"a regenerated certificate would break every client that trusted it"
);
assert!(key.is_file());
}
#[cfg(unix)]
#[test]
fn the_generated_key_is_owner_only() {
use std::os::unix::fs::PermissionsExt as _;
let data_dir = tempfile::tempdir().expect("data dir");
let (_, key) =
ensure_generated(data_dir.path(), &generated_subject_names("localhost")).expect("generate");
let mode = std::fs::metadata(&key)
.expect("stat the key")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "key mode was {:o}", mode & 0o777);
}
#[test]
fn the_generated_certificate_can_be_read_back() {
let data_dir = tempfile::tempdir().expect("data dir");
ensure_generated(data_dir.path(), &generated_subject_names("localhost")).expect("generate");
let pem = read_generated_certificate(data_dir.path()).expect("read it back");
assert!(pem.starts_with("-----BEGIN CERTIFICATE-----"), "{pem}");
}
#[test]
fn an_absent_certificate_names_the_remedy() {
let data_dir = tempfile::tempdir().expect("data dir");
let error = read_generated_certificate(data_dir.path()).expect_err("nothing generated yet");
assert!(error.contains("TLS_SELF_SIGNED=1"), "{error}");
}
#[test]
fn half_a_pair_is_refused_rather_than_ignored() {
assert!(TlsSetup::Disabled.is_enabled().eq(&false));
assert!(
TlsSetup::Enabled {
cert: "c.pem".into(),
key: "k.pem".into()
}
.is_enabled()
);
}
#[test]
fn the_cli_generates_and_prints_the_certificate() {
let data_dir = tempfile::tempdir().expect("data dir");
let generated = crate::tls_cli::run_in(
data_dir.path(),
&crate::cli::TlsOp::Generate {
dns: "hive-mind-router".to_string(),
},
);
assert_eq!(generated, std::process::ExitCode::SUCCESS);
let printed = crate::tls_cli::run_in(data_dir.path(), &crate::cli::TlsOp::Ca);
assert_eq!(printed, std::process::ExitCode::SUCCESS);
assert!(
read_generated_certificate(data_dir.path())
.expect("the certificate exists")
.contains("BEGIN CERTIFICATE")
);
}
#[test]
fn the_cli_reports_an_absent_certificate() {
let data_dir = tempfile::tempdir().expect("data dir");
assert_ne!(
crate::tls_cli::run_in(data_dir.path(), &crate::cli::TlsOp::Ca),
std::process::ExitCode::SUCCESS
);
}
#[test]
fn without_configuration_tls_stays_off() {
let data_dir = tempfile::tempdir().expect("data dir");
let setup = resolve(data_dir.path(), None, None, false, None).expect("no error");
assert_eq!(setup, TlsSetup::Disabled);
}
#[test]
fn an_explicit_pair_wins_over_generation() {
let data_dir = tempfile::tempdir().expect("data dir");
let setup = resolve(
data_dir.path(),
Some("/etc/tls/cert.pem".to_string()),
Some("/etc/tls/key.pem".to_string()),
true,
Some("ignored".to_string()),
)
.expect("no error");
assert_eq!(
setup,
TlsSetup::Enabled {
cert: "/etc/tls/cert.pem".into(),
key: "/etc/tls/key.pem".into()
}
);
}
#[test]
fn half_a_configured_pair_is_an_error() {
let data_dir = tempfile::tempdir().expect("data dir");
let missing_key = resolve(data_dir.path(), Some("c.pem".into()), None, false, None)
.expect_err("a certificate without a key is a misconfiguration");
assert!(missing_key.contains("TLS_KEY_FILE"), "{missing_key}");
let missing_cert = resolve(data_dir.path(), None, Some("k.pem".into()), false, None)
.expect_err("a key without a certificate is a misconfiguration");
assert!(missing_cert.contains("TLS_CERT_FILE"), "{missing_cert}");
}
#[test]
fn self_signed_generation_produces_a_servable_pair() {
let data_dir = tempfile::tempdir().expect("data dir");
let setup = resolve(
data_dir.path(),
None,
None,
true,
Some("hive-mind-router".to_string()),
)
.expect("generation succeeds");
let TlsSetup::Enabled { cert, key } = setup else {
panic!("self-signed configuration must enable TLS");
};
assert!(cert.is_file() && key.is_file());
assert!(
read_generated_certificate(data_dir.path())
.expect("readable")
.contains("BEGIN CERTIFICATE")
);
}
#[tokio::test]
async fn the_router_serves_https_from_a_generated_certificate() {
let data_dir = tempfile::tempdir().expect("data dir");
let (cert, key) = ensure_generated(data_dir.path(), &generated_subject_names("localhost"))
.expect("generate a certificate");
let address: std::net::SocketAddr = ([127, 0, 0, 1], 0).into();
let bound = std::net::TcpListener::bind(address).expect("reserve a port");
let port = bound.local_addr().expect("address").port();
drop(bound);
let app = axum::Router::new().route("/health", axum::routing::get(|| async { "ok" }));
let serving = tokio::spawn(serve_https(
([127, 0, 0, 1], port).into(),
app,
cert.clone(),
key,
));
let pem = std::fs::read(&cert).expect("read the certificate");
let client = reqwest::Client::builder()
.add_root_certificate(reqwest::Certificate::from_pem(&pem).expect("parse the certificate"))
.build()
.expect("build a client");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
let mut answered = None;
while std::time::Instant::now() < deadline && answered.is_none() {
if let Ok(response) = client
.get(format!("https://localhost:{port}/health"))
.send()
.await
{
answered = Some(response.status());
} else {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
serving.abort();
assert_eq!(
answered,
Some(reqwest::StatusCode::OK),
"the router must serve HTTPS from its own certificate"
);
}
#[tokio::test]
async fn an_unloadable_certificate_names_the_file() {
let data_dir = tempfile::tempdir().expect("data dir");
let missing = data_dir.path().join("absent.pem");
let error = serve_https(
([127, 0, 0, 1], 0).into(),
axum::Router::new(),
missing.clone(),
missing,
)
.await
.expect_err("an absent certificate cannot serve");
assert!(error.to_string().contains("absent.pem"), "{error}");
}