Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
//! `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<Config, ConfigError> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user