use std::fs;
use std::path::{Path, PathBuf};
use mini_docs::Builder;
use serde_json::Value;
const TEMPLATE: &str = "{{ page.content | safe }}";
fn build_site(pretty_urls: bool) -> (tempfile::TempDir, PathBuf) {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
fs::write(
input.path().join("article.md"),
"# Article\n\n[Setup](guide/setup.md)\n",
)
.expect("write article.md");
fs::create_dir_all(input.path().join("guide")).expect("create guide dir");
fs::write(input.path().join("guide/setup.md"), "# Setup\n").expect("write setup.md");
fs::write(input.path().join("guide/index.md"), "# Guide\n").expect("write index.md");
fs::write(templates.path().join("page.html"), TEMPLATE).expect("write page.html");
Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.link_base("/")
.data_json("data.json")
.pretty_urls(pretty_urls)
.build()
.expect("build should succeed");
let path = output.path().to_path_buf();
(output, path)
}
fn serve(output: &Path, url: &str) -> Option<PathBuf> {
let relative = url.trim_start_matches('/');
let candidate = if relative.is_empty() {
output.to_path_buf()
} else {
output.join(relative)
};
if candidate.is_file() {
return Some(candidate);
}
let index = candidate.join("index.html");
index.is_file().then_some(index)
}
fn data_json_urls(output: &Path) -> Vec<String> {
let raw = fs::read_to_string(output.join("data.json")).expect("read data.json");
let entries: Value = serde_json::from_str(&raw).expect("parse data.json");
entries
.as_array()
.expect("data.json is an array")
.iter()
.map(|entry| entry["url"].as_str().expect("url is a string").to_string())
.collect()
}
#[test]
fn plain_urls_write_sibling_html_files() {
let (_guard, output) = build_site(false);
assert!(output.join("article.html").is_file());
assert!(output.join("guide/setup.html").is_file());
assert!(output.join("guide/index.html").is_file());
}
#[test]
fn pretty_urls_write_directory_indexes() {
let (_guard, output) = build_site(true);
assert!(output.join("article/index.html").is_file());
assert!(output.join("guide/setup/index.html").is_file());
assert!(
!output.join("article.html").exists(),
"the flat file must not also be written — two URLs for one page"
);
}
#[test]
fn an_index_source_is_not_nested_a_second_time_under_pretty_urls() {
let (_guard, output) = build_site(true);
assert!(output.join("guide/index.html").is_file());
assert!(
!output.join("guide/index/index.html").exists(),
"index.md is already its directory's index"
);
}
#[test]
fn every_advertised_url_resolves_in_both_styles() {
for pretty_urls in [false, true] {
let (_guard, output) = build_site(pretty_urls);
for url in data_json_urls(&output) {
assert!(
serve(&output, &url).is_some(),
"data.json advertises {url}, which no file serves (pretty_urls = {pretty_urls})"
);
}
}
}
#[test]
fn a_rewritten_internal_link_resolves_in_both_styles() {
for pretty_urls in [false, true] {
let (_guard, output) = build_site(pretty_urls);
let article = serve(&output, "/article").or_else(|| serve(&output, "/article.html"));
let html =
fs::read_to_string(article.expect("article should be served")).expect("read article");
let href = html
.split(r#"href=""#)
.nth(1)
.and_then(|rest| rest.split('"').next())
.expect("article should contain a rewritten link");
assert!(
serve(&output, href).is_some(),
"article links to {href}, which no file serves (pretty_urls = {pretty_urls})"
);
}
}
#[test]
fn pretty_urls_advertise_extensionless_directory_urls() {
let (_guard, output) = build_site(true);
let urls = data_json_urls(&output);
assert!(urls.contains(&"/article/".to_string()), "{urls:?}");
assert!(urls.contains(&"/guide/setup/".to_string()), "{urls:?}");
assert!(
urls.iter().all(|url| !url.ends_with(".html")),
"pretty URLs must carry no extension: {urls:?}"
);
}
#[test]
fn no_advertised_pretty_url_would_be_redirected() {
let (_guard, output) = build_site(true);
for url in data_json_urls(&output) {
assert!(
url.ends_with('/'),
"{url} names a directory index without its trailing slash: a 301"
);
}
}
#[test]
fn plain_urls_advertise_the_html_file() {
let (_guard, output) = build_site(false);
let urls = data_json_urls(&output);
assert!(urls.contains(&"/article.html".to_string()), "{urls:?}");
assert!(
urls.iter().all(|url| url.ends_with(".html")),
"plain URLs must name the file written: {urls:?}"
);
}
#[test]
fn ids_are_unchanged_by_url_style() {
let ids = |pretty_urls| {
let (_guard, output) = build_site(pretty_urls);
let raw = fs::read_to_string(output.join("data.json")).expect("read data.json");
let entries: Value = serde_json::from_str(&raw).expect("parse data.json");
let mut ids: Vec<String> = entries
.as_array()
.expect("data.json is an array")
.iter()
.map(|entry| entry["id"].as_str().expect("id is a string").to_string())
.collect();
ids.sort();
ids
};
assert_eq!(ids(false), ids(true));
}