use super::root_presence::{LysRoot, RootPresence};
use std::fmt::Write as _;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::sync::mpsc;
use tracing_subscriber::prelude::*;
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn issuer(answers: Vec<(u16, &'static str)>) -> TestResult<(SocketAddr, mpsc::Receiver<String>)> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
let (asked, asks) = mpsc::channel();
std::thread::spawn(move || -> std::io::Result<()> {
for (status, body) in answers {
let (socket, _) = listener.accept()?;
let mut reader = BufReader::new(socket);
let mut first = String::new();
reader.read_line(&mut first)?;
let mut length = 0;
loop {
let mut header = String::new();
if reader.read_line(&mut header)? == 0 || header == "\r\n" {
break;
}
if let Some((name, value)) = header.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
length = value.trim().parse().map_err(std::io::Error::other)?;
}
}
let mut request = vec![0; length];
reader.read_exact(&mut request)?;
let _sent = asked.send(format!(
"{} {}",
first.trim_end(),
String::from_utf8_lossy(&request)
));
write!(
reader.get_mut(),
"HTTP/1.1 {status} fixture\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)?;
reader.get_mut().flush()?;
}
Ok(())
});
Ok((address, asks))
}
const NONE_HOLDS: &str = r#"{"holders":[],"revision":3,"complete":true,"next":null}"#;
const ONE_HOLDS: &str = r#"{"holders":[{"holder":"person-tom","kind":"person","grant":"g1"}],"revision":4,"complete":false,"next":"person-tom"}"#;
fn runtime() -> TestResult<tokio::runtime::Runtime> {
Ok(tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()?)
}
#[derive(Default)]
struct Lines(std::sync::Mutex<Vec<(tracing::Level, String)>>);
struct Capture(std::sync::Arc<Lines>);
#[derive(Default)]
struct Message(String);
impl tracing::field::Visit for Message {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
let _ = write!(self.0, " {}={value:?}", field.name());
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
let _ = write!(self.0, " {}={value}", field.name());
}
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Capture {
fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
if event.metadata().target() == "liminal::lys_root" {
let mut message = Message::default();
event.record(&mut message);
if let Ok(mut lines) = self.0.0.lock() {
lines.push((*event.metadata().level(), message.0));
}
}
}
}
fn asked(issuer_at: SocketAddr, answers: &[LysRoot]) -> TestResult<Vec<(tracing::Level, String)>> {
let lines = std::sync::Arc::new(Lines::default());
let subscriber = tracing_subscriber::registry().with(Capture(std::sync::Arc::clone(&lines)));
let runtime = runtime()?;
let presence = RootPresence::new(
"ablative",
&format!("http://{issuer_at}/api/"),
"fixture-app-credential".to_owned(),
runtime.handle().clone(),
)?;
assert_eq!(presence.answer(), LysRoot::NotAsked);
tracing::subscriber::with_default(subscriber, || {
for expected in answers {
presence.ask();
assert_eq!(&presence.answer(), expected);
}
});
let lines = lines
.0
.lock()
.map_err(|_| "captured lines poisoned")?
.clone();
Ok(lines)
}
#[test]
fn a_network_with_no_root_is_absent_at_start() -> TestResult {
let (at, asks) = issuer(vec![(200, NONE_HOLDS)])?;
let lines = asked(at, &[LysRoot::Absent])?;
let ask = asks.try_recv()?;
assert!(ask.starts_with("POST /api/grants/who "), "{ask}");
assert!(
ask.contains("\"liminal.network\"") && ask.contains("\"ablative\""),
"{ask}"
);
assert!(
ask.contains("\"admin\"") && ask.contains("\"page_size\":1"),
"{ask}"
);
assert!(asks.try_recv().is_err(), "exactly one who ask");
assert_eq!(lines.len(), 1, "{lines:?}");
assert_eq!(lines[0].0, tracing::Level::ERROR);
assert!(
lines[0].1.contains("ablative") && lines[0].1.contains(&at.to_string()),
"{lines:?}"
);
Ok(())
}
#[test]
fn a_rooted_network_is_held_and_asked_once() -> TestResult {
let (at, asks) = issuer(vec![(200, ONE_HOLDS)])?;
let lines = asked(at, &[LysRoot::Held])?;
assert!(asks.try_recv().is_ok());
assert!(asks.try_recv().is_err(), "exactly one who ask");
assert!(lines.is_empty(), "a held root logs nothing: {lines:?}");
Ok(())
}
#[test]
fn a_reset_asks_again() -> TestResult {
let (at, asks) = issuer(vec![(200, ONE_HOLDS), (200, NONE_HOLDS)])?;
let lines = asked(at, &[LysRoot::Held, LysRoot::Absent])?;
assert!(asks.try_recv().is_ok() && asks.try_recv().is_ok());
assert_eq!(lines.len(), 1, "{lines:?}");
assert_eq!(lines[0].0, tracing::Level::ERROR);
Ok(())
}
#[test]
fn a_refused_question_is_unknown_with_its_cause() -> TestResult {
let (at, _asks) = issuer(vec![(403, r#"{"error":"not yours to read"}"#)])?;
let lines = asked(
at,
&[LysRoot::Unknown {
cause: "lys_root_refused",
}],
)?;
assert_eq!(lines.len(), 1, "{lines:?}");
assert_eq!(lines[0].0, tracing::Level::WARN);
assert!(
!lines[0].1.contains("not yours to read"),
"no response words in the log: {lines:?}"
);
Ok(())
}