use super::assistant_test_support::*;
use serde_json::json;
#[path = "assistant_decision_revision_tests.rs"]
mod revision;
#[test]
fn accepted_decision_only_then_reaches_seat() -> Result {
let f = Fixture::new(json!({}))?;
let mut seat = f.seat("agent-pass", "wards")?;
let door = std::sync::Arc::clone(&f.door);
let (send, read) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || -> Result {
let op = seat.read()?;
assert_eq!(
door.issuer()
.decisions
.load(std::sync::atomic::Ordering::SeqCst),
1
);
seat.send(&json!({"ack":{"id":op["op"]["id"]}}))?;
send.send(op)?;
Ok(())
});
check(
f.answer(
"POST",
"/decide",
&json!({"draft":"draft-accepted","decision":"approve"}),
),
200,
"seat_told",
)?;
let op = read.recv_timeout(std::time::Duration::from_secs(5))?;
assert_eq!(
op["op"]["decided"],
json!({"draft":"draft-accepted","decision":"approve"})
);
worker.join().map_err(|_| anyhow::anyhow!("seat panic"))??;
Ok(())
}
#[test]
fn lys_refusal_has_draft_url_and_sends_nothing_to_seat() -> Result {
let f = Fixture::new(json!({}))?;
let mut seat = f.seat("agent-pass", "wards")?;
let out = written(f.answer(
"POST",
"/decide",
&json!({"draft":"draft-refused","decision":"approve"}),
))?;
assert!(
out.contains("draft_not_pending")
&& out.contains("https://lys.example/#/access/drafts?draft=draft-refused"),
"{out}"
);
let read = std::thread::spawn(move || -> Result {
let op = seat.read()?;
assert_eq!(op["op"]["text"], "after refusal");
seat.send(&json!({"ack":{"id":op["op"]["id"]}}))
});
check(
f.answer("POST", "", &json!({"text":"after refusal"})),
202,
"id",
)?;
read.join().map_err(|_| anyhow::anyhow!("seat panic"))??;
Ok(())
}
#[test]
fn start_names_lys_when_person_app_pass_is_not_admitted() -> Result {
let f = Fixture::new(json!({}))?;
let text = written(f.answer("POST", "/start", &json!({})))?;
assert!(
text.starts_with("HTTP/1.1 501 ")
&& text.contains("assistant_start_in_lys")
&& text.contains("https://lys.example"),
"{text}"
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn native_draft_client_carries_person_pass_digest_and_lys_operation_id() -> Result {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let issuer = format!("http://{}", listener.local_addr()?);
let server = std::thread::spawn(move || -> Result {
for step in 0..2 {
let (socket, _) = listener.accept()?;
socket.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
let request =
super::http::read(&mut std::io::BufReader::new(&socket), 16_384, 1_048_576)?;
assert_eq!(request.header("authorization"), Some("Bearer person-pass"));
let response = if step == 0 {
assert_eq!(request.path, "/api/product-drafts");
super::http::Response::json(
200,
&json!({"drafts":[{"id":"draft-native","app":"haematite","holder":"agent-assistant","responsible":PERSON,"target":{"kind":"haematite.database","id":"wards","action":"agent_permission"},"request_digest":"a".repeat(64)}],"next":null}),
)
} else {
assert_eq!(request.path, "/api/product-drafts/draft-native/approve");
let body: serde_json::Value = serde_json::from_slice(&request.body)?;
assert_eq!(body["request_digest"], "a".repeat(64));
let valid = body["operation"]
.as_str()
.and_then(|id| id.strip_prefix("op-"))
.is_some_and(|id| {
id.len() == 32 && id.bytes().all(|byte| byte.is_ascii_hexdigit())
});
if valid {
super::http::Response::json(
200,
&json!({"draft":"draft-native","state":"approved"}),
)
} else {
super::http::Response::json(400, &json!({"refusal":"IdentifierMalformed"}))
}
};
response.write_to(&socket)?;
}
Ok(())
});
let client = super::assistant_lys::Client {
issuer,
http: reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.timeout(std::time::Duration::from_secs(5))
.build()?,
};
let answer = client
.decide("person-pass", "draft-native", "approve")
.await;
server
.join()
.map_err(|_| anyhow::anyhow!("Lys fixture panic"))??;
let answer = answer?;
assert_eq!(answer["state"], "approved");
Ok(())
}