//! CLI integration tests. use assert_cmd::Command; use predicates::prelude::*; fn servo_fetch() -> Command { Command::cargo_bin("servo-fetch").expect("binary exists") } #[test] fn no_args_shows_error() { servo_fetch() .assert() .failure() .stderr(predicate::str::contains("URL is required")); } #[test] fn invalid_url_shows_error() { servo_fetch() .arg("not-a-url") .assert() .failure() .stderr(predicate::str::contains("invalid URL")); } #[test] fn file_scheme_rejected() { servo_fetch() .arg("file:///etc/passwd") .assert() .failure() .stderr(predicate::str::contains("not allowed")); } #[test] fn javascript_scheme_rejected() { servo_fetch() .arg("javascript:alert(1)") .assert() .failure() .stderr(predicate::str::contains("not allowed")); } #[test] fn version_flag() { servo_fetch() .arg("--version") .assert() .success() .stdout(predicate::str::contains("servo-fetch")); } #[test] fn help_flag() { servo_fetch() .arg("--help") .assert() .success() .stdout(predicate::str::contains("browser engine in a binary")); } const TIMEOUT: &str = "--timeout=60"; #[test] #[ignore = "requires Servo + network"] fn default_produces_markdown() { servo_fetch() .args([TIMEOUT, "https://example.com"]) .assert() .success() .stdout(predicate::str::contains("Example Domain")); } #[test] #[ignore = "requires Servo + network"] fn json_produces_valid_json() { let output = servo_fetch() .args(["--json", TIMEOUT, "https://example.com"]) .assert() .success() .get_output() .stdout .clone(); let parsed: serde_json::Value = serde_json::from_slice(&output).expect("valid JSON"); assert!(parsed.get("title").is_some()); } #[test] #[ignore = "requires Servo + network"] fn js_eval_returns_result() { servo_fetch() .args(["--js", "document.title", TIMEOUT, "https://example.com"]) .assert() .success() .stdout(predicate::str::contains("Example Domain")); } #[test] #[ignore = "requires Servo + network"] fn screenshot_creates_file() { let dir = std::env::temp_dir().join("servo-fetch-test"); std::fs::create_dir_all(&dir).ok(); let path = dir.join("test.png"); servo_fetch() .args(["--screenshot", path.to_str().unwrap(), TIMEOUT, "https://example.com"]) .assert() .success(); assert!(path.exists(), "screenshot file should be created"); assert!(path.metadata().unwrap().len() > 0, "screenshot should not be empty"); std::fs::remove_file(&path).ok(); } #[test] #[ignore = "requires Servo + network"] fn timeout_produces_error() { servo_fetch() .args(["--timeout=0", "https://example.com"]) .assert() .failure() .stderr(predicate::str::contains("invalid value '0'")); } #[test] fn crawl_rejects_private_ip() { servo_fetch() .args(["crawl", "http://127.0.0.1/"]) .assert() .failure() .stderr(predicate::str::contains("not allowed")); } #[test] fn crawl_rejects_file_scheme() { servo_fetch() .args(["crawl", "file:///etc/passwd"]) .assert() .failure() .stderr(predicate::str::contains("not allowed")); } #[test] fn crawl_rejects_invalid_url() { servo_fetch() .args(["crawl", "not-a-url"]) .assert() .failure() .stderr(predicate::str::contains("invalid URL")); } #[test] fn crawl_help_shows_options() { servo_fetch() .args(["crawl", "--help"]) .assert() .success() .stdout(predicate::str::contains("--limit")) .stdout(predicate::str::contains("--max-depth")) .stdout(predicate::str::contains("--include")) .stdout(predicate::str::contains("--exclude")); } #[test] #[ignore = "requires Servo + network"] fn crawl_produces_ndjson() { let output = servo_fetch() .args(["crawl", "https://example.com", "--limit", "2", "--timeout", "60"]) .assert() .success() .get_output() .stdout .clone(); let first_line = std::str::from_utf8(&output) .expect("valid utf8") .lines() .next() .expect("at least one line"); let parsed: serde_json::Value = serde_json::from_str(first_line).expect("valid NDJSON"); assert_eq!(parsed["status"], "ok"); assert!(parsed["url"].as_str().is_some()); assert!(parsed["content"].as_str().is_some()); }