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:
2026-09-23 19:05:44 -07:00
co-authored by Claude Opus 5.5
parent f38dc8d474
commit 0339dc13b2
69 changed files with 7790 additions and 3 deletions
@@ -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}");
}
}