//! `gatewayd.toml` (M4a spec, section 3). Do not edit. #[path = "support/tmp.rs"] mod tmp; use std::path::PathBuf; use gatewayd::config::{Config, ConfigError, SecretSource, ServerUrl, parse_url, valid_id}; use tmp::TempDir; const OWNER: &str = "abcdefghijklmnopqrstuvwxyz"; const CHANNEL: &str = "0123456789abcdefghijklmnop"; fn minimal() -> String { format!( "[mattermost]\nurl = \"https://straylight.scylla-hammerhead.ts.net\"\n\ [secrets.mattermost_token]\ncredential = \"mattermost-token\"\n\ [allow]\nusers = [\"{OWNER}\"]\n" ) } fn load(text: &str) -> Result { let dir = TempDir::new("cfg"); let path = dir.write("gatewayd.toml", text); Config::load(&path) } fn invalid(text: &str) -> String { match load(text) { Err(ConfigError::Invalid(_, why)) => why, other => panic!("expected Invalid for {text:?}, got {other:?}"), } } #[test] fn a_minimal_config_gets_every_default() { let c = load(&minimal()).unwrap(); assert_eq!( c.server().unwrap(), ServerUrl { tls: true, host: "straylight.scylla-hammerhead.ts.net".to_string(), port: 443 } ); assert_eq!(c.mattermost.ca_file, None); assert_eq!( c.token_source().unwrap(), SecretSource::Credential("mattermost-token".to_string()) ); assert_eq!(c.allow.users, vec![OWNER.to_string()]); assert!(c.allow.channels.is_empty()); assert_eq!((c.limits.queue, c.limits.typing_every_ms), (20, 3_000)); assert_eq!( (c.limits.ping_every_ms, c.limits.dead_after_ms), (30_000, 60_000) ); let home = std::env::var_os("BOXMAKER_HOME") .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("/var/lib/boxmaker")); assert_eq!(c.loop_socket(), home.join("run/loop/loop.sock")); assert_eq!(c.state_path(), home.join("gateway/state.json")); } #[test] fn every_value_can_be_set() { let text = format!( "[mattermost]\nurl = \"http://127.0.0.1:8065\"\nca_file = \"/etc/boxmaker/ca.pem\"\n\ [secrets.mattermost_token]\nfile = \"/home/k/.config/boxmaker/token\"\n\ [allow]\nusers = [\"{OWNER}\"]\nchannels = [\"{CHANNEL}\"]\n\ [loop]\nsocket = \"/run/l.sock\"\n[paths]\nhome = \"/h\"\n\ [limits]\nqueue = 5\ntyping_every_ms = 1\nping_every_ms = 2\ndead_after_ms = 3\n" ); let c = load(&text).unwrap(); assert_eq!( c.server().unwrap(), ServerUrl { tls: false, host: "127.0.0.1".to_string(), port: 8065 } ); assert_eq!( c.mattermost.ca_file, Some(PathBuf::from("/etc/boxmaker/ca.pem")) ); assert_eq!( c.token_source().unwrap(), SecretSource::File(PathBuf::from("/home/k/.config/boxmaker/token")) ); assert_eq!(c.allow.channels, vec![CHANNEL.to_string()]); assert_eq!(c.loop_socket(), PathBuf::from("/run/l.sock")); assert_eq!(c.state_path(), PathBuf::from("/h/gateway/state.json")); assert_eq!( ( c.limits.queue, c.limits.typing_every_ms, c.limits.ping_every_ms, c.limits.dead_after_ms ), (5, 1, 2, 3) ); } #[test] fn an_env_secret() { let text = minimal().replace( "credential = \"mattermost-token\"", "env = \"BOXMAKER_MM_TOKEN\"", ); assert_eq!( load(&text).unwrap().token_source().unwrap(), SecretSource::Env("BOXMAKER_MM_TOKEN".to_string()) ); } #[test] fn urls() { for (url, tls, host, port) in [ ("https://a.example", true, "a.example", 443), ("https://a.example:8443", true, "a.example", 8443), ("http://localhost", false, "localhost", 80), ("http://127.0.0.1:8065", false, "127.0.0.1", 8065), ] { assert_eq!( parse_url(url), Ok(ServerUrl { tls, host: host.to_string(), port }), "{url}" ); } for url in [ "", "a.example", "ftp://a.example", "https://", "https://a.example/", "https://a.example/api", "https://A.example", "https://a.example:0", "https://a.example:65536", "https://a.example:0443", "https://a.example:", "https://user@a.example", "https://a.example?x", "https://.a.example", "https://a.example.", "https://[::1]:443", "https:// a.example", ] { assert!(parse_url(url).is_err(), "{url:?} must be refused"); } } #[test] fn ids() { assert!(valid_id(OWNER)); assert!(valid_id(CHANNEL)); for id in [ "", "abc", "abcdefghijklmnopqrstuvwxyZ", "abcdefghijklmnopqrstuvwxy-", "abcdefghijklmnopqrstuvwxyza", ] { assert!(!valid_id(id), "{id:?}"); } } #[test] fn bad_values_are_named() { let token = "credential = \"mattermost-token\""; let cases: Vec<(String, &str)> = vec![ ( minimal().replace( "https://straylight.scylla-hammerhead.ts.net", "https://x.example/path", ), "url", ), ( minimal() .replace( "[secrets.mattermost_token]", "[mattermost2]\n[secrets.other]", ) .replace("[mattermost2]\n", ""), "mattermost_token", ), ( minimal().replace(token, "credential = \"a b\""), "credential", ), (minimal().replace(token, "env = \"lower\""), "env"), ( minimal().replace(token, "file = \"relative/token\""), "absolute", ), ( minimal().replace(token, "credential = \"x\"\nenv = \"Y\""), "exactly one", ), (minimal().replace(token, ""), "exactly one"), ( minimal().replace(&format!("[\"{OWNER}\"]"), "[]"), "users is empty", ), (minimal().replace(OWNER, "tooshort"), "not a Mattermost id"), ( format!("{}channels = [\"NOTANID\"]\n", minimal()), "not a Mattermost id", ), ( minimal().replace("[mattermost]\n", "[mattermost]\nca_file = \"ca.pem\"\n"), "ca_file", ), (format!("{}[limits]\nqueue = 0\n", minimal()), "queue"), ( format!("{}[limits]\ndead_after_ms = 0\n", minimal()), "dead_after_ms", ), ]; for (text, word) in cases { let why = invalid(&text); assert!(why.contains(word), "{word}: {why}"); } } #[test] fn unknown_keys_and_missing_tables_are_parse_errors() { for text in [ format!("{}[allow2]\n", minimal()), minimal().replace("[mattermost]\n", "[mattermost]\nproxy = \"x\"\n"), minimal().replace( "credential = \"mattermost-token\"", "credential = \"t\"\nkeyring = \"x\"", ), minimal().replace(&format!("[allow]\nusers = [\"{OWNER}\"]\n"), ""), format!("{}[limits]\nqueue = -1\n", minimal()), ] { assert!(matches!(load(&text), Err(ConfigError::Parse(..))), "{text}"); } }