Gunnar

nordisk/gunnar-ui

uitest/src/lfsfileview.rs

//! ★ **A file kept in Git LFS is shown as the file, in a real browser** (owner
//! 2026-10-15: on gunnar.rs, Code → mikronova/modgunn `docs/idiot_guide.pdf`
//! read "131 B · Plain Text · version https://git-lfs.github.com/spec/v1 …") —
//! the BUILT console (`trunk build`'s `dist`) behind a loopback stub that
//! answers the BFF's JSON the way the server now does for an LFS pointer,
//! driven by headless Chrome over the DevTools Protocol.
//!
//! * A PDF: its real size, "PDF", "stored in Git LFS"; Open and Download point
//!   at the raw route; the pointer text is not on the page except inside the
//!   closed "LFS pointer" detail.
//! * A picture: drawn inline from the raw route, and actually decoded.
//! * Blame of an LFS file says it is stored in LFS and draws no pointer lines.
//!
//! Needs `GUNNAR_UI_CHROME` (a Chrome binary) and `GUNNAR_UI_DIST` (the built
//! console's `dist`); without both each test says NOT RUN and returns.

#![cfg(test)]

use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::time::Duration;

use crate::cdp::{Cdp, Chrome};

const STATUS: &str = r#"{"authenticated":true,"user":"rickard","role":"admin","passkey_enabled":true,"open":false,"account":"rickard","account_role":"admin"}"#;
const HEALTH: &str = r#"{"serving":true,"agent":"gunnar","version":"1.5.0","build_ref":"v1.4.0","uptime_seconds":60,"grpc_authentication_enabled":true,"grpc_bind":"127.0.0.1:1","ssh_bind":"127.0.0.1:2"}"#;
const REPO: &str = r#"{"path":"acme/app","namespace":"acme","name":"app","hash_kind":"sha256","default_branch":"refs/heads/main","visibility":"public","created_at_unix_ms":1760000000000,"description":"","encrypted":false}"#;
const REFS: &str = r#"{"items":[{"ref_name":"refs/heads/main","oid":"2222222222222222222222222222222222222222","symbolic_target":null,"peeled_oid":null}],"total":1,"truncated":false}"#;
const POINTER: &str = "version https://git-lfs.github.com/spec/v1\noid sha256:fdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfd\nsize 189950\n";

/// A real 1×1 PNG, so "decoded" is a fact about pixels and not a status code.
const PNG_1PX: &[u8] = &[
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
    0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0,
    0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, 0x3d, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
    0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];

/// The BFF's answer for an LFS pointer, as `api::browse::read_lfs_blob` writes it.
fn lfs_blob(path: &str, size: u64, shown: &str) -> String {
    let pointer = POINTER.replace('\n', "\\n");
    format!(
        r#"{{"kind":"blob","revision":{{"commit_oid":"2222222222222222222222222222222222222222","tree_oid":"t","subject":"docs","author":"rickard","at_unix_seconds":1760000000}},"path":"{path}","crumbs":[{{"name":"{path}","path":"{path}"}}],"oid":"cc00000000000000000000000000000000000033","size":{size},"truncated":false,"too_large":false,"binary":true,"syntax":null,"lines":[],"sealed":false,"lfs":{{"sha256":"fdfd","shown":"{shown}","pointer":"{pointer}"}}}}"#
    )
}

fn query_path(target: &str) -> String {
    target
        .split_once('?')
        .map(|(_, q)| q)
        .unwrap_or("")
        .split('&')
        .find_map(|kv| kv.strip_prefix("path="))
        .unwrap_or("")
        .replace("%2F", "/")
}

fn mime(path: &Path) -> &'static str {
    match path.extension().and_then(|e| e.to_str()) {
        Some("html") => "text/html; charset=utf-8",
        Some("js") => "text/javascript",
        Some("wasm") => "application/wasm",
        Some("css") => "text/css",
        Some("svg") => "image/svg+xml",
        _ => "application/octet-stream",
    }
}

