crates/gunnar-grpc/tests/topology.rs
//! **`Admin.Topology` over the service seam** (2026-10-04): a solo gunnar is
//! one box; the console that asks is the second; a twin that polls is the
//! third — counted by the SERVER, so the console only draws what it is told.
//!
//! Red-first: with the handler's caller-log mark removed, the second row has
//! no console; with `twin_rows()` left out of the input, the third has no twin.
use std::net::SocketAddr;
use std::sync::Arc;
use gunnar_grpc::{
AdminService, Caller, ControlPlane, EventBus, MemoryControlPlane, ServerConfig, ServiceContext,
};
use gunnar_proto::admin_server::Admin as _;
use gunnar_proto::*;
use tonic::transport::server::TcpConnectInfo;
use tonic::{Code, Request};
struct Rig {
plane: Arc<MemoryControlPlane>,
ctx: ServiceContext,
}
impl Rig {
fn new() -> Rig {
let plane = Arc::new(MemoryControlPlane::new());
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(64)),
ServerConfig::default(),
);
Rig { plane, ctx }
}
fn principal(&self, name: &str, kind: PrincipalKind, level: Option<GrantLevel>) -> Caller {
let p = self
.plane
.add_principal(name, name, kind)
.expect("principal added");
if let Some(level) = level {
self.plane.grant_server(&p.id, level);
}
Caller {
principal_id: p.id,
fingerprint: format!("SHA256:{name}"),
machine: kind == PrincipalKind::Machine,
authenticated: true,
}
}
/// A request as the gate hands it on: the verified caller, and the peer
/// the connection came from.
fn from<T>(&self, caller: &Caller, peer: &str, message: T) -> Request<T> {
let mut request = Request::new(message);
request.extensions_mut().insert(caller.clone());
let remote: SocketAddr = peer.parse().expect("peer");
request.extensions_mut().insert(TcpConnectInfo {
local_addr: None,
remote_addr: Some(remote),
});
request
}
fn admin(&self) -> AdminService {
AdminService::new(self.ctx.clone())
}
}
fn roles(r: &TopologyResponse) -> Vec<(String, String)> {
r.boxes
.iter()
.map(|b| (b.role.clone(), b.state.clone()))
.collect()
}
#[tokio::test]
async fn solo_then_a_console_then_a_twin_one_box_each() {
let rig = Rig::new();
let admin = rig.admin();
// The operator's laptop asks: a PERSON is not a box. One box.
let operator = rig.principal("operator", PrincipalKind::User, Some(GrantLevel::Admin));
let r = admin
.topology(rig.from(&operator, "192.0.2.10:55000", TopologyRequest {}))
.await
.expect("topology")
.into_inner();
assert_eq!(
roles(&r),
[("primary".into(), "up".into())],
"a solo gunnar is one box"
);
assert!(r.boxes[0].this_box);
// The console asks — it is a machine, and the picture it gets has it on it.
let console = rig.principal("gunnar-ui", PrincipalKind::Machine, Some(GrantLevel::Admin));
let r = admin
.topology(rig.from(&console, "127.0.0.1:41234", TopologyRequest {}))
.await
.expect("topology")
.into_inner();
assert_eq!(
roles(&r),
[
("primary".into(), "up".into()),
("console".into(), "up".into())
],
"front + server = 2"
);
assert_eq!(r.boxes[1].name, "gunnar-ui");
assert_eq!(r.boxes[1].address, "127.0.0.1:41234");
// A twin polls (the primary half of the heartbeat records it).
let report = TwinReport {
applied_seq: 7,
disk_total_bytes: 10 << 30,
disk_used_bytes: 1 << 30,
state: "following".into(),
..TwinReport::default()
};
rig.ctx.heartbeats.record(
"gunnar-twin",
"10.13.8.122:5222",
&report,
7,
estate_clock::now_unix_ms(),
);
let r = admin
.topology(rig.from(&console, "127.0.0.1:41234", TopologyRequest {}))
.await
.expect("topology")
.into_inner();
assert_eq!(
roles(&r),
[
("primary".into(), "up".into()),
("twin".into(), "up".into()),
("console".into(), "up".into())
],
"+ twin = 3"
);
let twin = &r.boxes[1];
assert!(
twin.disk_measured && twin.disk_total_bytes == 10 << 30,
"{twin:?}"
);
assert!(twin.sources.iter().any(|s| s == "heartbeat"));
}
/// Any machine's ordinary call is recorded (it is the evidence a front key is
/// up), but only a machine that ASKED for the picture is drawn as a console.
#[tokio::test]
async fn an_ordinary_machine_call_is_recorded_but_is_not_a_console() {
let rig = Rig::new();
let builder = rig.principal(
"dwarves-oden",
PrincipalKind::Machine,
Some(GrantLevel::Read),
);
rig.admin()
.stats(rig.from(&builder, "10.0.0.9:5000", StatsRequest {}))
.await
.expect("stats");
let seen = rig.ctx.callers.sightings();
assert_eq!(seen.len(), 1);
assert_eq!(
(seen[0].address.as_str(), seen[0].asked_topology),
("10.0.0.9:5000", false)
);
let operator = rig.principal("operator", PrincipalKind::User, Some(GrantLevel::Admin));
let r = rig
.admin()
.topology(rig.from(&operator, "192.0.2.10:1", TopologyRequest {}))
.await
.unwrap()
.into_inner();
assert_eq!(
r.boxes.len(),
1,
"a builder's key is not a box of the estate: {:?}",
r.boxes
);
}
/// A credential with no grant is refused: the topology is a server-wide read.
#[tokio::test]
async fn a_caller_with_no_server_read_is_refused() {
let rig = Rig::new();
let nobody = rig.principal("nobody", PrincipalKind::Machine, None);
let err = rig
.admin()
.topology(rig.from(&nobody, "10.0.0.1:1", TopologyRequest {}))
.await
.expect_err("refused");
assert_eq!(err.code(), Code::PermissionDenied);
}