use std::error::Error;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
type TestResult = Result<(), Box<dyn Error>>;
fn command() -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_liminal"));
for (name, _) in std::env::vars_os() {
if name.to_string_lossy().starts_with("LIMINAL_") {
command.env_remove(name);
}
}
command
}
fn check(path: &Path) -> Result<Output, std::io::Error> {
command().arg("check").arg("--config").arg(path).output()
}
fn toml_string(path: &Path) -> String {
let escaped = path
.display()
.to_string()
.replace('\\', "\\\\")
.replace('"', "\\\"");
format!("\"{escaped}\"")
}
fn fixture(root: &Path) -> Result<PathBuf, Box<dyn Error>> {
let store = root.join("store");
std::fs::create_dir(&store)?;
let config = root.join("liminal.toml");
std::fs::write(
&config,
format!(
r#"listen_address = "127.0.0.1:7600"
health_listen_address = "127.0.0.1:7601"
drain_timeout_ms = 0
channels = []
routing_rules = []
persistence_path = {store}
[auth]
token = "test-token"
[auth.lys]
issuer = "https://unreachable.invalid"
network_id = "network-a"
keys_path = "absent-issuer.json"
live = true
[auth.lys.machine]
identity_path = "absent-identity.key"
pass_path = "absent-machine.pass"
"#,
store = toml_string(&store),
),
)?;
Ok(config)
}
fn snapshot(root: &Path) -> Result<Vec<(PathBuf, Vec<u8>)>, std::io::Error> {
let mut entries = vec![(root.to_path_buf(), Vec::new())];
for entry in std::fs::read_dir(root)? {
let path = entry?.path();
if path.is_dir() {
entries.extend(snapshot(&path)?);
} else {
entries.push((path.clone(), std::fs::read(path)?));
}
}
entries.sort();
Ok(entries)
}
fn assert_valid(output: &Output, path: &Path) {
assert_eq!(
output.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
format!("{}: configuration valid\n", path.display())
);
assert!(output.stderr.is_empty());
}
#[test]
fn check_accepts_valid_config_without_serving_or_writing() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let before = snapshot(root.path())?;
let output = check(&path)?;
assert_valid(&output, &path);
assert_eq!(snapshot(root.path())?, before);
Ok(())
}
#[test]
fn check_reports_each_startup_refusal_on_its_own_line() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let text = std::fs::read_to_string(&path)?
.replace("127.0.0.1:7600", "127.0.0.1:0")
.replace("127.0.0.1:7601", "127.0.0.1:0");
let text = text
.lines()
.filter(|line| !line.starts_with("persistence_path"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&path, text)?;
let output = check(&path)?;
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let stderr = String::from_utf8(output.stderr)?;
let expected = concat!(
"configuration validation failed: listen_address: port must be non-zero\n",
"health_listen_address: port must be non-zero\n",
"health_listen_address: must differ from listen_address for probe isolation\n",
"persistence_path: full services require an existing directory; set persistence_path or LIMINAL_PERSISTENCE_PATH; no store is opened\n",
);
assert_eq!(stderr, expected);
let startup = command().arg("--config").arg(&path).output()?;
assert_eq!(startup.status.code(), Some(1));
assert_eq!(
String::from_utf8(startup.stderr)?,
format!("Error: {expected}")
);
Ok(())
}
#[test]
fn check_applies_environment_overrides_before_validation() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let output = command()
.arg("check")
.arg("--config")
.arg(&path)
.env("LIMINAL_LISTEN_ADDRESS", "127.0.0.1:0")
.output()?;
assert_eq!(output.status.code(), Some(2));
assert_eq!(
String::from_utf8(output.stderr)?,
"configuration validation failed: listen_address: port must be non-zero\n"
);
assert!(output.stdout.is_empty());
Ok(())
}
#[test]
fn check_resolves_schemas_relative_to_the_configuration() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let text = std::fs::read_to_string(&path)?
.replace("channels = []\n", "")
.replace(
"[auth]",
"[[channels]]\nname = 'orders'\ndurable = true\nschema_ref = 'orders.json'\n[auth]",
);
std::fs::write(&path, text)?;
std::fs::write(root.path().join("orders.json"), r#"{"type":"object"}"#)?;
let elsewhere = tempfile::tempdir()?;
let output = command()
.arg("check")
.arg("--config")
.arg(&path)
.current_dir(elsewhere.path())
.output()?;
assert_valid(&output, &path);
std::fs::write(root.path().join("orders.json"), "not JSON")?;
let output = check(&path)?;
assert_eq!(output.status.code(), Some(2));
assert!(String::from_utf8(output.stderr)?.contains("is not valid JSON"));
Ok(())
}
#[test]
fn check_reports_missing_and_unreadable_files_with_the_os_error() -> TestResult {
let root = tempfile::tempdir()?;
for path in [root.path().join("missing.toml"), root.path().to_path_buf()] {
let error = std::fs::read_to_string(&path)
.err()
.ok_or("file was readable")?;
let output = check(&path)?;
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
assert_eq!(
String::from_utf8(output.stderr)?,
format!(
"configuration load failed: failed to read configuration file '{}': {error}\n",
path.display()
)
);
}
Ok(())
}
#[test]
fn check_reports_malformed_toml_and_unknown_fields() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let valid = std::fs::read_to_string(&path)?;
for text in [
"[broken".to_owned(),
format!("unknown_field = true\n{valid}"),
] {
std::fs::write(&path, text)?;
let error = liminal_server::config::load_from_file(&path)
.err()
.ok_or("malformed configuration loaded")?;
let output = check(&path)?;
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let stderr = String::from_utf8(output.stderr)?;
assert_eq!(stderr, format!("{error}\n"));
}
Ok(())
}
#[test]
fn check_requires_a_configuration_path() -> TestResult {
let output = command().arg("check").output()?;
assert_eq!(output.status.code(), Some(2));
let stderr = String::from_utf8(output.stderr)?;
assert!(stderr.contains("--config <CONFIG> is required"), "{stderr}");
assert!(
stderr.contains("liminal check --config <CONFIG>"),
"{stderr}"
);
let output = check(Path::new(""))?;
assert_eq!(output.status.code(), Some(2));
assert_eq!(
String::from_utf8(output.stderr)?,
"configuration load failed: configuration path is empty\n"
);
Ok(())
}
#[test]
fn check_is_documented_in_both_help_pages() -> TestResult {
for arguments in [vec!["--help"], vec!["check", "--help"]] {
let output = command().args(arguments).output()?;
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8(output.stdout)?;
assert!(
stdout.contains("Validate the server configuration without serving"),
"{stdout}"
);
assert!(stdout.contains("--config <CONFIG>"), "{stdout}");
}
Ok(())
}
#[test]
fn check_validates_extra_listen_addresses_without_serving_or_writing() -> TestResult {
let root = tempfile::tempdir()?;
let path = fixture(root.path())?;
let original = std::fs::read_to_string(&path)?;
std::fs::write(
&path,
format!("extra_listen_addresses = ['100.64.0.1:7600']\n{original}"),
)?;
let before = snapshot(root.path())?;
assert_valid(&check(&path)?, &path);
assert_eq!(snapshot(root.path())?, before);
std::fs::write(
&path,
format!("extra_listen_addresses = ['0.0.0.0:7600']\n{original}"),
)?;
let before = snapshot(root.path())?;
let output = check(&path)?;
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let error = String::from_utf8(output.stderr)?;
assert!(
error.contains("extra_listen_addresses[0] (0.0.0.0:7600)"),
"{error}"
);
assert!(
error.contains("unspecified addresses are refused"),
"{error}"
);
assert_eq!(snapshot(root.path())?, before);
Ok(())
}