use std::time::Duration;
use serde_json::{Value, json};
use super::{ALIASES, LAB, LAB_MAC, Lab};
use crate::Policy;
use crate::listen::MAX_DEVICE_PAGES;
use crate::policy::Caps;
const UPDATED: &str = "notifications/resources/updated";
fn lab_devices() -> Vec<u64> {
(300..305)
.chain(LAB..LAB + 1)
.chain(ALIASES)
.map(u64::from)
.collect()
}
fn paged(page: usize) -> Policy {
Policy {
caps: Caps {
devices: page,
..Caps::default()
},
..Policy::default()
}
}
fn instances(body: &Value) -> Vec<u64> {
body["devices"]
.as_array()
.unwrap()
.iter()
.map(|row| row["instance"].as_u64().unwrap())
.collect()
}
async fn follow(lab: &mut Lab, uri: &str) -> Vec<Vec<u64>> {
let mut pages = Vec::new();
let mut uri = uri.to_string();
loop {
let body = lab.agent.read(&uri).await.unwrap();
assert_eq!(body["uri"], json!(uri));
pages.push(instances(&body));
match body.get("next") {
Some(next) => {
assert_eq!(body["truncated"], json!(true), "{body}");
uri = next.as_str().unwrap().to_string();
}
None => {
assert_eq!(body["truncated"], json!(false), "{body}");
return pages;
}
}
}
}
#[tokio::test(start_paused = true)]
async fn next_leads_through_every_device_once() {
let all = lab_devices();
for (page, sizes) in [
(3, vec![3, 3, 3, 1]),
(5, vec![5, 5]),
(10, vec![10]),
(200, vec![10]),
] {
let mut lab = Lab::start(paged(page)).await;
let first = lab.agent.read("bacnet://devices").await.unwrap();
assert_eq!(first["total"], json!(10), "{first}");
if page == 3 {
assert_eq!(first["next"], json!("bacnet://devices/after/302"));
let second = lab.agent.read("bacnet://devices/after/302").await.unwrap();
assert_eq!(instances(&second), [303, 304, 1201]);
assert_eq!(second["total"], json!(10), "the whole list's count");
assert_eq!(second["next"], json!("bacnet://devices/after/1201"));
}
let pages = follow(&mut lab, "bacnet://devices").await;
let lengths: Vec<usize> = pages.iter().map(Vec::len).collect();
assert_eq!(lengths, sizes, "page {page}");
assert_eq!(pages.concat(), all, "page {page}");
let some = lab.agent.read("bacnet://devices/after/1000").await.unwrap();
assert_eq!(instances(&some)[0], u64::from(LAB));
let none = lab
.agent
.read("bacnet://devices/after/4194303")
.await
.unwrap();
assert_eq!(none["devices"], json!([]));
assert_eq!(none["truncated"], json!(false));
assert!(none.get("next").is_none(), "{none}");
lab.stop().await;
}
}
#[tokio::test(start_paused = true)]
async fn a_device_found_between_pages_shifts_nothing() {
let mut lab = Lab::start(paged(3)).await;
let first = lab.agent.read("bacnet://devices").await.unwrap();
assert_eq!(instances(&first), [300, 301, 302]);
for instance in [100, 1000] {
lab.client.add_device(instance, &LAB_MAC).await.unwrap();
}
let next = first["next"].as_str().unwrap().to_string();
let rest = follow(&mut lab, &next).await;
assert_eq!(rest[0], [303, 304, 1000]);
let mut seen = instances(&first);
seen.extend(rest.concat());
let mut expected = lab_devices();
expected.insert(5, 1000);
assert_eq!(seen, expected, "each listed once, 100 behind the cursor");
let last = lab.agent.read("bacnet://devices/after/1204").await.unwrap();
assert_eq!(last["total"], json!(12), "{last}");
lab.stop().await;
}
#[tokio::test(start_paused = true)]
async fn a_bad_cursor_is_refused_with_nothing_sent() {
let mut lab = Lab::start(paged(3)).await;
let before = lab.sends().len();
for bad in [
"bacnet://devices/after/abc",
"bacnet://devices/after/4194304",
"bacnet://devices/after/+1",
"bacnet://devices/after/",
"bacnet://devices/after/1/2",
] {
let error = lab.agent.read(bad).await.unwrap_err();
assert_eq!(error["code"], json!(-32602), "{bad}: {error}");
assert_eq!(error["data"]["error"], json!("invalid-argument"), "{bad}");
assert_eq!(error["data"]["field"], json!("uri"), "{bad}");
}
assert_eq!(lab.sends().len(), before);
lab.stop().await;
}
#[tokio::test(start_paused = true)]
async fn the_page_template_is_listed() {
let mut lab = Lab::start(Policy::default()).await;
let listed = lab
.agent
.modern("resources/templates/list", json!({}))
.await;
let templates = listed["result"]["resourceTemplates"].as_array().unwrap();
let page = templates
.iter()
.find(|t| t["uriTemplate"] == json!("bacnet://devices/after/{instance}"))
.unwrap_or_else(|| panic!("{listed}"));
assert_eq!(page["name"], json!("devices-after"));
assert_eq!(page["mimeType"], json!("application/json"));
lab.stop().await;
}
#[tokio::test(start_paused = true)]
async fn a_passive_server_offers_no_page() {
use bacnet_transport::bip::BipTransport;
let policy = Policy {
passive: true,
..Policy::default()
};
let (mut agent, serving) = super::Agent::connect(
crate::McpServer::<BipTransport>::new(None, policy, "tester").unwrap(),
);
let templates = agent.modern("resources/templates/list", json!({})).await;
assert_eq!(templates["result"]["resourceTemplates"], json!([]));
for uri in ["bacnet://devices", "bacnet://devices/after/5"] {
let error = agent.read(uri).await.unwrap_err();
assert_eq!(error["data"]["reason"], json!("passive"), "{uri}: {error}");
}
let (_, accepted) = agent
.listen(&["bacnet://devices/after/5", "bacnet://session"])
.await;
assert_eq!(accepted, ["bacnet://session"]);
drop(agent);
serving.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn a_stream_follows_at_most_the_page_cap() {
let mut lab = Lab::start(paged(3)).await;
let mut asked = vec![
"bacnet://session".to_string(),
"bacnet://devices".to_string(),
];
asked.extend((0..MAX_DEVICE_PAGES + 4).map(|n| format!("bacnet://devices/after/{n}")));
asked.push("bacnet://devices/after/00".to_string());
let asked: Vec<&str> = asked.iter().map(String::as_str).collect();
let (_, accepted) = lab.agent.listen(&asked).await;
let expected: Vec<&str> = asked[..MAX_DEVICE_PAGES + 1].to_vec();
assert_eq!(accepted, expected);
lab.client.clear_devices().await;
lab.server.broadcast_i_am().await.unwrap();
let updates = lab.agent.drain(UPDATED, Duration::from_secs(3)).await;
let mut told: Vec<&str> = updates
.iter()
.map(|u| u["params"]["uri"].as_str().unwrap())
.collect();
told.sort_unstable();
let mut pages = expected[1..].to_vec();
pages.sort_unstable();
assert_eq!(told, pages, "one update per followed page");
lab.stop().await;
}
#[tokio::test(start_paused = true)]
async fn every_followed_page_is_announced_when_a_device_arrives() {
let mut lab = Lab::start(paged(3)).await;
let uris = ["bacnet://devices", "bacnet://devices/after/302"];
let (_, accepted) = lab.agent.listen(&uris).await;
assert_eq!(accepted, uris);
lab.client.clear_devices().await;
for _ in 0..3 {
lab.server.broadcast_i_am().await.unwrap();
}
let updates = lab.agent.drain(UPDATED, Duration::from_secs(3)).await;
let mut told: Vec<&str> = updates
.iter()
.map(|u| u["params"]["uri"].as_str().unwrap())
.collect();
told.sort_unstable();
assert_eq!(told, uris, "{updates:?}");
lab.stop().await;
}