use super::assistant_test_support::*;
use crate::serve::{
frame,
protocol::{PROTOCOL, RECEIPT_CAPABILITY},
};
use serde_json::{Value, json};
use std::sync::{Arc, Mutex};
#[derive(Clone, Copy)]
pub(super) enum Put {
Accepted,
Refused,
}
pub(super) struct Store {
calls: Arc<Mutex<Vec<Value>>>,
worker: std::thread::JoinHandle<Result>,
}
impl Store {
pub(super) fn start(f: &Fixture, pages: Vec<Vec<&'static str>>, put: Put) -> Result<Self> {
let listener = std::os::unix::net::UnixListener::bind(f.files.path().join("store.sock"))?;
let calls = Arc::new(Mutex::new(Vec::new()));
let captured = Arc::clone(&calls);
let worker = std::thread::spawn(move || -> Result {
listener.set_nonblocking(true)?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
let mut socket = loop {
match listener.accept() {
Ok((socket, _)) => break socket,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
if std::time::Instant::now() > deadline {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
Err(error) => return Err(error.into()),
}
};
socket.set_nonblocking(false)?;
let cap = std::num::NonZeroU32::new(1_048_576).ok_or_else(|| anyhow::anyhow!("cap"))?;
while let Some(bytes) = frame::read(&mut socket, cap)? {
let request: Value = serde_json::from_slice(&bytes)?;
let method = request["operation"]["method"]
.as_str()
.unwrap_or_default()
.to_owned();
let p = request["operation"]["params"].clone();
captured
.lock()
.map_err(|_| anyhow::anyhow!("calls"))?
.push(request.clone());
let version = json!({"generation":"1","sequence":"5"});
let answer = match method.as_str() {
"hello" => Ok(
json!({"protocol":PROTOCOL,"capabilities":[RECEIPT_CAPABILITY],"max_page":64,"max_frame":cap.get()}),
),
"session.pass" => {
assert_eq!(p["pass"], "person-pass");
Ok(json!({"holder":PERSON,"expires":crate::serve::pass::now()+3600}))
}
"collections.list" => {
let index = p["after"]
.as_str()
.map_or(0, |after| after.parse::<usize>().unwrap_or(usize::MAX));
let page = pages
.get(index)
.ok_or_else(|| anyhow::anyhow!("page {index}"))?;
let next = (index + 1 < pages.len()).then(|| (index + 1).to_string());
let entries: Vec<_> =
page.iter().map(|name| json!({"name":name})).collect();
Ok(
json!({"branch":"main","version":version,"entries":entries,"next_after":next}),
)
}
"store.stats" => Ok(json!({"selection":{"branch":"main","version":version}})),
"mutation.apply" => match put {
Put::Accepted => Ok(
json!({"branch":"main","version":{"generation":"1","sequence":"6"},"request_id":p["request_id"]}),
),
Put::Refused => Err(
json!({"code":"permission_refused","message":"{\"refused\":{\"resource\":\"haematite.database:wards\",\"action\":\"collection.put\",\"request\":\"https://lys.example/#/requests\"}}"}),
),
},
other => anyhow::bail!("unexpected method {other}"),
};
let reply = match answer {
Ok(result) => json!({"id":request["id"],"result":result}),
Err(error) => json!({"id":request["id"],"error":error}),
};
frame::write(&mut socket, &serde_json::to_vec(&reply)?, cap)?;
}
Ok(())
});
Ok(Self { calls, worker })
}
pub(super) fn mutations(self) -> Result<Vec<Value>> {
self.worker
.join()
.map_err(|_| anyhow::anyhow!("store panic"))??;
let calls = self.calls.lock().map_err(|_| anyhow::anyhow!("calls"))?;
Ok(calls
.iter()
.filter(|call| call["operation"]["method"] == "mutation.apply")
.map(|call| call["operation"]["params"].clone())
.collect())
}
}
#[test]
fn start_on_a_store_without_the_record_defines_it_under_the_person_pass() -> Result {
let f = Fixture::new(json!({}))?;
let store = Store::start(&f, vec![vec!["patients", "wards"]], Put::Accepted)?;
let text = written(f.answer("POST", "/start", &json!({})))?;
assert!(
text.starts_with("HTTP/1.1 501 ") && text.contains("assistant_start_in_lys"),
"{text}"
);
let mutations = store.mutations()?;
assert_eq!(mutations.len(), 1, "{mutations:?}");
assert_eq!(mutations[0]["branch"], "main");
assert_eq!(
mutations[0]["expected"],
json!({"generation":"1","sequence":"5"})
);
assert_eq!(
mutations[0]["change"],
json!({"kind":"collection.put","value":{"collection":"_assistant","schema":true}})
);
Ok(())
}
#[test]
fn start_on_a_store_that_holds_the_record_defines_nothing_even_on_a_later_page() -> Result {
let f = Fixture::new(json!({}))?;
let store = Store::start(
&f,
vec![vec!["patients"], vec!["_assistant", "wards"]],
Put::Refused,
)?;
let text = written(f.answer("POST", "/start", &json!({})))?;
assert!(
text.starts_with("HTTP/1.1 501 ") && text.contains("assistant_start_in_lys"),
"{text}"
);
assert!(store.mutations()?.is_empty());
Ok(())
}
#[test]
fn a_person_who_may_not_define_the_record_is_told_why_with_the_way_to_lys() -> Result {
let f = Fixture::new(json!({}))?;
let store = Store::start(&f, vec![vec!["patients"]], Put::Refused)?;
let text = written(f.answer("POST", "/start", &json!({})))?;
assert!(
text.starts_with("HTTP/1.1 403 ")
&& text.contains("assistant_record_unplaced")
&& text.contains("you may not create it")
&& text.contains("https://lys.example"),
"{text}"
);
assert_eq!(store.mutations()?.len(), 1);
Ok(())
}