use std::error::Error;
use std::path::Path;
use super::{Config, Credential};
const FIELDS: [&str; 8] = [
"server",
"channel",
"token_file",
"frame_bytes",
"record_bytes",
"reply_timeout_ms",
"flow_steps",
"request_bytes",
];
fn written(token: &Path, without: Option<&str>, zero: Option<&str>) -> String {
let mut top = String::new();
let mut limits = String::new();
for field in FIELDS {
if without == Some(field) {
continue;
}
let value = match field {
"server" => "\"127.0.0.1:4100\"".to_owned(),
"channel" => "\"events\"".to_owned(),
"token_file" => format!("{:?}", token.display().to_string()),
_ if zero == Some(field) => "0".to_owned(),
_ => "16".to_owned(),
};
if matches!(field, "server" | "channel" | "token_file") {
top.extend([field, " = ", value.as_str(), "\n"]);
} else {
limits.extend([field, " = ", value.as_str(), "\n"]);
}
}
format!("{top}\n[limits]\n{limits}")
}
#[test]
fn config_refuses_each_missing_or_zero_field_by_name() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
std::fs::write(&token, "pass-bytes\n")?;
let path = dir.path().join("ticket.toml");
std::fs::write(&path, written(&token, Some("server"), None))?;
assert_eq!(Config::load(&path)?.server, crate::seat::CENTRAL_SERVER);
for field in FIELDS[1..].iter().filter(|field| **field != "token_file") {
std::fs::write(&path, written(&token, Some(field), None))?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("a missing field was accepted")?;
assert!(refusal.contains(field), "missing {field}: {refusal}");
}
for field in &FIELDS[3..] {
std::fs::write(&path, written(&token, None, Some(field)))?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("a zero field was accepted")?;
assert!(refusal.contains(field), "zero {field}: {refusal}");
}
std::fs::write(&path, written(&token, None, None))?;
let config = Config::load(&path)?;
assert_eq!(config.limits.flow_steps, 16);
let Credential::Token(token) = &config.credential else {
return Err("a token_file configuration did not hold a token".into());
};
assert_eq!(token.as_str(), "pass-bytes");
Ok(())
}
#[test]
fn pass_bytes_appear_in_no_debug_and_no_error() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
std::fs::write(&token, "distinct-pass-7f3a\n")?;
let path = dir.path().join("ticket.toml");
std::fs::write(&path, written(&token, None, None))?;
let config = Config::load(&path)?;
let debug = format!("{config:?}");
assert!(!debug.contains("distinct-pass-7f3a"), "{debug}");
assert!(debug.contains("Token(18 bytes)"), "{debug}");
std::fs::write(&token, "")?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("an empty token was accepted")?;
assert!(refusal.contains("token file is empty"), "{refusal}");
Ok(())
}
#[test]
fn agt001_listen_defaults_and_every_zero_bound_are_named() -> Result<(), Box<dyn Error>> {
let directory = tempfile::tempdir()?;
let token = directory.path().join("token");
std::fs::write(&token, "fixture-pass")?;
let path = directory.path().join("seat.toml");
std::fs::write(&path, written(&token, None, None))?;
let config = Config::load(&path)?;
assert_eq!(config.listen.listen_bound, 16);
assert_eq!(config.listen.content_bytes, 65536);
for field in [
"listen_bound",
"content_bytes",
"join_bound",
"join_bytes",
"batch_messages",
"subject_bytes",
"key_bytes",
"agent_bytes",
"issuer_response_bytes",
"run_pass_bytes",
] {
let text = format!("{}\n[listen]\n{field} = 0\n", written(&token, None, None));
std::fs::write(&path, text)?;
let error = Config::load(&path)
.err()
.ok_or("a zero listen bound was accepted")?;
assert!(
error.to_string().contains(field),
"the listen refusal did not name its bound"
);
}
Ok(())
}
#[test]
fn agt001_listen_refuses_unknown_fields_and_non_subject_text() -> Result<(), Box<dyn Error>> {
let directory = tempfile::tempdir()?;
let token = directory.path().join("token");
std::fs::write(&token, "fixture-pass")?;
let path = directory.path().join("seat.toml");
for table in [
"[listen]\nsubjects = [\"a..b\"]\n",
"[listen]\nunknown = true\n",
] {
std::fs::write(&path, format!("{}\n{table}", written(&token, None, None)))?;
assert!(
Config::load(&path).is_err(),
"an invalid listen table was accepted"
);
}
Ok(())
}
#[test]
fn access010_an_open_seat_carries_no_credential_and_reaches_loopback_only()
-> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
let path = dir.path().join("seat.toml");
for server in ["127.0.0.1:4100", "localhost:7900", "ws://[::1]:7902/ws"] {
let text = written(&token, Some("token_file"), None).replace("127.0.0.1:4100", server);
std::fs::write(&path, text)?;
let config = Config::load(&path)?;
assert!(
matches!(config.credential, Credential::Open { .. }),
"{config:?}"
);
assert_eq!(config.server, server);
}
assert!(!token.exists(), "an open seat reads no token file");
for server in ["192.168.50.40:7900", "wss://liminal.ablative.com.au/ws"] {
let text = written(&token, Some("token_file"), None).replace("127.0.0.1:4100", server);
std::fs::write(&path, text)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("an open seat off loopback was accepted")?;
assert!(
refusal.contains(&format!(
"an open seat (no token_file, no [lys]) reaches a loopback exchange only: server {server}"
)),
"{refusal}"
);
}
std::fs::write(&token, "pass-bytes\n")?;
let both = format!(
"{}\n[lys]\nissuer = \"http://127.0.0.1:8490\"\nagent = \"agent-x\"\n",
written(&token, None, None)
);
std::fs::write(&path, both)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("two credentials were accepted")?;
assert!(
refusal.contains("give exactly one of token_file and [lys]"),
"{refusal}"
);
Ok(())
}
#[test]
fn access011_an_open_seat_reaching_the_network_loads_off_loopback() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
let path = dir.path().join("seat.toml");
for server in ["192.168.50.40:7900", "ws://liminal.lan:7902/ws"] {
let text = format!(
"{}\n[open]\nreach = \"network\"\n",
written(&token, Some("token_file"), None).replace("127.0.0.1:4100", server)
);
std::fs::write(&path, text)?;
let config = Config::load(&path)?;
assert!(
matches!(config.credential, Credential::Open { .. }),
"{config:?}"
);
assert_eq!(config.server, server);
}
let text = format!(
"{}\n[open]\nreach = \"loopback\"\n",
written(&token, Some("token_file"), None).replace("127.0.0.1:4100", "192.168.50.40:7900")
);
std::fs::write(&path, text)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("reach = \"loopback\" off loopback was accepted")?;
assert!(
refusal.contains("reaches a loopback exchange only: server 192.168.50.40:7900"),
"{refusal}"
);
Ok(())
}
#[test]
fn access011_open_beside_a_credential_and_an_unknown_reach_are_refused_by_name()
-> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
let path = dir.path().join("seat.toml");
std::fs::write(&token, "pass-bytes\n")?;
let text = format!(
"{}\n[open]\nreach = \"network\"\n",
written(&token, None, None)
);
std::fs::write(&path, text)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("[open] beside token_file was accepted")?;
assert!(
refusal.contains(
"[open] names a seat with no credential; this seat has token_file or [lys], remove one"
),
"{refusal}"
);
let text = format!(
"{}\n[open]\nreach = \"anywhere\"\n",
written(&token, Some("token_file"), None)
);
std::fs::write(&path, text)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("reach = \"anywhere\" was accepted")?;
for word in ["anywhere", "loopback", "network"] {
assert!(refusal.contains(word), "{word}: {refusal}");
}
Ok(())
}
#[test]
fn access012_an_open_seat_declares_its_name() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("seat.toml");
std::fs::write(
&path,
format!(
"{}\n[open]\nname = \"hermes\"\n",
written(&dir.path().join("token"), Some("token_file"), None)
.replace("127.0.0.1:4100", "127.0.0.1:7900")
),
)?;
let config = Config::load(&path)?;
assert!(
matches!(&config.credential, Credential::Open { name: Some(name) } if name == "hermes"),
"{config:?}"
);
assert_eq!(config.credential.holder(), "open:hermes");
assert_eq!(config.credential.open_record(), b"open:hermes".to_vec());
let bare = dir.path().join("bare.toml");
std::fs::write(
&bare,
written(&dir.path().join("token"), Some("token_file"), None)
.replace("127.0.0.1:4100", "127.0.0.1:7900"),
)?;
let config = Config::load(&bare)?;
assert!(
matches!(&config.credential, Credential::Open { name: None }),
"{config:?}"
);
assert_eq!(config.credential.holder(), "open:anonymous");
assert!(config.credential.open_record().is_empty());
Ok(())
}
#[test]
fn access012_an_invalid_open_name_is_refused_naming_the_member() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
for bad in ["Hermes", "her mes", "", "a/b", &"x".repeat(65)] {
let path = dir.path().join("seat.toml");
std::fs::write(
&path,
format!(
"{}\n[open]\nname = \"{bad}\"\n",
written(&dir.path().join("token"), Some("token_file"), None)
.replace("127.0.0.1:4100", "127.0.0.1:7900")
),
)?;
let refusal = Config::load(&path)
.err()
.ok_or_else(|| format!("name {bad:?} was accepted"))?;
assert!(
refusal
.to_string()
.contains("[open] name must be 1 to 64 of a-z, 0-9, '.', '_' or '-'"),
"{bad:?}: {refusal}"
);
}
Ok(())
}
mod runtime_identity {
use std::error::Error;
use super::super::{Config, Credential, Overrides};
const OPEN: &str = "server = \"127.0.0.1:4100\"\nchannel = \"events\"\n\n[open]\nname = \"ada\"\n\n\
[limits]\nframe_bytes = 16\nrecord_bytes = 16\nreply_timeout_ms = 16\n\
flow_steps = 16\nrequest_bytes = 16\n";
const LYS: &str = "server = \"127.0.0.1:4100\"\nchannel = \"events\"\n\n[lys]\nissuer = \"http://127.0.0.1:4300\"\n\
agent = \"agent-ada\"\nkey_file = \"/keys/ada\"\n\n[limits]\nframe_bytes = 16\n\
record_bytes = 16\nreply_timeout_ms = 16\nflow_steps = 16\nrequest_bytes = 16\n";
fn refused(
path: Option<&std::path::Path>,
overrides: &Overrides,
) -> Result<String, Box<dyn Error>> {
Ok(Config::load_with(path, overrides)
.err()
.ok_or("the flags were accepted")?
.to_string())
}
#[test]
fn a_flag_beside_the_file_wins_over_its_value() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("seat.toml");
std::fs::write(&path, OPEN)?;
let overrides = Overrides {
server: Some("127.0.0.1:4200".into()),
channel: Some("other".into()),
name: Some("bea".into()),
..Overrides::default()
};
let config = Config::load_with(Some(&path), &overrides)?;
assert_eq!(config.server, "127.0.0.1:4200");
assert_eq!(config.channel, "other");
assert!(
matches!(&config.credential, Credential::Open { name: Some(name) } if name == "bea"),
"{:?}",
config.credential
);
assert_eq!(config.agent.as_deref(), Some("bea"));
assert_eq!(config.limits.frame_bytes, 16);
let config = Config::load_with(Some(&path), &Overrides::default())?;
assert_eq!(config.agent.as_deref(), Some("ada"));
Ok(())
}
#[test]
fn with_no_file_the_flags_and_the_seat_init_defaults_make_the_seat()
-> Result<(), Box<dyn Error>> {
let overrides = Overrides {
server: Some("100.64.0.7:7600".into()),
name: Some("tom".into()),
..Overrides::default()
};
let config = Config::load_with(None, &overrides)?;
assert_eq!(config.server, "100.64.0.7:7600");
assert_eq!(config.channel, "console.topology");
assert_eq!(config.limits.frame_bytes, 65536);
assert_eq!(config.limits.request_bytes, 65536);
assert!(
matches!(&config.credential, Credential::Open { name: Some(name) } if name == "tom")
);
assert_eq!(config.agent.as_deref(), Some("tom"));
let overrides = Overrides {
agent: Some("agent-ada".into()),
key_file: Some("/keys/ada".into()),
..Overrides::default()
};
let config = Config::load_with(None, &overrides)?;
assert_eq!(config.server, crate::seat::CENTRAL_SERVER);
let Credential::Lys(lys) = &config.credential else {
return Err(format!("not a Lys seat: {:?}", config.credential).into());
};
assert_eq!(
(
lys.agent.as_str(),
lys.key_file.as_path(),
lys.issuer.as_str()
),
(
"agent-ada",
std::path::Path::new("/keys/ada"),
crate::seat::CENTRAL_ISSUER
)
);
assert_eq!(config.agent.as_deref(), Some("agent-ada"));
Ok(())
}
#[test]
fn a_lys_seat_refuses_as_and_an_open_seat_refuses_agent() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let lys = dir.path().join("lys.toml");
std::fs::write(&lys, LYS)?;
let name = Overrides {
name: Some("bea".into()),
..Overrides::default()
};
let words = refused(Some(&lys), &name)?;
assert!(words.contains("--as") && words.contains("[lys]"), "{words}");
let moved = Overrides {
agent: Some("agent-bea".into()),
key_file: Some("/keys/bea".into()),
..Overrides::default()
};
let config = Config::load_with(Some(&lys), &moved)?;
let Credential::Lys(table) = &config.credential else {
return Err("not a Lys seat".into());
};
assert_eq!(table.issuer, "http://127.0.0.1:4300");
assert_eq!(table.agent, "agent-bea");
let open = dir.path().join("open.toml");
std::fs::write(&open, OPEN)?;
let words = refused(Some(&open), &moved)?;
assert!(
words.contains("--agent") && words.contains("[open]"),
"{words}"
);
let both = Overrides {
name: Some("bea".into()),
..moved
};
let words = refused(None, &both)?;
assert!(
words.contains("--as") && words.contains("--agent"),
"{words}"
);
Ok(())
}
#[test]
fn agent_and_key_file_are_refused_alone_by_name() -> Result<(), Box<dyn Error>> {
let agent = Overrides {
agent: Some("agent-ada".into()),
..Overrides::default()
};
let words = refused(None, &agent)?;
assert!(words.contains("--key-file"), "{words}");
let key = Overrides {
key_file: Some("/keys/ada".into()),
..Overrides::default()
};
let words = refused(None, &key)?;
assert!(words.contains("--agent"), "{words}");
let bad = Overrides {
server: Some("127.0.0.1:4100".into()),
name: Some("Not A Name".into()),
..Overrides::default()
};
let words = refused(None, &bad)?;
assert!(words.contains("--as"), "{words}");
Ok(())
}
}
#[test]
fn answer_bytes_defaults_to_sixteen_mebibytes_and_is_refused_at_zero() -> Result<(), Box<dyn Error>>
{
let dir = tempfile::tempdir()?;
let token = dir.path().join("token");
std::fs::write(&token, "pass-bytes\n")?;
let path = dir.path().join("ticket.toml");
std::fs::write(&path, written(&token, None, None))?;
let config = Config::load(&path)?;
assert_eq!(config.limits.answer_bytes, 16_777_216);
assert_eq!(config.limits.frame_bytes, 16);
std::fs::write(
&path,
format!("{}answer_bytes = 0\n", written(&token, None, None)),
)?;
let refusal = Config::load(&path).err().map(|e| e.to_string());
let refusal = refusal.ok_or("a zero answer_bytes was accepted")?;
assert!(refusal.contains("answer_bytes"), "{refusal}");
Ok(())
}