Files
alkgen/tests/ssr_fixtures.rs
glm-5.3-flash 14d8a49611 test(fixtures): wave-1 engine fixture doubles (ssr-shaped and code-shaped)
- ssr double: page template composing header/footer partials under the
  HTML escaper, typed context built by a builder fn per ADR-004; golden
  pins for composition, escaping of hostile interpolation, the trusted
  {{& }} slot, and render-by-partial-name rejection
- code double: struct-writer template under the no-op escaper, typed
  context built from a schema document with kind mapping and
  source-escaped literals in the builder; golden pins for document-order
  arrays and the active preserve_order feature
- fixture assets are include_str! files under tests/fixtures/; fixture
  modules are per-test-binary so wave-2 proofs can grow them
- task file updated to completed with Notes and Summary

Verification: cargo test green (19 lib + 8 fixture tests), clippy
-D warnings clean, cargo fmt --check clean
2026-11-06 20:05:42 +00:00

210 lines
6.3 KiB
Rust

//! The ssr-shaped engine fixture double (posture-B shape): a page
//! template composing fragment partials over a typed context built by
//! a Rust builder, rendered through the public API only, pinned by
//! golden renders.
use alkgen::{EscapePolicy, TemplateSet};
use serde::Serialize;
mod fixtures {
pub const PAGE: &str = include_str!("fixtures/ssr/page.hbs");
pub const HEADER: &str = include_str!("fixtures/ssr/header.hbs");
pub const FOOTER: &str = include_str!("fixtures/ssr/footer.hbs");
}
/// A section's prose kind. The mapping from this kind to the rendered
/// element (a note aside versus a plain paragraph) lives in the
/// builder: the template only reads the `is_callout` flag.
enum SectionKind {
Prose(String),
Note(String),
}
struct NavLinkInput {
href: String,
label: String,
}
#[derive(Serialize)]
struct PageContext {
nav: Vec<NavLink>,
sections: Vec<Section>,
show_banner: bool,
banner_text: String,
trusted_sidebar: String,
year: u16,
site_name: String,
}
#[derive(Serialize)]
struct NavLink {
href: String,
label: String,
}
#[derive(Serialize)]
struct Section {
is_callout: bool,
text: String,
}
/// The ADR-004 context builder: kind mapping and structure decisions
/// (an `Option` banner becomes a flag plus text, section kinds become
/// flag-carrying sections) happen here in Rust; the page template
/// stays dumb conditionals over context flags.
fn build_page(
nav: Vec<NavLinkInput>,
sections: Vec<SectionKind>,
banner: Option<&str>,
trusted_sidebar: &str,
year: u16,
site_name: &str,
) -> PageContext {
let (show_banner, banner_text) = match banner {
Some(text) => (true, text.to_owned()),
None => (false, String::new()),
};
PageContext {
nav: nav
.into_iter()
.map(|link| NavLink {
href: link.href,
label: link.label,
})
.collect(),
sections: sections
.into_iter()
.map(|section| match section {
SectionKind::Prose(text) => Section {
is_callout: false,
text,
},
SectionKind::Note(text) => Section {
is_callout: true,
text,
},
})
.collect(),
show_banner,
banner_text,
trusted_sidebar: trusted_sidebar.to_owned(),
year,
site_name: site_name.to_owned(),
}
}
/// The ssr-shaped fixture set: one `page` template kind plus the two
/// composition partials it includes, under the HTML escaper.
fn ssr_fixture_set() -> TemplateSet {
let mut set = TemplateSet::new(EscapePolicy::Html);
set.register_template("page", fixtures::PAGE)
.expect("fixture page template should be well-formed");
set.register_partial("header", fixtures::HEADER)
.expect("fixture header partial should be well-formed");
set.register_partial("footer", fixtures::FOOTER)
.expect("fixture footer partial should be well-formed");
set
}
const GOLDEN_PAGE: &str = concat!(
"<!doctype html>\n",
"<html lang=\"en\">\n",
"<header><nav><a href=\"/\">Home</a>",
"<a href=\"/docs?x&#x3D;1&amp;y&#x3D;2\">Docs &amp; Guide</a>",
"<a href=\"/search?q&#x3D;&lt;script&gt;\">&lt;script&gt;alert(1)&lt;/script&gt;</a></nav></header>\n",
"<body>\n",
"<main><p>Plain intro paragraph.</p>",
"<aside class=\"note\">Careful: &lt;tag&gt; &amp; &quot;quotes&quot;.</aside></main>\n",
"<p class=\"banner\">&lt;em&gt;the&lt;/em&gt; site</p>\n",
"<aside id=\"trusted\"><b>sidebar</b></aside>\n",
"<footer><span>&copy; 2026 Alk Forge</span></footer>\n",
"</body>\n",
"</html>\n",
);
#[test]
fn page_golden_render_pins_composition_and_escaping() {
let set = ssr_fixture_set();
let context = build_page(
vec![
NavLinkInput {
href: "/".into(),
label: "Home".into(),
},
NavLinkInput {
href: "/docs?x=1&y=2".into(),
label: "Docs & Guide".into(),
},
NavLinkInput {
href: "/search?q=<script>".into(),
label: "<script>alert(1)</script>".into(),
},
],
vec![
SectionKind::Prose("Plain intro paragraph.".into()),
SectionKind::Note("Careful: <tag> & \"quotes\".".into()),
],
Some("<em>the</em> site"),
r#"<aside id="trusted"><b>sidebar</b></aside>"#,
2026,
"Alk Forge",
);
let rendered = set
.render("page", &context)
.expect("the golden page should render");
assert_eq!(rendered, GOLDEN_PAGE);
assert!(
rendered.contains("<a href=\"/docs?x&#x3D;1&amp;y&#x3D;2\">Docs &amp; Guide</a>"),
"interpolated user data must be entity-escaped (the substrate escaper also escapes '=' in attributes)"
);
assert!(
!rendered.contains("<script>alert(1)</script>"),
"interpolated user data must be entity-escaped"
);
assert!(
rendered.contains(&format!(
"<aside class=\"note\">Careful: {}</aside>",
"&lt;tag&gt; &amp; &quot;quotes&quot;."
)),
"hostile-looking section text must never appear raw"
);
assert!(
rendered.contains(r#"<aside id="trusted"><b>sidebar</b></aside>"#),
"the deliberately-trusted raw slot ({{& }}) must pass through verbatim"
);
}
#[test]
fn no_banner_flag_is_the_builder_callout_not_a_template_lookup() {
let set = ssr_fixture_set();
let context = build_page(
Vec::new(),
vec![SectionKind::Prose("Only prose.".into())],
None,
"",
2026,
"Alk Forge",
);
let rendered = set
.render("page", &context)
.expect("the flag-only page should render");
assert!(
!rendered.contains("banner"),
"the builder flag decides the banner; the template only conditions on it"
);
}
#[test]
fn render_by_partial_name_is_rejected_by_the_public_api() {
let set = ssr_fixture_set();
let context = build_page(Vec::new(), Vec::new(), None, "", 2026, "Alk Forge");
let err = set
.render("header", &context)
.expect_err("partials are composition units, never direct render targets");
assert!(matches!(err, alkgen::Error::Render(_)));
}