[fix](trx-server): validate config semantics at startup

Add semantic validate() checks for server/client config models and fail fast on invalid ranges, field combinations, and auth token values before runtime startup.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 22:05:54 +01:00
parent 3cc36d9c24
commit 144afbae8e
4 changed files with 239 additions and 0 deletions
+70
View File
@@ -188,6 +188,40 @@ pub struct HttpJsonAuthConfig {
}
impl ClientConfig {
pub fn validate(&self) -> Result<(), String> {
validate_log_level(self.general.log_level.as_deref())?;
if self.remote.poll_interval_ms == 0 {
return Err("[remote].poll_interval_ms must be > 0".to_string());
}
if let Some(url) = &self.remote.url {
if url.trim().is_empty() {
return Err("[remote].url must not be empty when set".to_string());
}
}
if let Some(token) = &self.remote.auth.token {
if token.trim().is_empty() {
return Err("[remote.auth].token must not be empty when set".to_string());
}
}
if self.frontends.http.enabled && self.frontends.http.port == 0 {
return Err("[frontends.http].port must be > 0 when enabled".to_string());
}
if self.frontends.rigctl.enabled && self.frontends.rigctl.port == 0 {
return Err("[frontends.rigctl].port must be > 0 when enabled".to_string());
}
if self.frontends.audio.enabled && self.frontends.audio.server_port == 0 {
return Err("[frontends.audio].server_port must be > 0 when enabled".to_string());
}
validate_tokens(
"[frontends.http_json.auth].tokens",
&self.frontends.http_json.auth.tokens,
)?;
Ok(())
}
/// Load configuration from a specific file path.
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
<Self as ConfigFile>::load_from_file(path)
@@ -233,6 +267,28 @@ impl ClientConfig {
}
}
fn validate_log_level(level: Option<&str>) -> Result<(), String> {
if let Some(level) = level {
match level {
"trace" | "debug" | "info" | "warn" | "error" => {}
_ => {
return Err(format!(
"[general].log_level '{}' is invalid (expected one of: trace, debug, info, warn, error)",
level
))
}
}
}
Ok(())
}
fn validate_tokens(path: &str, tokens: &[String]) -> Result<(), String> {
if tokens.iter().any(|t| t.trim().is_empty()) {
return Err(format!("{path} must not contain empty tokens"));
}
Ok(())
}
impl ConfigFile for ClientConfig {
fn config_filename() -> &'static str {
"client.toml"
@@ -299,4 +355,18 @@ port = 8080
let example = ClientConfig::example_toml();
let _config: ClientConfig = toml::from_str(&example).unwrap();
}
#[test]
fn test_validate_rejects_zero_poll_interval() {
let mut config = ClientConfig::default();
config.remote.poll_interval_ms = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_validate_rejects_empty_http_json_token() {
let mut config = ClientConfig::default();
config.frontends.http_json.auth.tokens = vec!["".to_string()];
assert!(config.validate().is_err());
}
}
+2
View File
@@ -137,6 +137,8 @@ async fn async_init() -> DynResult<AppState> {
} else {
ClientConfig::load_from_default_paths()?
};
cfg.validate()
.map_err(|e| format!("Invalid client configuration: {}", e))?;
init_logging(cfg.general.log_level.as_deref());