use std::error::Error;
use liminal_sdk::{PushClient, PushReceiveOutcome};
use serde_json::{Value, json};
use super::network::OpenServer;
use super::{CHANNEL, Host, SUBJECT, Ticket, registration};
fn open_worker(address: &str) -> Result<PushClient, Box<dyn Error>> {
let worker = PushClient::connect_with_registration_and_auth(address, registration(), b"")?;
let receiver = worker.receiver_handle()?;
let writer = worker.writer_handle();
std::thread::spawn(move || {
let Ok(PushReceiveOutcome::Frame(push)) = receiver.recv() else {
return;
};
let input: Value = serde_json::from_slice(push.payload()).unwrap_or(Value::Null);
let reply = serde_json::to_vec(&json!({ "echoed": input })).unwrap_or_default();
drop(writer.reply(push.correlation_id(), reply));
});
Ok(worker)
}
fn names_under(dir: &std::path::Path) -> Result<Vec<String>, Box<dyn Error>> {
let mut names = Vec::new();
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
names.push(
path.file_name()
.map_or_else(String::new, |name| name.to_string_lossy().into_owned()),
);
if path.is_dir() {
names.extend(names_under(&path)?);
}
}
Ok(names)
}
#[test]
fn access010_an_open_exchange_and_an_open_seat_work_with_no_lys_anywhere()
-> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let server = OpenServer::start(dir.path())?;
let _worker = open_worker(&server.address.to_string())?;
let seat = dir.path().join("seat.toml");
std::fs::write(
&seat,
format!(
"server = \"{}\"\nchannel = \"{CHANNEL}\"\n\n[limits]\nframe_bytes = 65536\nrecord_bytes = 1048576\nreply_timeout_ms = 5000\nflow_steps = 8\nrequest_bytes = 65536\n",
server.address
),
)?;
let mut host = Host::open(Ticket::spawn(&seat)?, true)?;
let listed = host.tool("list", &json!({}))?;
assert_eq!(listed["isError"], false, "{listed}");
let stations = listed["structuredContent"]["stations"].to_string();
assert!(stations.contains(SUBJECT), "{listed}");
let services = host.tool("services", &json!({}))?;
assert_eq!(services["isError"], true, "{services}");
assert!(
services["structuredContent"]["error"]
.as_str()
.is_some_and(|error| error
.contains("lifecycle_unavailable: no [[auth.lys.templates]] are configured")),
"{services}"
);
let answer = host.tool(
"call",
&json!({ "subject": SUBJECT, "input": { "text": "alone" } }),
)?;
assert_eq!(answer["isError"], false, "{answer}");
assert_eq!(
answer["structuredContent"]["echoed"]["text"], "alone",
"{answer}"
);
let (stderr, code) = host.ticket.close()?;
assert_eq!(code, 0, "{stderr}");
let log = server.log()?;
assert!(log.contains("liminal server listener bound"), "{log}");
assert!(log.contains("mode=open"), "{log}");
assert!(!log.contains("mode=gated"), "{log}");
let names = names_under(dir.path())?;
for word in ["key", "pass", "token"] {
assert!(
!names
.iter()
.any(|name| name.to_ascii_lowercase().contains(word)),
"a file named with {word} under the open directory: {names:?}"
);
}
Ok(())
}
#[test]
fn access010_an_open_exchange_off_loopback_refuses_to_start_by_name() -> Result<(), Box<dyn Error>>
{
let dir = tempfile::tempdir()?;
let refusal = match OpenServer::start_on(dir.path(), "0.0.0.0") {
Ok(server) => {
return Err(format!("an open exchange off loopback started: {}", server.log()?).into());
}
Err(refusal) => refusal.to_string(),
};
assert!(
refusal.contains("the open exchange did not start (exit status: 1)"),
"{refusal}"
);
assert!(
refusal.contains("open mode (no [auth]): listen_address 0.0.0.0:")
&& refusal.contains("is not loopback; an exchange with no [auth] listens on loopback only, give [auth] to listen elsewhere"),
"{refusal}"
);
Ok(())
}