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