//! holger-server-ui **Sign-in pane** robot cell — proves the mounted shared
//! `facett_auth_ui::LoginPane` is driven by the REAL
//! [`holger_server_ui::login_seam::HolgerLoginSeam`] over the shared `nornir-auth-client`
//! device-code grant, end to end, WITHOUT a real IdP and WITHOUT ever leaking a
//! secret value into the observable `state_json`.
//!
//! Gated behind `gui` (the pane + seam only compile there). Run:
//! `cargo test -p holger-server-ui --features gui --test robot_login_pane`.
//!
//! LAW #5 (one shared pane, no per-app twin) + the pane's RED-when-broken security
//! invariant: the seam exposes no token/secret VALUE, so a sentinel access token can
//! never reach the pane or its state — this test asserts it never does.
#![cfg(feature = "gui")]
use holger_server_ui::facett_auth_ui::{Facet, LoginPane};
use holger_server_ui::login_seam::{subject_of, HolgerLoginSeam};
use nornir_auth_client::{FileTokenStore, LoginConfig};
/// A JWT with the given JSON payload (unsigned `header.payload.sig` — display-only,
/// the same shape the IdP would mint; the server verifies the signature on presentation).
fn jwt(payload: serde_json::Value) -> String {
use base64::Engine;
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
format!("{}.{}.sig", b64(br#"{"alg":"none"}"#), b64(payload.to_string().as_bytes()))
}
/// **The REAL `HolgerLoginSeam` drives the shared `LoginPane` SignedOut → AwaitingUser
/// → SignedIn over a LOCAL stub IdP (no real network), and NO secret value ever appears
/// in the pane's `state_json`.** A tiny loopback HTTP server plays the RFC 8628
/// device-authorization + token endpoints; the seam runs the real device-code grant
/// against it through `nornir-auth-client`, and the presentational pane renders only the
/// states the seam surfaces. Drives deterministically with `auto_poll(false)`.
#[test]
fn holger_seam_drives_the_shared_login_pane_no_leak() {
// The IdP mints THIS access token — a sentinel we then prove never leaks.
let access_token = jwt(serde_json::json!({ "preferred_username": "robot@vetra.se" }));
// Sanity: the seam's display decoder recovers the identity from it.
assert_eq!(subject_of(&access_token).as_deref(), Some("robot@vetra.se"));
// A multi-thread runtime serves the stub IdP on its background workers while the
// main thread drives the (sync) seam — whose own `block_on` runs the real grant.
// The server is IO (a mock HTTP endpoint), NOT CPU parallelism (ROOT LAW #0).
let server_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
let addr = server_rt.block_on(stub_idp::spawn(access_token.clone()));
let dir = tempfile::tempdir().unwrap();
let store = FileTokenStore::at(dir.path().join("auth-token.json"));
let cfg = LoginConfig {
device_auth_url: format!("http://{addr}/device"),
token_url: format!("http://{addr}/token"),
client_id: "holger-server-ui".into(),
scope: "openid offline_access".into(),
};
let seam = HolgerLoginSeam::with_config_and_store(cfg, store);
let mut pane = LoginPane::new(Box::new(seam)).with_auto_poll(false);
// SignedOut — a fresh seam over an empty store opens signed-out.
assert_eq!(pane.state_json()["state"], "signed_out");
// Begin → AwaitingUser: the device-code prompt (user-code + verification URL) shows.
pane.update_json(r#"{"begin":true}"#);
let s = pane.state_json();
assert_eq!(s["state"], "awaiting_user", "begin → the device-code prompt: {s}");
assert_eq!(s["user_code"], "WDJB-MJHT");
assert!(s["verification_uri"].as_str().unwrap().contains("/device"));
// Poll → SignedIn: the subject shows; credential NAMES only (no values).
pane.update_json(r#"{"poll":true}"#);
let s = pane.state_json();
assert_eq!(s["state"], "signed_in", "poll → signed in: {s}");
assert_eq!(s["subject"], "robot@vetra.se");
let names = s["credential_names"].as_array().unwrap();
assert!(names.iter().any(|n| n == "access_token"), "credential NAMES surface: {s}");
assert!(names.iter().any(|n| n == "refresh_token"), "offline_access ⇒ refresh_token name");
// NO secret value ANYWHERE — not the sentinel token, not an access_token/value/token key.
let whole = serde_json::to_string(&s).unwrap();
assert!(!whole.contains(&access_token), "the sentinel access token never reaches state: {whole}");
assert!(!whole.contains("eyJ"), "no JWT bytes leak into the state: {whole}");
assert!(s.as_object().unwrap().get("value").is_none());
assert!(s.as_object().unwrap().get("token").is_none());
// Sign out → back to signed-out (the store is cleared; nothing lingers).
pane.update_json(r#"{"sign_out":true}"#);
assert_eq!(pane.state_json()["state"], "signed_out");
}
/// **RED-when-broken: the pane is a REAL egui surface (non-blank render).** Drive the
/// mounted `LoginPane`'s body through the headless kittest harness in the signed-out
/// state and assert its *Sign in* button is in the AccessKit tree — the LAW #4 render
/// proof that the shared pane actually paints when holger mounts it (not just state).
#[test]
fn login_pane_renders_a_signin_button() {
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
// A fresh seam over an empty temp store ⇒ signed-out ⇒ the pane shows *Sign in*.
let dir = tempfile::tempdir().unwrap();
let store = FileTokenStore::at(dir.path().join("auth-token.json"));
let seam = HolgerLoginSeam::with_config_and_store(
LoginConfig {
device_auth_url: "http://127.0.0.1:1/device".into(),
token_url: "http://127.0.0.1:1/token".into(),
client_id: "holger-server-ui".into(),
scope: "openid".into(),
},
store,
);
// Distinct heading so the render query matches only the *Sign in* BUTTON (the
// default title would also be "Sign in", colliding with the button label).
let pane = std::rc::Rc::new(std::cell::RefCell::new(
LoginPane::new(Box::new(seam)).with_auto_poll(false).with_title("Sign in to holger"),
));
let p = pane.clone();
let mut h = Harness::builder()
.with_size(egui::vec2(420.0, 320.0))
.build_ui(move |ui| p.borrow_mut().ui(ui));
h.run();
assert!(h.query_by_label("Sign in").is_some(), "the signed-out pane paints a Sign in button");
assert_eq!(pane.borrow().state_json()["state"], "signed_out");
}
/// A minimal localhost stub of the RFC 8628 IdP endpoints for the sign-in test — a raw
/// `tokio` TCP HTTP/1.1 server answering `/device` (device-authorization) and `/token`
/// (immediate approval). No real network; bound to `127.0.0.1:0`. Mirrors korp's
/// `src/login.rs` stub (the shared reuse pattern). The `tokio::spawn`s here run IO (a mock
/// HTTP server), NOT CPU work — ROOT LAW #0 targets compute parallelism, not IO servers.
mod stub_idp {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
/// Bind on an ephemeral loopback port, serve the two endpoints on a background task,
/// and return the bound `addr`.
pub async fn spawn(access_token: String) -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let device_body = format!(
r#"{{"device_code":"dc-123","user_code":"WDJB-MJHT","verification_uri":"http://{addr}/device","interval":1,"expires_in":600}}"#
);
let token_body = format!(
r#"{{"access_token":"{access_token}","refresh_token":"rt-456","expires_in":3600,"token_type":"Bearer"}}"#
);
tokio::spawn(async move {
loop {
let Ok((mut sock, _)) = listener.accept().await else { break };
let device_body = device_body.clone();
let token_body = token_body.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
let n = sock.read(&mut buf).await.unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]);
let path = req.split_whitespace().nth(1).unwrap_or("/");
let body = if path.starts_with("/token") { &token_body } else { &device_body };
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes()).await;
let _ = sock.shutdown().await;
});
}
});
addr
}
}