use std::fs;
use std::process::Command;
use tempfile::tempdir;
fn guidebook_bin() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("guidebook");
path.to_string_lossy().to_string()
}
#[test]
fn test_invalid_book_json() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), "{ invalid json }}}").unwrap();
fs::write(
source.join("SUMMARY.md"),
"# Summary\n* [Intro](README.md)\n",
)
.unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
if !result.status.success() {
assert!(
!stderr.contains("panicked"),
"Should not panic on invalid book.json"
);
}
}
#[test]
fn test_empty_summary() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(source.join("SUMMARY.md"), "").unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on empty SUMMARY.md"
);
}
#[test]
fn test_missing_summary() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on missing SUMMARY.md"
);
}
#[test]
fn test_summary_with_missing_files() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(
source.join("SUMMARY.md"),
r#"# Summary
* [Intro](README.md)
* [Missing](does-not-exist.md)
* [Also Missing](another/missing.md)
"#,
)
.unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on missing chapter files"
);
assert!(
result.status.success(),
"Build should succeed even with missing files"
);
}
#[test]
fn test_malformed_markdown() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(
source.join("SUMMARY.md"),
"# Summary\n* [Intro](README.md)\n",
)
.unwrap();
fs::write(
source.join("README.md"),
r#"# Malformed Content
## Unclosed code block
```
fn never_closed() {
## Deeply nested headers
###### Level 6
####### Level 7 (invalid)
## Broken links
[link with no url]()
[link to nowhere](javascript:alert(1))
![image with no src]()
## Mixed content
<div>Raw HTML that might break things</div>
## Unicode extremes
🎉🎊🎈 Emoji headers 🎉🎊🎈
## Empty sections
---
---
---
"#,
)
.unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on malformed markdown"
);
assert!(
result.status.success(),
"Build should succeed with malformed markdown"
);
}
#[test]
fn test_empty_book_json() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), "{}").unwrap();
fs::write(
source.join("SUMMARY.md"),
"# Summary\n* [Intro](README.md)\n",
)
.unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on empty book.json"
);
assert!(
result.status.success(),
"Build should succeed with empty book.json"
);
}
#[test]
fn test_binary_content_in_markdown() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(
source.join("SUMMARY.md"),
"# Summary\n* [Intro](README.md)\n",
)
.unwrap();
let mut content = b"# Title\n\nSome text\n\n".to_vec();
content.extend_from_slice(&[0x00, 0xFF, 0xFE, 0x89, 0x50, 0x4E, 0x47]);
content.extend_from_slice(b"\n\nMore text after binary\n");
fs::write(source.join("README.md"), content).unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on binary content in markdown"
);
}
#[test]
fn test_nonexistent_source_directory() {
let temp = tempdir().unwrap();
let source = temp.path().join("does-not-exist");
let output = temp.path().join("output");
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
assert!(
!result.status.success(),
"Build should fail for nonexistent source"
);
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on nonexistent source"
);
}
#[test]
fn test_large_summary() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
let output = temp.path().join("output");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(source.join("README.md"), "# Hello\n").unwrap();
let mut summary = String::from("# Summary\n\n* [Intro](README.md)\n");
for i in 0..50 {
let filename = format!("chapter_{}.md", i);
summary.push_str(&format!("* [Chapter {}]({})\n", i, filename));
if i < 5 {
fs::write(
source.join(&filename),
format!("# Chapter {}\nContent\n", i),
)
.unwrap();
}
}
fs::write(source.join("SUMMARY.md"), summary).unwrap();
let result = Command::new(guidebook_bin())
.arg("build")
.arg(source.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("Failed to execute guidebook build");
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"Should not panic on large summary with missing files"
);
assert!(result.status.success(), "Build should succeed");
}