Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

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.");
}