use serde_json::{Value, json};
use super::Config;
fn sample() -> Value {
json!({
"listen": "127.0.0.1:8740",
"origin": "https://studio.example",
"files": "/opt/haematite/studio",
"stores": {"wards": "/run/haematite/wards.sock", "studio": "/run/haematite/studio.sock"},
"lys": {
"issuer": "https://lys.example/",
"client_id": "studio",
"secret_file": "/etc/haematite/studio.secret",
"redirect": "https://studio.example/callback"
}
})
}
fn parsed(value: &Value) -> Result<Config, String> {
Config::parse(value.to_string().as_bytes())
}
#[test]
fn a_whole_config_is_taken_with_its_defaults() -> Result<(), String> {
let config = parsed(&sample())?;
let site = config.site(&crate::client::network::Trust::system()?)?;
assert_eq!(site.origin, "https://studio.example");
assert_eq!(site.stores.len(), 2);
assert_eq!(config.limits().max_connections, 64);
Ok(())
}
#[test]
fn each_unsafe_field_is_refused_by_name() -> Result<(), String> {
let cases: [(&str, Value, &str); 9] = [
("/listen", json!("0.0.0.0:8740"), "listen"),
("/origin", json!("http://studio.example"), "origin"),
("/origin", json!("https://studio.example/app"), "origin"),
("/files", json!("studio"), "files"),
("/stores", json!({}), "stores"),
(
"/stores",
json!({"a/b": "/run/haematite/a.sock"}),
"store name",
),
("/stores", json!({"wards": "wards.sock"}), "store socket"),
(
"/lys/redirect",
json!("https://elsewhere.example/callback"),
"redirect",
),
("/lys/secret_file", json!("studio.secret"), "secret_file"),
];
for (pointer, value, named) in cases {
let mut config = sample();
let slot = config
.pointer_mut(pointer)
.ok_or_else(|| format!("the sample has no {pointer}"))?;
*slot = value.clone();
let message = parsed(&config)
.err()
.ok_or_else(|| format!("{pointer} = {value} was taken"))?;
assert!(message.contains(named), "{pointer} = {value}: {message}");
}
Ok(())
}
#[test]
fn an_unknown_field_is_refused() -> Result<(), String> {
let mut config = sample();
config
.as_object_mut()
.ok_or("the sample is not an object")?
.insert("secret".to_owned(), json!("x"));
assert!(parsed(&config).is_err());
Ok(())
}
fn local() -> Value {
json!({
"listen": "127.0.0.1:7443",
"origin": "http://127.0.0.1:7443",
"files": "/opt/haematite/studio",
"stores": {"wards": "/run/haematite/wards.sock", "studio": "/run/haematite/studio.sock"}
})
}
#[test]
fn a_door_with_no_lys_block_takes_a_loopback_origin_and_checks_with_no_secret()
-> Result<(), Box<dyn std::error::Error>> {
let config = parsed(&local())?;
let site = config.site(&crate::client::network::Trust::system()?)?;
assert_eq!(site.origin, "http://127.0.0.1:7443");
let folder = tempfile::tempdir()?;
let path = folder.path().join("door.json");
std::fs::write(&path, local().to_string())?;
let args = [
"--check",
"--config",
path.to_str().ok_or("path is not text")?,
];
crate::door::cli::run(args.into_iter().map(str::to_owned))?;
Ok(())
}
#[test]
fn a_door_with_no_lys_block_refuses_any_other_origin_by_name() -> Result<(), String> {
for origin in [
"http://10.0.0.5:7443",
"https://studio.example",
"http://127.0.0.1:7443/x",
] {
let mut config = local();
config["origin"] = json!(origin);
let message = parsed(&config)
.err()
.ok_or_else(|| format!("{origin} was taken"))?;
assert!(
message.starts_with("door_local_origin_not_loopback: ") && message.contains(origin),
"{origin}: {message}"
);
}
Ok(())
}
fn without_studio() -> Value {
json!({
"listen": "127.0.0.1:7443",
"origin": "http://127.0.0.1:7443",
"files": "/opt/haematite/studio",
"stores": {"haematite": "/run/haematite/haematite.sock"}
})
}
#[test]
fn a_door_whose_stores_do_not_name_studio_is_refused_with_the_line_to_add() -> Result<(), String> {
let message = parsed(&without_studio())
.err()
.ok_or("a door with no studio store was taken")?;
assert_eq!(
message,
"Studio keeps its arrangements and saved views in a store named studio, and this door's \
stores name none: serve a store for Studio and add it to stores, as \
\"studio\": \"/run/haematite/studio.sock\"."
);
Ok(())
}
#[test]
fn a_door_of_network_stores_is_given_an_endpoint_line_to_add() -> Result<(), String> {
let mut config = without_studio();
config["stores"] =
json!({"wards": {"endpoint": "wards.example:7444", "server_name": "wards.example"}});
let message = parsed(&config)
.err()
.ok_or("a door with no studio store was taken")?;
assert!(
message.contains(
"\"studio\": {\"endpoint\": \"<host>:<port>\", \"server_name\": \"<TLS name>\"}"
),
"{message}"
);
Ok(())
}
#[test]
fn door_check_refuses_a_door_with_no_studio_store_and_names_the_stores_it_serves()
-> Result<(), Box<dyn std::error::Error>> {
let folder = tempfile::tempdir()?;
let path = folder.path().join("door.json");
let args = || {
[
"--check".to_owned(),
"--config".to_owned(),
path.to_string_lossy().into_owned(),
]
.into_iter()
};
std::fs::write(&path, without_studio().to_string())?;
let refused = crate::door::cli::run(args())
.err()
.ok_or("door --check passed a door with no studio store")?;
assert!(
refused.contains("store named studio") && refused.contains("\"studio\": "),
"{refused}"
);
let mut config = without_studio();
config["stores"]["studio"] = json!("/run/haematite/studio.sock");
std::fs::write(&path, config.to_string())?;
crate::door::cli::run(args())?;
Ok(())
}