fn answer(mut conn: TcpStream, dist: &Path) {
    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 mut parts = line.split_whitespace();
    let (method, target) = (
        parts.next().unwrap_or("").to_owned(),
        parts.next().unwrap_or("/").to_owned(),
    );
    let mut len = 0usize;
    loop {
        let mut h = String::new();
        if r.read_line(&mut h).is_err() || h.trim().is_empty() {
            break;
        }
        if let Some(v) = h.to_ascii_lowercase().strip_prefix("content-length:") {
            len = v.trim().parse().unwrap_or(0);
        }
    }
    let mut body = vec![0u8; len];
    let _ = r.read_exact(&mut body);
    let path = target.split('?').next().unwrap_or("/").to_owned();
    let file = query_path(&target);
    let json = |s: &str| (200u16, "application/json", s.as_bytes().to_vec());
    let (code, kind, out): (u16, &str, Vec<u8>) = match (method.as_str(), path.as_str()) {
        ("GET", "/auth/status") => json(STATUS),
        ("GET", "/api/health") => json(HEALTH),
        ("GET", "/api/repos/acme/app") => json(REPO),
        ("GET", "/api/repos/acme/app/refs") => json(REFS),
        ("GET", "/api/repos/acme/app/browse") if file.ends_with(".png") => {
            json(&lfs_blob(&file, 70, "image"))
        }
        ("GET", "/api/repos/acme/app/browse") => json(&lfs_blob(&file, 189_950, "pdf")),
        ("GET", "/api/repos/acme/app/raw") if file.ends_with(".png") => {
            (200, "image/png", PNG_1PX.to_vec())
        }
        ("GET", "/api/repos/acme/app/raw") => {
            (200, "application/pdf", b"%PDF-1.7\n%%EOF\n".to_vec())
        }
        ("GET", "/api/repos/acme/app/blame") => json(&format!(
            r#"{{"rev":"","path":"{file}","lines":[],"commits":["{}"],"verified":0,"truncated":false,"lfs":true}}"#,
            "a".repeat(40)
        )),
        ("GET", p) if p.starts_with("/api/") || p.starts_with("/auth/") => (
            404,
            "application/json",
            br#"{"code":"notfound","message":"not in this stub"}"#.to_vec(),
        ),
        ("GET", p) => {
            let f = dist.join(p.trim_start_matches('/'));
            let f = if p != "/" && f.is_file() {
                f
            } else {
                dist.join("index.html")
            };
            match std::fs::read(&f) {
                Ok(bytes) => (200, mime(&f), bytes),
                Err(_) => (404, "text/plain", b"no".to_vec()),
            }
        }
        _ => (404, "text/plain", b"no".to_vec()),
    };
    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);
}

fn serve(dist: PathBuf) -> u16 {
    let l = TcpListener::bind("127.0.0.1:0").expect("a loopback port");
    let port = l.local_addr().unwrap().port();
    std::thread::spawn(move || {
        for conn in l.incoming().flatten() {
            let dist = dist.clone();
            std::thread::spawn(move || answer(conn, &dist));
        }
    });
    port
}

/// The browser on `page` of acme/app once `ready` holds, or NOT RUN.
fn rig(tag: &str, page: &str, ready: &str) -> Option<(Chrome, Cdp, String)> {
    let (Some(bin), Some(dist)) = (
        std::env::var("GUNNAR_UI_CHROME")
            .ok()
            .filter(|v| !v.is_empty()),
        std::env::var("GUNNAR_UI_DIST")
            .ok()
            .filter(|v| !v.is_empty()),
    ) else {
        eprintln!("NOT RUN: set GUNNAR_UI_CHROME (a Chrome binary) and GUNNAR_UI_DIST (the console's built dist)");
        return None;
    };
    assert!(
        Path::new(&dist).join("index.html").is_file(),
        "{dist} holds no built console"
    );
    let port = serve(PathBuf::from(dist));
    let debug = TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port();
    let profile = std::env::temp_dir().join(format!("lfsfileview-{tag}-{}", std::process::id()));
    let chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut cdp = Cdp::attach(debug).expect("cdp");
    let sess = cdp.page().expect("a page");
    cdp.navigate(&sess, &format!("http://127.0.0.1:{port}{page}"))
        .expect("the console loads");
    if let Err(e) = cdp.wait_for(&sess, ready, 30_000) {
        let text = cdp
            .eval_str(&sess, "document.body.innerText")
            .unwrap_or_default();
        panic!(
            "{page} never drew what was waited for ({e}): {}",
            crate::http::cut(&text, 800)
        );
    }
    Some((chrome, cdp, sess))
}

