uitest/src/sitewelcome.rs
//! ★ **A site's first-login page greets its admin** (live 2026-10-14: the first admin of
//! test2.gunnar.rs — a SITE the operator's shop laid, nobody bought anything — was told
//! "Payment received — your account … is ready").
//!
//! The login page (`login.html`, as a metered console serves it) is served by a stub on
//! loopback and opened in headless Chrome with the ready mail's link:
//!
//! * `…&user=ignalina&kind=site` → "Welcome — register your security key for ignalina on
//! <host>." and no "Payment received";
//! * a buyer's link (no `kind`) keeps "Payment received — your account … is ready".
//!
//! Needs `GUNNAR_UI_CHROME` (a Chrome binary); without it each test says NOT RUN and returns.
#![cfg(test)]
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::time::Duration;
use crate::cdp::{Cdp, Chrome};
const PAGE: &str = include_str!("../../crates/gunnar-ui-server/src/auth/login.html");
/// The open line and what a metered build says instead (`auth::routes::login_html`).
const OPEN_READY_LINE: &str = "'Your account ' + who + ' is ready. Touch your security key to finish.'";
const PAID_READY_LINE: &str = "'Payment received — your account ' + who + ' is ready. Touch your security key to finish.'";
fn metered_page() -> String {
assert!(PAGE.contains(OPEN_READY_LINE), "login.html no longer carries the line a metered build replaces");
PAGE.replacen(OPEN_READY_LINE, PAID_READY_LINE, 1)
}
fn serve() -> u16 {
let l = TcpListener::bind("127.0.0.1:0").expect("a loopback port");
let port = l.local_addr().unwrap().port();
let page = metered_page();
std::thread::spawn(move || {
for conn in l.incoming().flatten() {
let page = page.clone();
std::thread::spawn(move || answer(conn, &page));
}
});
port
}
fn answer(mut conn: TcpStream, page: &str) {
let _ = conn.set_read_timeout(Some(Duration::from_secs(5)));
let mut r = BufReader::new(conn.try_clone().unwrap());
let mut line = String::new();
if r.read_line(&mut line).is_err() {
return;
}
let target = line.split_whitespace().nth(1).unwrap_or("/").to_owned();
loop {
let mut h = String::new();
if r.read_line(&mut h).is_err() || h.trim().is_empty() {
break;
}
}
let path = target.split('?').next().unwrap_or("/").to_owned();
let (code, kind, out): (u16, &str, String) = match path.as_str() {
"/login" => (200, "text/html; charset=utf-8", page.to_owned()),
"/auth/status" => (200, "application/json", r#"{"authenticated":false}"#.to_owned()),
"/login/repos" => (200, "application/json", "[]".to_owned()),
_ => (404, "text/plain", "no".to_owned()),
};
let head = format!("HTTP/1.1 {code} X\r\ncontent-type: {kind}\r\ncontent-length: {}\r\nconnection: close\r\n\r\n", out.len());
let _ = conn.write_all(head.as_bytes());
let _ = conn.write_all(out.as_bytes());
}
fn chrome() -> Option<String> {
let c = std::env::var("GUNNAR_UI_CHROME").ok().filter(|c| !c.is_empty());
if c.is_none() {
eprintln!("NOT RUN: set GUNNAR_UI_CHROME to a Chrome binary to open the site's first-login page");
}
c
}
/// The line the page shows above the register button for `query`.
fn ready_line(bin: &str, query: &str, tag: &str) -> String {
let port = serve();
let cdp_port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
let profile = std::env::temp_dir().join(format!("sitewelcome-{tag}-{}", std::process::id()));
let _chrome = Chrome::launch(bin, &profile, cdp_port).expect("chrome starts");
let mut c = Cdp::attach(cdp_port).expect("cdp");
let sess = c.page().expect("a page");
c.navigate(&sess, &format!("http://127.0.0.1:{port}/login?{query}")).expect("the page loads");
c.wait_for(&sess, "!document.getElementById('reg-who').hidden", 15_000).expect("the line is shown");
c.eval(&sess, "document.getElementById('reg-who').textContent").unwrap().as_str().unwrap_or("").to_owned()
}
#[test]
fn a_sites_first_admin_is_welcomed_and_never_told_of_a_payment() {
let Some(bin) = chrome() else { return };
let line = ready_line(&bin, "invite=00ff&user=ignalina&kind=site", "site");
assert_eq!(line, "Welcome — register your security key for ignalina on 127.0.0.1.");
}
#[test]
fn a_buyers_first_login_still_says_payment_received() {
let Some(bin) = chrome() else { return };
let line = ready_line(&bin, "invite=00ff&user=alice", "buyer");
assert_eq!(line, "Payment received — your account alice is ready. Touch your security key to finish.");
}