crates/gunnar-cli/tests/principal_identity_plane.rs
#![cfg(all(feature = "server", feature = "control"))]
//! **A principal's id is not its name — on the REAL plane** (2026-09-12).
//!
//! `gunnar-core`'s `principal_identity` proves the table; `gunnar-grpc`'s
//! services test proves the RPC shape on the in-memory double. This proves
//! the row the shipped `CoreControlPlane` writes: the account namespace is
//! created under the ID, not the name; the name filter and the rename reach
//! the real identity table; and the refusals come back as the codes a client
//! is documented to expect.
mod common;
use std::sync::Arc;
use common::Harness;
use gunnar_cli::server::CoreControlPlane;
use gunnar_core::{HashKind, Level};
use gunnar_grpc::{AuthMode, Caller, ControlPlane, EventBus, ServerConfig, ServiceContext};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::{
AddPrincipalRequest, ListPrincipalsRequest, PrincipalKind, RenamePrincipalRequest,
};
use tonic::{Code, Request};
fn now<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime")
.block_on(future)
}
fn as_admin<T>(message: T) -> Request<T> {
let mut request = Request::new(message);
request.extensions_mut().insert(Caller {
principal_id: "root".to_owned(),
fingerprint: String::new(),
machine: false,
authenticated: true,
});
request
}
fn access(plane: &Arc<CoreControlPlane>) -> gunnar_grpc::AccessService {
let control: Arc<dyn ControlPlane> = plane.clone();
gunnar_grpc::AccessService::new(ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig {
auth: AuthMode::Required,
..ServerConfig::default()
},
))
}
#[test]
fn the_account_is_the_id_and_the_name_is_free_to_move() {
let harness = Harness::start("team", "proj", HashKind::Sha1);
let auth = harness.stores.auth();
auth.add_principal(
gunnar_core::Principal::new("root", "root", gunnar_core::PrincipalKind::User).unwrap(),
)
.expect("root");
auth.grant(gunnar_core::Grant {
principal: "root".into(),
scope: gunnar_core::Scope::Server,
level: Level::Admin,
})
.expect("root is admin");
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&harness.stores)));
let access = access(&plane);
let bob = now(access.add_principal(as_admin(AddPrincipalRequest {
name: "Bob Builder".into(),
kind: PrincipalKind::User as i32,
id: "bob".into(),
})))
.expect("created")
.into_inner()
.principal
.unwrap();
assert_eq!(bob.id, "bob");
assert_eq!(bob.name, "Bob Builder");
assert!(
bob.created_at_unix_ms > 0,
"the real plane must report the stamp"
);
// The namespace is the ID's. Applied output: the catalog, and the ACL.
assert!(
harness
.stores
.catalog()
.namespace("bob")
.expect("read")
.is_some(),
"the account namespace must be created under the id"
);
assert!(
harness
.stores
.catalog()
.namespace("Bob Builder")
.expect("read")
.is_none(),
"nothing may be created under the display name"
);
assert!(
auth.may_namespace("bob", "bob", Level::Admin).expect("acl"),
"the principal owns the namespace named after its id"
);
// The name filter reaches the real index.
let found = now(access.list_principals(as_admin(ListPrincipalsRequest {
name: "Bob Builder".into(),
..Default::default()
})))
.expect("listed")
.into_inner()
.principals;
assert_eq!(found.len(), 1);
assert_eq!(found[0].id, "bob");
// RED first: the name is taken (as a name), and the id is taken.
let status = now(access.add_principal(as_admin(AddPrincipalRequest {
name: "Bob Builder".into(),
kind: PrincipalKind::User as i32,
id: "bob2".into(),
})))
.expect_err("a taken name is refused by the real table");
assert_eq!(status.code(), Code::AlreadyExists, "{status}");
assert!(
harness
.stores
.catalog()
.namespace("bob2")
.expect("read")
.is_none(),
"a refused principal must not leave a namespace behind"
);
// Rename on the real plane: label moves, account and grant stay.
let renamed = now(access.rename_principal(as_admin(RenamePrincipalRequest {
principal_id: "bob".into(),
name: "Robert".into(),
})))
.expect("renamed")
.into_inner()
.principal
.unwrap();
assert_eq!(renamed.id, "bob");
assert_eq!(renamed.name, "Robert");
assert_eq!(renamed.created_at_unix_ms, bob.created_at_unix_ms);
assert!(auth.may_namespace("bob", "bob", Level::Admin).expect("acl"));
assert_eq!(
auth.principal_by_name("Robert")
.expect("read")
.map(|p| p.id),
Some("bob".to_owned())
);
assert!(auth
.principal_by_name("Bob Builder")
.expect("read")
.is_none());
}