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

nordisk/gunnar-ui

crates/gunnar-public-site/tests/shell.rs

//! ★ **GUN-GUEST: the console's document with a page's content in it** ([`shell::compose`]),
//! and the cache of pre-rendered parts: dropped when the repository changes, when it is
//! older than the slow refresh, and bounded.

mod common;
use common::*;

use std::time::Instant;

use gunnar_public_site::head::Meta;
use gunnar_public_site::shell::{self, Part};

const INDEX: &str = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Gunnar</title>\n<script type=\"module\">import init from '/gunnar-ui-web-1.js'; init();</script>\n</head>\n<body class=\"x\"></body>\n</html>\n";

fn meta(title: &str, path: &str) -> Meta {
    Meta { title: title.into(), description: "The git server itself.".into(), path: path.into(), og_type: "article", json_ld: serde_json::json!({"@type": "SoftwareSourceCode"}), noindex: false }
}

#[test]
fn the_document_is_the_consoles_with_the_pages_title_tags_and_content() {
    let c = config();
    let doc = shell::compose(&c, INDEX, &meta("nordisk/gunnar · Gunnar", "/nordisk/gunnar"), "<h1>nordisk/gunnar</h1>");
    assert_eq!(doc.matches("<title>").count(), 1, "{doc}");
    assert!(doc.contains("<title>nordisk/gunnar · Gunnar</title>"), "{doc}");
    let head = &doc[..doc.find("</head>").unwrap()];
    for tag in ["<link rel=\"canonical\" href=\"https://gunnar.example/nordisk/gunnar\">", "<meta name=\"description\"", "<meta property=\"og:title\"", "<script type=\"application/ld+json\">"] {
        assert!(head.contains(tag), "{tag}: {head}");
    }
    assert!(doc.contains("import init from '/gunnar-ui-web-1.js'"), "the console's bootstrap is kept");
    assert!(doc.contains("<body class=\"x\"><main id=\"prerender\">\n<h1>nordisk/gunnar</h1>\n</main>"), "the part is first in the body: {doc}");
}

/// The console's real head names `<body>` in a script's comment (its no-WebAssembly notice):
/// the part goes into the real body, and the head's scripts are left byte for byte.
#[test]
fn the_part_never_lands_inside_the_heads_own_script() {
    let index = "<!doctype html><html><head><title>Gunnar</title><script>// there is no <body> yet here\nif (!document.body) {}</script><style>.x{}</style></head><body></body></html>";
    let doc = shell::compose(&config(), index, &meta("t", "/a/b"), "<p>part</p>");
    assert!(doc.contains("<script>// there is no <body> yet here\nif (!document.body) {}</script>"), "the head's script changed: {doc}");
    assert!(doc.contains("</head><body><main id=\"prerender\">\n<p>part</p>\n</main>"), "{doc}");
}

#[test]
fn a_console_with_no_bundle_still_answers_a_whole_document() {
    let doc = shell::compose(&config(), shell::BARE_INDEX, &meta("t", "/a/b"), "<p>x</p>");
    assert!(doc.starts_with("<!doctype html>") && doc.contains("<main id=\"prerender\">") && doc.ends_with("</html>\n"), "{doc}");
}

#[test]
fn a_hostile_title_cannot_break_out_of_the_head() {
    let doc = shell::compose(&config(), INDEX, &meta("</title><script>alert(1)</script>", "/a/b"), "");
    assert!(!doc.contains("<script>alert(1)"), "{doc}");
}

#[tokio::test]
async fn a_change_drops_the_repositorys_parts_and_the_refresh_drops_old_ones() {
    let (site, _) = site().await;
    let part = || Part { status: 200, title: "t".into(), description: "d".into(), html: "<p>x</p>".into() };
    site.prerendered().put(&format!("/{NS}/{PUBLIC}"), &format!("{NS}/{PUBLIC}"), part(), Instant::now());
    site.prerendered().put("/other/repo", "other/repo", part(), Instant::now());
    site.changed(&format!("{NS}/{PUBLIC}")).await.unwrap();
    assert!(site.prerendered().get(&format!("/{NS}/{PUBLIC}")).is_none(), "the changed repository's part is dropped");
    assert!(site.prerendered().get("/other/repo").is_some(), "another repository's is kept");
    assert!(site.publishes(NS, PUBLIC).is_some() && site.publishes(NS, PRIVATE).is_none(), "only a public repository is published");
}

#[tokio::test]
async fn a_moved_head_drops_the_repositorys_parts_on_the_refresh() {
    let fake = std::sync::Arc::new(Fake::new());
    struct Shared(std::sync::Arc<Fake>);
    impl gunnar_public_site::Source for Shared {
        fn public_repos(&self) -> gunnar_public_site::BoxFuture<'_, Result<Vec<gunnar_public_site::RepoRef>, String>> {
            self.0.public_repos()
        }
        fn head<'a>(&'a self, r: &'a gunnar_public_site::RepoRef) -> gunnar_public_site::BoxFuture<'a, Result<Option<String>, String>> {
            self.0.head(r)
        }
        fn snapshot<'a>(&'a self, r: &'a gunnar_public_site::RepoRef, l: &'a gunnar_public_site::Limits) -> gunnar_public_site::BoxFuture<'a, Result<gunnar_public_site::RepoSnapshot, String>> {
            self.0.snapshot(r, l)
        }
    }
    let site = gunnar_public_site::Site::new(config(), std::sync::Arc::new(Shared(fake.clone())), hooks(), true);
    site.refresh().await.unwrap();
    let part = Part { status: 200, title: "t".into(), description: "d".into(), html: "x".into() };
    site.prerendered().put(&format!("/{NS}/{PUBLIC}"), &format!("{NS}/{PUBLIC}"), part, Instant::now());
    site.refresh().await.unwrap();
    assert!(site.prerendered().get(&format!("/{NS}/{PUBLIC}")).is_some(), "an unchanged head keeps it");
    *fake.head.lock().unwrap() = "ffff000011112222".into();
    site.refresh().await.unwrap();
    assert!(site.prerendered().get(&format!("/{NS}/{PUBLIC}")).is_none(), "a moved head drops it");
}

#[test]
fn the_slow_refresh_is_hourly() {
    assert_eq!(gunnar_public_site::Config::gunnar("https://x").refresh_every, std::time::Duration::from_secs(3600));
}