/// The page's visible text, the closed pointer detail left out.
fn visible(c: &mut Cdp, sess: &str) -> String {
    c.eval_str(
        sess,
        "(() => { const d = document.querySelector('details.lfs-pointer'); const open = d && d.open; \
         const body = document.body.innerText; return open ? '' : body; })()",
    )
    .unwrap()
}

#[test]
fn an_lfs_pdf_is_shown_as_the_pdf_and_never_as_its_pointer() {
    let Some((_chrome, mut c, sess)) = rig(
        "pdf",
        "/repo/acme/app/files?path=docs/idiot_guide.pdf",
        "!!document.querySelector('.lfs-head')",
    ) else {
        return;
    };
    let head = c
        .eval_str(&sess, "document.querySelector('.lfs-head').innerText")
        .unwrap();
    assert!(head.contains("185.5 KiB"), "the real size: {head}");
    assert!(head.contains("PDF"), "{head}");
    assert!(head.contains("stored in Git LFS"), "{head}");
    assert!(
        !head.contains("131 B") && !head.contains("Plain Text"),
        "{head}"
    );
    let open = c
        .eval_str(
            &sess,
            "document.querySelector('a.lfs-open').getAttribute('href')",
        )
        .unwrap();
    assert_eq!(open, "/api/repos/acme/app/raw?path=docs/idiot_guide.pdf");
    let download = c
        .eval_str(
            &sess,
            "document.querySelector('a.lfs-download').getAttribute('href')",
        )
        .unwrap();
    assert_eq!(download, open);
    let text = visible(&mut c, &sess);
    assert!(
        !text.contains("version https://git-lfs"),
        "the pointer is shown as the file: {}",
        crate::http::cut(&text, 600)
    );
    assert!(
        text.contains("LFS pointer"),
        "the pointer detail is offered"
    );
}

#[test]
fn an_lfs_picture_is_drawn_inline_from_the_raw_route() {
    let Some((_chrome, mut c, sess)) = rig(
        "png",
        "/repo/acme/app/files?path=assets/pic.png",
        "(() => { const i = document.querySelector('img.lfs-image'); return !!i && i.complete && i.naturalWidth > 0; })()",
    ) else {
        return;
    };
    let src = c
        .eval_str(
            &sess,
            "document.querySelector('img.lfs-image').getAttribute('src')",
        )
        .unwrap();
    assert_eq!(src, "/api/repos/acme/app/raw?path=assets/pic.png");
    let head = c
        .eval_str(&sess, "document.querySelector('.lfs-head').innerText")
        .unwrap();
    assert!(
        head.contains("PNG image") && head.contains("stored in Git LFS"),
        "{head}"
    );
    assert!(!visible(&mut c, &sess).contains("version https://git-lfs"));
}

#[test]
fn blame_of_an_lfs_file_says_it_is_in_lfs_and_blames_no_pointer_lines() {
    let Some((_chrome, mut c, sess)) = rig(
        "blame",
        "/repo/acme/app/blame?path=docs/idiot_guide.pdf",
        "!!document.querySelector('[data-blame-lfs]')",
    ) else {
        return;
    };
    let said = c
        .eval_str(
            &sess,
            "document.querySelector('[data-blame-lfs]').innerText",
        )
        .unwrap();
    assert!(said.contains("stored in Git LFS"), "{said}");
    let rows = c
        .eval(
            &sess,
            "document.querySelectorAll('tr[data-blame-line]').length",
        )
        .unwrap()
        .as_f64()
        .unwrap();
    assert_eq!(rows, 0.0, "pointer lines were blamed");
}