crates/gunnar-server/tests/lean_egress.rs
//! **The lean estate: a box behind its front leaves through the door it is fed** (owner
//! 2026-10-23, A19 rows 13-14). Main and the twin are laid with a utility address alone; the
//! shop feeds them the front's egress door (`egress`, Skidbladnir nordisk-pairing), and every
//! client of theirs that reaches the internet takes it (`nordisk_pairing::egress::proxy()`):
//! the identity provider's calls, the webhooks, the mirrors (`mirror_http`'s own test) and the
//! sign-in-again mail.
//!
//! One door for the whole binary (the fed door is process-wide, the first wins), on loopback,
//! recording each destination it was asked for and carrying it.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::{Mutex, OnceLock};
static SEEN: Mutex<Vec<(String, u16)>> = Mutex::new(Vec::new());
/// What the door was asked for (a failed test's panic must not poison it for the others).
fn seen() -> std::sync::MutexGuard<'static, Vec<(String, u16)>> {
SEEN.lock().unwrap_or_else(|e| e.into_inner())
}
fn record(host: &str, port: u16, resolved: &[SocketAddr]) -> Result<(), String> {
seen().push((host.to_string(), port));
if resolved.is_empty() {
Err("unresolved".into())
} else {
Ok(())
}
}
/// The door, served once and fed to this process as the shop feeds a lean box.
fn door() -> &'static str {
static DOOR: OnceLock<String> = OnceLock::new();
DOOR.get_or_init(|| {
let at = nordisk_pairing::egress::serve(
"127.0.0.1:0".parse().unwrap(),
nordisk_pairing::egress::Policy {
peer: |_| true,
admits: record,
},
)
.unwrap();
let url = format!("http://{at}");
let feed: BTreeMap<String, String> =
[(gunnar_server::feed::FEED_EGRESS.to_string(), url.clone())].into();
gunnar_server::feed::take_egress(&feed);
url
})
}
#[cfg(any(feature = "sso-egress", feature = "webhooks", feature = "fleet-pull"))]
fn went_through(port: u16) -> bool {
seen().iter().any(|(h, p)| h == "127.0.0.1" && *p == port)
}
/// A one-request-at-a-time HTTP server on loopback answering `200` with `body`.
#[cfg(any(feature = "sso-egress", feature = "webhooks"))]
fn http_200(body: &'static str) -> u16 {
use std::io::{Read as _, Write as _};
use std::net::TcpListener;
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
std::thread::spawn(move || {
for mut c in l.incoming().flatten() {
let mut head = Vec::new();
let mut b = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") && c.read(&mut b).map(|n| n == 1).unwrap_or(false) {
head.push(b[0]);
}
let _ = write!(c, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
}
});
port
}
/// ★ The door the box was fed is the one its clients take.
#[test]
fn the_fed_door_is_the_one_this_box_leaves_through() {
let d = door();
assert_eq!(nordisk_pairing::egress::proxy(), Some(d));
}
/// ★ **The identity provider's call leaves through the door** (A19 row 14): the SSO egress
/// resolves nothing itself then (the door names the host), and its answer comes back as ever.
#[cfg(feature = "sso-egress")]
#[test]
fn an_identity_providers_call_leaves_through_the_door() {
door();
let idp = http_200("{\"issuer\":\"x\"}");
let egress = gunnar_server::sso_egress::UreqEgress::new(true);
let (status, body) = gunnar_grpc::sso::Egress::get(
&egress,
&format!("http://127.0.0.1:{idp}/.well-known/openid-configuration"),
)
.expect("the IdP through the door");
assert_eq!((status, body.as_str()), (200, "{\"issuer\":\"x\"}"));
assert!(went_through(idp), "the door carried it: {:?}", seen());
}
/// ★ **A webhook delivery leaves through the door** (A19 row 14).
#[cfg(feature = "webhooks")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_webhook_delivery_leaves_through_the_door() {
use std::sync::Arc;
door();
let sink = http_200("");
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(gunnar_server::stores::ServerStores::open(dir.path()).expect("stores"));
let bus = Arc::new(gunnar_grpc::EventBus::new(64));
let slot = gunnar_server::webhook::WebhookSlot::new(
Arc::clone(&stores),
Arc::clone(&bus),
tokio::runtime::Handle::current(),
);
slot.add(gunnar_server::webhook::Endpoint {
name: "ci".into(),
url: format!("http://127.0.0.1:{sink}/hook"),
kinds: Vec::new(),
namespace: String::new(),
repo: None,
secret: None,
added_at_unix_ms: 0,
})
.expect("add endpoint");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
bus.publish(gunnar_proto::event::Payload::RepoCreated(
gunnar_proto::RepoCreated {
repo: Some(gunnar_proto::Repo {
path: Some(gunnar_proto::RepoPath {
namespace: "alice".into(),
repo: "edda".into(),
}),
..Default::default()
}),
principal_id: "p-1".into(),
},
));
for _ in 0..100 {
if went_through(sink) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
panic!("the delivery never reached the door: {:?}", seen());
}
/// A mail relay on loopback that greets, answers `EHLO` offering no STARTTLS, and says whether
/// a client reached it. Only the sign-in-again mail test (`fleet-pull`) speaks to it.
#[cfg(feature = "fleet-pull")]
fn relay_offering_no_starttls() -> (u16, std::sync::Arc<std::sync::atomic::AtomicBool>) {
use std::io::{Read as _, Write as _};
use std::net::TcpListener;
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let reached = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let r = reached.clone();
std::thread::spawn(move || {
for mut c in l.incoming().flatten() {
let _ = c.write_all(b"220 relay.mock.invalid ESMTP\r\n");
let mut line = Vec::new();
let mut b = [0u8; 1];
while !line.ends_with(b"\r\n") && c.read(&mut b).map(|n| n == 1).unwrap_or(false) {
line.push(b[0]);
}
if line.starts_with(b"EHLO") {
r.store(true, std::sync::atomic::Ordering::SeqCst);
}
let _ = c.write_all(b"250 relay.mock.invalid\r\n");
let _ = c.read(&mut [0u8; 64]);
}
});
(port, reached)
}
/// ★ **The sign-in-again mail of a box behind its front leaves through the door** (A19 row 13,
/// the SMTP gap): lettre speaks no proxy, so the postman dials a loopback forward
/// (`nordisk_pairing::egress::Forward`) carried through the front's door as `CONNECT
/// relay:port`, and names the RELAY in its TLS. The relay here offers no STARTTLS, so the mail
/// is refused, never sent in the clear: what is proven is the road, and that TLS is still
/// required on it.
#[cfg(feature = "fleet-pull")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_sign_in_again_mail_leaves_through_the_door() {
use gunnar_server::postman::Postman as _;
door();
let (relay, reached) = relay_offering_no_starttls();
let smtp = gunnar_server::postman::Smtp::from_url(
&format!("smtp://u:tok@127.0.0.1:{relay}"),
"gunnar@mock.invalid",
)
.unwrap();
let mail = gunnar_repair::sign_in::SignInMail {
to: "buyer@mock.invalid".into(),
subject: "sign in".into(),
body: "a link".into(),
};
let sent = smtp.post(&mail).await;
assert!(
sent.is_err(),
"no STARTTLS on offer: refused, never in the clear"
);
assert!(
went_through(relay),
"the door carried the relay's connection: {:?}",
seen()
);
assert!(
reached.load(std::sync::atomic::Ordering::SeqCst),
"the relay was reached and greeted"
);
}