use anyhow::{Context, Result};
use lvqr_signal::IceServer;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServeConfigFile {
#[serde(default)]
pub auth: AuthSection,
#[serde(default)]
pub mesh_ice_servers: Vec<IceServer>,
#[serde(default)]
pub hmac_playback_secret: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthSection {
#[serde(default)]
pub admin_token: Option<String>,
#[serde(default)]
pub publish_key: Option<String>,
#[serde(default)]
pub subscribe_token: Option<String>,
#[serde(default)]
pub jwt_secret: Option<String>,
#[serde(default)]
pub jwt_issuer: Option<String>,
#[serde(default)]
pub jwt_audience: Option<String>,
#[serde(default)]
pub jwks_url: Option<String>,
#[serde(default)]
pub webhook_auth_url: Option<String>,
}
impl ServeConfigFile {
pub fn from_path(path: &Path) -> Result<Self> {
let body = std::fs::read_to_string(path).with_context(|| format!("read config file {}", path.display()))?;
Self::from_toml_str(&body).with_context(|| format!("parse config file {}", path.display()))
}
pub fn from_toml_str(body: &str) -> Result<Self> {
let parsed: Self = toml::from_str(body).context("toml parse")?;
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_body_parses_to_default() {
let parsed = ServeConfigFile::from_toml_str("").expect("empty toml is valid");
assert!(parsed.auth.admin_token.is_none());
assert!(parsed.auth.publish_key.is_none());
assert!(parsed.mesh_ice_servers.is_empty());
assert!(parsed.hmac_playback_secret.is_none());
}
#[test]
fn auth_section_round_trips() {
let body = r#"
[auth]
publish_key = "pubkey-1"
admin_token = "admin-1"
jwt_secret = "hs256-secret"
jwt_issuer = "iss-1"
jwt_audience = "aud-1"
"#;
let parsed = ServeConfigFile::from_toml_str(body).expect("auth section parses");
assert_eq!(parsed.auth.publish_key.as_deref(), Some("pubkey-1"));
assert_eq!(parsed.auth.admin_token.as_deref(), Some("admin-1"));
assert_eq!(parsed.auth.jwt_secret.as_deref(), Some("hs256-secret"));
assert_eq!(parsed.auth.jwt_issuer.as_deref(), Some("iss-1"));
assert_eq!(parsed.auth.jwt_audience.as_deref(), Some("aud-1"));
assert!(parsed.auth.subscribe_token.is_none());
assert!(parsed.auth.jwks_url.is_none());
assert!(parsed.auth.webhook_auth_url.is_none());
}
#[test]
fn mesh_ice_servers_array_of_tables() {
let body = r#"
[[mesh_ice_servers]]
urls = ["stun:stun.l.google.com:19302"]
[[mesh_ice_servers]]
urls = ["turn:turn.example.com:3478"]
username = "u"
credential = "p"
"#;
let parsed = ServeConfigFile::from_toml_str(body).expect("ice servers parse");
assert_eq!(parsed.mesh_ice_servers.len(), 2);
assert_eq!(parsed.mesh_ice_servers[0].urls, vec!["stun:stun.l.google.com:19302"]);
assert!(parsed.mesh_ice_servers[0].username.is_none());
assert_eq!(parsed.mesh_ice_servers[1].username.as_deref(), Some("u"));
assert_eq!(parsed.mesh_ice_servers[1].credential.as_deref(), Some("p"));
}
#[test]
fn hmac_playback_secret_top_level() {
let body = r#"hmac_playback_secret = "abc123""#;
let parsed = ServeConfigFile::from_toml_str(body).expect("hmac parse");
assert_eq!(parsed.hmac_playback_secret.as_deref(), Some("abc123"));
}
#[test]
fn unknown_top_level_key_is_rejected() {
let body = r#"
future_key_we_dont_know_yet = "hello"
[auth]
publish_key = "x"
"#;
let parsed = ServeConfigFile::from_toml_str(body).expect("unknown keys are tolerated");
assert_eq!(parsed.auth.publish_key.as_deref(), Some("x"));
}
#[test]
fn malformed_toml_returns_error() {
let body = r#"this is = not = valid toml"#;
let err = ServeConfigFile::from_toml_str(body).expect_err("malformed must fail");
let chain = format!("{err:#}");
assert!(chain.contains("toml parse") || chain.to_lowercase().contains("parse"));
}
#[test]
fn from_path_round_trips_through_disk() {
let dir = tempfile::tempdir().expect("tmp");
let path = dir.path().join("lvqr.toml");
std::fs::write(
&path,
"hmac_playback_secret = \"disk-test\"\n[auth]\npublish_key = \"from-disk\"\n",
)
.expect("write");
let parsed = ServeConfigFile::from_path(&path).expect("from_path");
assert_eq!(parsed.hmac_playback_secret.as_deref(), Some("disk-test"));
assert_eq!(parsed.auth.publish_key.as_deref(), Some("from-disk"));
}
#[test]
fn auth_section_eq_lets_diffs_be_detected() {
let a = AuthSection {
publish_key: Some("v1".into()),
..Default::default()
};
let b = AuthSection {
publish_key: Some("v1".into()),
..Default::default()
};
let c = AuthSection {
publish_key: Some("v2".into()),
..Default::default()
};
assert_eq!(a, b);
assert_ne!(a, c);
}
}