#![cfg(feature = "cli")]
use std::process::Command;
fn binary() -> std::path::PathBuf {
let mut path = std::env::current_exe()
.expect("current_exe")
.parent()
.expect("parent")
.parent()
.expect("grandparent")
.to_path_buf();
path.push("schemaorg-validate");
path
}
fn fixture(name: &str) -> String {
format!("tests/fixtures/cli/{name}")
}
#[test]
fn valid_product_exits_zero() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--profile",
"google",
])
.output()
.expect("failed to run binary");
assert!(
output.status.success(),
"expected exit 0, got {:?}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn invalid_product_exits_one() {
let output = Command::new(binary())
.args([
"--file",
&fixture("invalid_product.html"),
"--profile",
"none",
])
.output()
.expect("failed to run binary");
assert_eq!(
output.status.code(),
Some(1),
"expected exit 1 for validation errors\nstdout: {}",
String::from_utf8_lossy(&output.stdout),
);
}
#[test]
fn missing_file_exits_two() {
let output = Command::new(binary())
.args(["--file", "nonexistent_file.html", "--profile", "none"])
.output()
.expect("failed to run binary");
assert_eq!(
output.status.code(),
Some(2),
"expected exit 2 for input error",
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot read file"),
"stderr should mention file error: {stderr}",
);
}
#[test]
fn text_output_contains_source_and_version() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--profile",
"none",
"--no-color",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Source:"), "should show source: {stdout}");
assert!(
stdout.contains("Schema.org:"),
"should show schema version: {stdout}"
);
}
#[test]
fn text_output_shows_errors_for_invalid() {
let output = Command::new(binary())
.args([
"--file",
&fixture("invalid_product.html"),
"--profile",
"none",
"--no-color",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("ERROR"), "should contain ERROR: {stdout}");
assert!(
stdout.contains("Unknown type 'Produc'"),
"should report unknown type: {stdout}",
);
assert!(
stdout.contains("Did you mean 'Product'"),
"should suggest correction: {stdout}",
);
}
#[test]
fn text_output_shows_profile_results() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--profile",
"google",
"--no-color",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Profile Results"),
"should show profile section: {stdout}",
);
assert!(
stdout.contains("ELIGIBLE"),
"should show eligibility: {stdout}",
);
}
#[test]
fn json_output_is_valid_json() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--format",
"json",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("output should be valid JSON");
assert!(parsed["source"].is_string(), "should have source field");
assert!(
parsed["schema_version"].is_string(),
"should have schema_version"
);
assert!(
parsed["extraction"]["node_count"].is_number(),
"should have node_count"
);
assert!(
parsed["vocabulary"]["error_count"].is_number(),
"should have error_count"
);
}
#[test]
fn json_output_includes_profile_when_requested() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--format",
"json",
"--profile",
"google",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("output should be valid JSON");
assert!(
parsed["profile"]["eligibility"].is_string(),
"should have profile.eligibility: {}",
&stdout,
);
}
#[test]
fn json_output_omits_profile_when_none() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--format",
"json",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("output should be valid JSON");
assert!(
parsed.get("profile").is_none(),
"should NOT have profile field when --profile none",
);
}
#[test]
fn sarif_output_is_valid_sarif() {
let output = Command::new(binary())
.args([
"--file",
&fixture("invalid_product.html"),
"--format",
"sarif",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("output should be valid JSON");
assert_eq!(parsed["version"], "2.1.0", "should be SARIF 2.1.0");
assert!(
parsed["$schema"].as_str().unwrap().contains("sarif-schema"),
"should reference SARIF schema",
);
let runs = parsed["runs"].as_array().expect("should have runs array");
assert_eq!(runs.len(), 1, "should have exactly one run");
let tool = &runs[0]["tool"]["driver"];
assert_eq!(tool["name"], "schemaorg-validate");
let results = runs[0]["results"].as_array().expect("should have results");
assert!(
!results.is_empty(),
"invalid fixture should produce SARIF results"
);
for result in results {
assert!(
result["ruleId"].is_string(),
"each result should have ruleId",
);
}
}
#[test]
fn sarif_output_empty_for_valid() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--format",
"sarif",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("output should be valid JSON");
let results = parsed["runs"][0]["results"].as_array().expect("results");
assert!(
results.is_empty(),
"valid file should produce no SARIF results"
);
}
#[test]
fn quiet_mode_produces_no_output() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--quiet",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
assert!(
output.stdout.is_empty(),
"quiet mode should produce no stdout",
);
assert!(output.status.success(), "valid file should exit 0");
}
#[test]
fn quiet_mode_exits_one_on_errors() {
let output = Command::new(binary())
.args([
"--file",
&fixture("invalid_product.html"),
"--quiet",
"--profile",
"none",
])
.output()
.expect("failed to run binary");
assert!(output.stdout.is_empty(), "quiet mode: no stdout");
assert_eq!(output.status.code(), Some(1), "should exit 1 for errors");
}
#[test]
fn schema_version_flag() {
let output = Command::new(binary())
.args(["--schema-version"])
.output()
.expect("failed to run binary");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Schema.org v"),
"should print schema version: {stdout}",
);
}
#[test]
fn stdin_mode_works() {
let html = r#"<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Person","name":"Alice"}
</script>"#;
let output = Command::new(binary())
.args(["--stdin", "--profile", "none", "--no-color"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(html.as_bytes())?;
child.wait_with_output()
})
.expect("failed to run with stdin");
assert!(
output.status.success(),
"stdin with valid Person should exit 0"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("<stdin>"), "should show <stdin> as source");
}
#[test]
fn severity_error_only_hides_warnings() {
let output = Command::new(binary())
.args([
"--file",
&fixture("valid_product.html"),
"--profile",
"google",
"--severity",
"error",
"--no-color",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("WARN"),
"severity=error should hide warnings: {stdout}",
);
}
#[test]
fn no_color_flag_removes_ansi() {
let output = Command::new(binary())
.args([
"--file",
&fixture("invalid_product.html"),
"--profile",
"none",
"--no-color",
])
.output()
.expect("failed to run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("\x1b["),
"no-color should strip ANSI codes: {stdout}",
);
}