use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use clawft_types::secret::SecretString;
use serde_json::{Value, json};
use wiremock::matchers::{header, method};
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
use super::fleet_inventory::{
EVENT_KIND_FLEET_INVENTORY, FleetCheck, FleetError, FleetInventory, FleetMcp, READ_TOOLS,
ReadOnlyFleet, parse_fleet_status,
};
use super::fleet_mcp::{HttpFleetMcp, OAuthTokens};
use super::seed::{SEED_CONCURRENCY_CAP, SeedApiRuntime, SeedConfig, SeedPin};
use super::seed_http::HttpSeedTransport;
use super::seed_tls::SeedTls;
use super::test_support::MemoryCredentials;
use crate::chain::{ChainManager, EVENT_KIND_WORKLOAD_REFUSE};
const FIXTURE: &str = include_str!("fixtures/cognitum_fleet_status.json");
const OAUTH: &str = "oauth-access-token-fleet-0123456789";
const SEED_TOKEN: &str = "seed-token-fleet-0123456789";
const DEVICE: &str = "4b0c1b1e-7a52-4f0e-8d3a-0d3c8f9a1e11";
const KEY: &str = "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90";
#[derive(Default)]
struct Recorder {
calls: Mutex<Vec<String>>,
}
#[async_trait]
impl FleetMcp for Recorder {
async fn call_tool(&self, name: &str, _args: Value) -> Result<Value, FleetError> {
self.calls.lock().unwrap().push(name.to_string());
assert!(
READ_TOOLS.contains(&name),
"non-read MCP tool invoked: {name}"
);
Ok(serde_json::from_str(FIXTURE).unwrap())
}
}
fn inventory() -> (FleetInventory, Arc<Recorder>, Arc<ChainManager>) {
let rec = Arc::new(Recorder::default());
let chain = Arc::new(ChainManager::new(0, 1000));
(
FleetInventory::new(
ReadOnlyFleet::new(rec.clone()),
chain.clone(),
"cognitum-oauth",
),
rec,
chain,
)
}
#[test]
fn the_fixture_parses_in_every_accepted_spelling() {
let (c, skipped) = parse_fleet_status(&serde_json::from_str(FIXTURE).unwrap()).unwrap();
assert_eq!(skipped, 1, "an entry without a device id is skipped");
assert_eq!(c.len(), 3);
assert_eq!(c[0].device_id, DEVICE);
assert_eq!(c[0].firmware.as_deref(), Some("0.24.2"));
assert_eq!(c[0].online, Some(true));
assert_eq!(c[0].device_pubkey.as_deref(), Some(KEY));
assert_eq!(c[1].online, Some(false), "status: offline");
assert_eq!(c[1].firmware.as_deref(), Some("0.22.20"));
assert_eq!(c[2].device_pubkey, None);
assert!(parse_fleet_status(&json!("nope")).is_err());
assert!(parse_fleet_status(&json!({"unrelated": 1})).is_err());
assert_eq!(
parse_fleet_status(&json!([{"id": "x"}])).unwrap().0.len(),
1
);
}
#[tokio::test]
async fn the_inventory_only_ever_calls_the_read_tool_and_chains_the_read() {
let (inv, rec, chain) = inventory();
let list = inv.candidates(Some("us-east")).await.unwrap();
assert_eq!(list.len(), 3);
assert_eq!(*rec.calls.lock().unwrap(), ["fleet_status"]);
let events: Vec<_> = chain
.tail(chain.len())
.into_iter()
.filter(|e| e.kind == EVENT_KIND_FLEET_INVENTORY)
.collect();
assert_eq!(events.len(), 1);
let p = events[0].payload.clone().unwrap();
assert_eq!(p["tool"], "fleet_status");
assert_eq!(p["credential"], "cognitum-oauth");
assert_eq!(p["candidates"][0]["device_id"], DEVICE);
}
#[tokio::test]
async fn a_write_tool_is_refused_before_it_reaches_the_transport() {
let rec = Arc::new(Recorder::default());
let guard = ReadOnlyFleet::new(rec.clone());
for tool in [
"device_register",
"contact_send",
"leads_create",
"lead_subscribe",
"fleet_status ",
"",
] {
let e = guard.call(tool, json!({})).await.unwrap_err();
assert!(matches!(e, FleetError::NonReadTool(_)), "{tool:?}: {e}");
}
assert!(rec.calls.lock().unwrap().is_empty(), "nothing was sent");
guard.call("fleet_status", json!({})).await.unwrap();
assert_eq!(rec.calls.lock().unwrap().len(), 1);
}
async fn local_seed(identity: Value) -> (MockServer, SeedApiRuntime) {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(wiremock::matchers::path("/api/v1/identity"))
.respond_with(ResponseTemplate::new(200).set_body_json(identity))
.mount(&server)
.await;
let rt = SeedApiRuntime::new(
SeedConfig {
node_id: "seed-fleet".into(),
pins: vec![SeedPin::new("fall-detect", "1.0.0")],
concurrency_cap: SEED_CONCURRENCY_CAP,
},
Arc::new(HttpSeedTransport::new(&server.uri(), SeedTls::WebPki).unwrap()),
Arc::new(MemoryCredentials::with("seed-fleet", SEED_TOKEN)),
)
.unwrap();
(server, rt)
}
fn identity(device: &str, key: &str) -> Value {
json!({"device_id": device, "public_key": key, "firmware_version": "0.24.2"})
}
#[tokio::test]
async fn the_paired_seed_matches_the_cloud_listing_or_a_key_mismatch_is_refused() {
let (inv, _, chain) = inventory();
let list = inv.candidates(None).await.unwrap();
let (_s, rt) = local_seed(identity(DEVICE, KEY)).await;
assert_eq!(
inv.cross_check(&list, &rt).await.unwrap(),
FleetCheck::Verified
);
let (_s2, bad) = local_seed(identity(DEVICE, &"00".repeat(32))).await;
let e = inv.cross_check(&list, &bad).await.unwrap_err();
assert!(e.to_string().contains("differs"), "{e}");
let refusals: Vec<_> = chain
.tail(chain.len())
.into_iter()
.filter(|e| e.kind == EVENT_KIND_WORKLOAD_REFUSE)
.collect();
assert_eq!(refusals.len(), 1);
assert_eq!(
refusals[0].payload.as_ref().unwrap()["code"],
"key_mismatch"
);
let (_s3, other) = local_seed(identity("not-in-the-cloud", KEY)).await;
assert_eq!(
inv.cross_check(&list, &other).await.unwrap(),
FleetCheck::NotListed
);
let (_s4, nokey) = local_seed(identity("c0ffee00-0000-4000-8000-000000000003", KEY)).await;
assert_eq!(
inv.cross_check(&list, &nokey).await.unwrap(),
FleetCheck::KeyNotReported
);
}
struct Token;
impl OAuthTokens for Token {
fn access_token(&self) -> Result<SecretString, FleetError> {
Ok(SecretString::new(OAUTH.to_string()))
}
}
struct Mcp(Mutex<Vec<String>>);
impl Respond for Mcp {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body: Value = serde_json::from_slice(&req.body).unwrap();
let m = body["method"].as_str().unwrap_or("").to_string();
self.0.lock().unwrap().push(match m.as_str() {
"tools/call" => format!(
"tools/call {}",
body["params"]["name"].as_str().unwrap_or("")
),
other => other.to_string(),
});
match m.as_str() {
"initialize" => ResponseTemplate::new(200)
.insert_header("mcp-session-id", "sess-1")
.set_body_json(json!({"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-03-26", "capabilities": {}}})),
"notifications/initialized" => ResponseTemplate::new(202),
"tools/call" => {
assert_eq!(
req.headers
.get("mcp-session-id")
.map(|v| v.to_str().unwrap()),
Some("sess-1")
);
ResponseTemplate::new(200).set_body_raw(
format!(
"event: message\ndata: {}\n\n",
json!({"jsonrpc": "2.0", "id": 2, "result": {"content": [
{"type": "text", "text": FIXTURE}]}})
),
"text/event-stream",
)
}
_ => ResponseTemplate::new(400),
}
}
}
#[tokio::test]
async fn the_http_transport_speaks_mcp_with_the_bearer_and_only_calls_fleet_status() {
let server = MockServer::start().await;
let seen = Arc::new(Mcp(Mutex::new(Vec::new())));
let log = seen.clone();
struct Shared(Arc<Mcp>);
impl Respond for Shared {
fn respond(&self, r: &Request) -> ResponseTemplate {
self.0.respond(r)
}
}
Mock::given(method("POST"))
.and(header("authorization", format!("Bearer {OAUTH}").as_str()))
.respond_with(Shared(seen))
.mount(&server)
.await;
let t = HttpFleetMcp::new(&server.uri(), Box::new(Token)).unwrap();
let chain = Arc::new(ChainManager::new(0, 1000));
let inv = FleetInventory::new(
ReadOnlyFleet::new(Arc::new(t)),
chain.clone(),
"cognitum-oauth",
);
let list = inv.candidates(None).await.unwrap();
assert_eq!(list.len(), 3);
assert_eq!(
*log.0.lock().unwrap(),
[
"initialize",
"notifications/initialized",
"tools/call fleet_status"
]
);
let export = serde_json::to_string(&chain.tail(chain.len())).unwrap();
assert!(!export.contains(OAUTH), "OAuth token in the chain export");
}
#[tokio::test]
async fn a_rejected_credential_is_reported_without_the_token() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(401))
.mount(&server)
.await;
let t = HttpFleetMcp::new(&server.uri(), Box::new(Token)).unwrap();
let e = t.call_tool("fleet_status", json!({})).await.unwrap_err();
assert_eq!(e, FleetError::Unauthorized(401));
assert!(!e.to_string().contains(OAUTH));
let dead = HttpFleetMcp::new("http://127.0.0.1:1", Box::new(Token)).unwrap();
let e = dead.call_tool("fleet_status", json!({})).await.unwrap_err();
assert!(!e.to_string().contains(OAUTH), "{e}");
}
#[test]
fn the_transport_accepts_https_and_exact_loopback_only() {
for ok in [
"https://api.cognitum.one/v1/mcp",
"http://127.0.0.1:8080/mcp",
"http://localhost/mcp",
"http://[::1]:9/mcp",
] {
assert!(HttpFleetMcp::new(ok, Box::new(Token)).is_ok(), "{ok}");
}
for bad in [
"http://api.cognitum.one/v1/mcp",
"http://127.0.0.1.evil.com/mcp",
"http://localhost.evil.com/mcp",
"http://127.0.0.1@evil.com/mcp",
"http://user@127.0.0.1/mcp",
"ftp://127.0.0.1/mcp",
"https://",
"not a url",
] {
assert!(HttpFleetMcp::new(bad, Box::new(Token)).is_err(), "{bad}");
}
assert!(ReadOnlyFleet::http("http://127.0.0.1.evil.com/", Box::new(Token)).is_err());
}
#[tokio::test]
async fn an_oversize_reply_is_cut_off_at_the_cap() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200)
.set_body_raw(vec![b' '; 5 * 1024 * 1024], "application/json"),
)
.mount(&server)
.await;
let t = HttpFleetMcp::new(&server.uri(), Box::new(Token)).unwrap();
let e = t.call_tool("fleet_status", json!({})).await.unwrap_err();
assert!(e.to_string().contains("too large"), "{e}");
}
#[tokio::test]
async fn the_public_http_guard_refuses_a_write_tool() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.expect(0)
.mount(&server)
.await;
let g = ReadOnlyFleet::http(&server.uri(), Box::new(Token)).unwrap();
let e = g.call("device_register", json!({})).await.unwrap_err();
assert!(matches!(e, FleetError::NonReadTool(_)));
}