use std::fs;
use std::path::Path;
use tempfile::TempDir;
use optica::config::SiteConfig;
use optica::graph;
use optica::output;
use optica::parser;
use optica::render;
use optica::scanner;
fn setup_test_env() -> (TempDir, SiteConfig) {
let tmp = TempDir::new().expect("failed to create temp dir");
let graph_dir = tmp.path().join("graph");
fs::create_dir_all(&graph_dir).expect("failed to create graph dir");
let mut config = SiteConfig::default();
config.build.input_dir = tmp.path().to_path_buf();
config.build.output_dir = tmp.path().join("build");
config.content.public_only = false; config.content.default_public = true;
config.search.enabled = false; config.feeds.enabled = false; config.graph.enabled = false;
(tmp, config)
}
fn write_page(base: &Path, name: &str, content: &str) {
let graph_dir = base.join("graph");
let page_path = graph_dir.join(format!("{}.md", name));
if let Some(parent) = page_path.parent() {
fs::create_dir_all(parent).expect("failed to create page parent dir");
}
fs::write(&page_path, content).expect("failed to write page");
}
fn delete_page(base: &Path, name: &str) {
let page_path = base.join("graph").join(format!("{}.md", name));
if page_path.exists() {
fs::remove_file(&page_path).expect("failed to delete page");
}
if let Some(parent) = page_path.parent() {
let _ = fs::remove_dir(parent); }
}
fn full_build(config: &SiteConfig) -> Vec<render::RenderedPage> {
let discovered = scanner::scan(&config.build.input_dir, &config.content)
.expect("scan failed");
let parsed_pages = parser::parse_all(&discovered).expect("parse failed");
let page_store = graph::build_graph(parsed_pages).expect("build_graph failed");
let rendered = render::render_all(&page_store, config).expect("render failed");
output::write_output(&rendered, &page_store, config, &discovered)
.expect("write_output failed");
rendered
}
fn rebuild(config: &SiteConfig) -> Vec<render::RenderedPage> {
let discovered = scanner::scan(&config.build.input_dir, &config.content)
.expect("scan failed");
let parsed_pages = parser::parse_all(&discovered).expect("parse failed");
let page_store = graph::build_graph(parsed_pages).expect("build_graph failed");
let rendered = render::render_all(&page_store, config).expect("render failed");
output::write_incremental(&rendered, &page_store, config, &discovered)
.expect("write_incremental failed");
rendered
}
fn output_exists(config: &SiteConfig, slug: &str) -> bool {
let index_path = config.build.output_dir.join(slug).join("index.html");
index_path.exists()
}
fn read_output(config: &SiteConfig, slug: &str) -> String {
let index_path = config.build.output_dir.join(slug).join("index.html");
fs::read_to_string(&index_path)
.unwrap_or_else(|_| panic!("failed to read output at {}", index_path.display()))
}
#[test]
fn test_new_page_appears_in_output() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "test", "---\ntags: cyber\n---\n\nHello world");
let _rendered = full_build(&config);
assert!(
output_exists(&config, "test"),
"build/test/index.html should exist after building graph/test.md"
);
let html = read_output(&config, "test");
assert!(
html.contains("Hello world"),
"output HTML should contain the page content"
);
}
#[test]
fn test_deleted_page_removed_from_output() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "deleteme", "---\ntags: test\n---\n\nTemporary page");
write_page(tmp.path(), "keeper", "---\ntags: test\n---\n\nPermanent page");
full_build(&config);
assert!(output_exists(&config, "deleteme"), "page should exist after initial build");
assert!(output_exists(&config, "keeper"), "keeper should exist after initial build");
delete_page(tmp.path(), "deleteme");
rebuild(&config);
assert!(
!output_exists(&config, "deleteme"),
"build/deleteme/index.html should be removed after page deletion and rebuild"
);
assert!(
output_exists(&config, "keeper"),
"keeper page should still exist after rebuild"
);
}
#[test]
fn test_moved_page_old_output_removed() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "old", "---\ntags: test\n---\n\nMoving page");
full_build(&config);
assert!(output_exists(&config, "old"), "old page should exist after initial build");
delete_page(tmp.path(), "old");
write_page(tmp.path(), "new", "---\ntags: test\n---\n\nMoving page");
rebuild(&config);
assert!(
!output_exists(&config, "old"),
"old output should be removed after page move"
);
assert!(
output_exists(&config, "new"),
"new output should exist after page move"
);
}
#[test]
fn test_namespace_move_updates_listing() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "mypage", "---\ntags: test\n---\n\nRoot page");
full_build(&config);
assert!(output_exists(&config, "mypage"), "root page should exist");
delete_page(tmp.path(), "mypage");
write_page(tmp.path(), "ns/mypage", "---\ntags: test\n---\n\nNamespaced page");
rebuild(&config);
assert!(
!output_exists(&config, "mypage"),
"root-level output should be removed after namespace move"
);
let ns_slug = parser::slugify_page_name("ns/mypage");
assert!(
output_exists(&config, &ns_slug),
"namespaced page output should exist at build/{}/index.html",
ns_slug
);
let ns_html = read_output(&config, &ns_slug);
assert!(
ns_html.contains("Namespaced page"),
"namespaced page output should contain its content"
);
}
#[test]
fn test_content_edit_reflects_in_output() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"editable",
"---\ntags: test\n---\n\nOriginal content here",
);
full_build(&config);
let html_before = read_output(&config, "editable");
assert!(
html_before.contains("Original content here"),
"initial build should contain original content"
);
write_page(
tmp.path(),
"editable",
"---\ntags: test\n---\n\nUpdated content with new info",
);
rebuild(&config);
let html_after = read_output(&config, "editable");
assert!(
html_after.contains("Updated content with new info"),
"rebuilt output should contain updated content"
);
assert!(
!html_after.contains("Original content here"),
"rebuilt output should not contain old content"
);
}
#[test]
fn test_tag_change_updates_tag_page() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"tagged",
"---\ntags: alpha\n---\n\nA page with a tag",
);
full_build(&config);
assert!(
output_exists(&config, "tags"),
"tags index page should exist"
);
if output_exists(&config, "tags/alpha") {
let tag_html = read_output(&config, "tags/alpha");
assert!(
tag_html.contains("tagged"),
"tag page for 'alpha' should reference the tagged page"
);
}
write_page(
tmp.path(),
"tagged",
"---\ntags: beta\n---\n\nA page with a different tag",
);
rebuild(&config);
if output_exists(&config, "tags/beta") {
let tag_html = read_output(&config, "tags/beta");
assert!(
tag_html.contains("tagged"),
"tag page for 'beta' should reference the tagged page after tag change"
);
}
}
#[test]
fn test_multiple_pages_build_correctly() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "page-a", "---\ntags: test\n---\n\nPage A content");
write_page(tmp.path(), "page-b", "---\ntags: test\n---\n\nPage B links to page-a");
write_page(tmp.path(), "page-c", "---\ntags: test\n---\n\nPage C links to page-b");
full_build(&config);
assert!(output_exists(&config, "page-a"), "page-a should exist in output");
assert!(output_exists(&config, "page-b"), "page-b should exist in output");
assert!(output_exists(&config, "page-c"), "page-c should exist in output");
let html_b = read_output(&config, "page-b");
assert!(
html_b.contains("/page-a"),
"page-b output should contain a link to page-a"
);
}
#[test]
fn test_backlinks_appear_in_output() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "target", "---\ntags: test\n---\n\nTarget page");
write_page(
tmp.path(),
"linker",
"---\ntags: test\n---\n\nThis page links to target",
);
full_build(&config);
let target_html = read_output(&config, "target");
assert!(
target_html.contains("linker"),
"target page should show backlink from linker page"
);
}
#[test]
fn test_namespace_hierarchy_builds() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"project/alpha",
"---\ntags: test\n---\n\nAlpha project",
);
write_page(
tmp.path(),
"project/beta",
"---\ntags: test\n---\n\nBeta project",
);
full_build(&config);
let alpha_slug = parser::slugify_page_name("project/alpha");
let beta_slug = parser::slugify_page_name("project/beta");
assert!(
output_exists(&config, &alpha_slug),
"project/alpha should have output"
);
assert!(
output_exists(&config, &beta_slug),
"project/beta should have output"
);
let alpha_html = read_output(&config, &alpha_slug);
assert!(
alpha_html.contains("Alpha project"),
"alpha page output should contain its content"
);
let beta_html = read_output(&config, &beta_slug);
assert!(
beta_html.contains("Beta project"),
"beta page output should contain its content"
);
}
#[test]
fn test_empty_graph_builds_without_error() {
let (_tmp, config) = setup_test_env();
full_build(&config);
assert!(
config.build.output_dir.exists(),
"output directory should exist even with empty graph"
);
let index_path = config.build.output_dir.join("index.html");
assert!(
index_path.exists(),
"index.html should exist even with empty graph"
);
}
#[test]
fn test_page_with_aliases_builds() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"canonical",
"---\ntags: test\nalias: alt-name, another-alias\n---\n\nPage with aliases",
);
write_page(
tmp.path(),
"referrer",
"---\ntags: test\n---\n\nLinks to alt-name",
);
full_build(&config);
assert!(output_exists(&config, "canonical"), "canonical page should exist");
let referrer_html = read_output(&config, "referrer");
assert!(
referrer_html.contains("canonical") || referrer_html.contains("alt-name"),
"referrer should contain a link related to the aliased page"
);
}
#[test]
fn test_rebuild_after_adding_new_page() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "first", "---\ntags: test\n---\n\nFirst page");
full_build(&config);
assert!(output_exists(&config, "first"), "first page should exist");
write_page(tmp.path(), "second", "---\ntags: test\n---\n\nSecond page");
rebuild(&config);
assert!(output_exists(&config, "first"), "first page should still exist");
assert!(output_exists(&config, "second"), "second page should appear after rebuild");
}