use std::process::Stdio; use tempfile::TempDir; use tokio::process::Command; /// Build the binary path for the witryna executable. fn witryna_bin() -> std::path::PathBuf { let mut path = std::path::PathBuf::from(env!("CARGO_BIN_EXE_witryna")); if !path.exists() { path = std::path::PathBuf::from("target/debug/witryna"); } path } /// Write a minimal witryna.toml config for status tests. async fn write_status_config( dir: &std::path::Path, sites: &[&str], log_dir: &std::path::Path, ) -> std::path::PathBuf { let base_dir = dir.join("data"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); let mut sites_toml = String::new(); for name in sites { sites_toml.push_str(&format!( r#" [[sites]] name = "{name}" repo_url = "https://example.com/{name}.git" branch = "main" webhook_token = "unused" "# )); } let config_path = dir.join("witryna.toml"); let config = format!( r#"listen_address = "127.0.0.1:0" container_runtime = "podman" base_dir = "{base_dir}" log_dir = "{log_dir}" log_level = "info" {sites_toml}"#, base_dir = base_dir.display(), log_dir = log_dir.display(), ); tokio::fs::write(&config_path, config).await.unwrap(); config_path } /// Write a fake build log with a valid header. async fn write_test_build_log( log_dir: &std::path::Path, site_name: &str, timestamp: &str, status: &str, commit: &str, image: &str, duration: &str, ) { let site_log_dir = log_dir.join(site_name); tokio::fs::create_dir_all(&site_log_dir).await.unwrap(); let content = format!( "=== BUILD LOG ===\n\ Site: {site_name}\n\ Timestamp: {timestamp}\n\ Git Commit: {commit}\n\ Image: {image}\n\ Duration: {duration}\n\ Status: {status}\n\ \n\ === STDOUT ===\n\ build output\n\ \n\ === STDERR ===\n" ); let log_file = site_log_dir.join(format!("{timestamp}.log")); tokio::fs::write(&log_file, content).await.unwrap(); } /// Write a fake hook log with a valid header. async fn write_test_hook_log( log_dir: &std::path::Path, site_name: &str, timestamp: &str, status: &str, ) { let site_log_dir = log_dir.join(site_name); tokio::fs::create_dir_all(&site_log_dir).await.unwrap(); let content = format!( "=== HOOK LOG ===\n\ Site: {site_name}\n\ Timestamp: {timestamp}\n\ Command: hook-cmd\n\ Duration: 1s\n\ Status: {status}\n\ \n\ === STDOUT ===\n\ \n\ === STDERR ===\n" ); let log_file = site_log_dir.join(format!("{timestamp}-hook.log")); tokio::fs::write(&log_file, content).await.unwrap(); } // --------------------------------------------------------------------------- // Tier 1: no container runtime / git needed // --------------------------------------------------------------------------- #[tokio::test] async fn cli_status_no_builds() { let tempdir = TempDir::new().unwrap(); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&log_dir).await.unwrap(); let config_path = write_status_config(tempdir.path(), &["empty-site"], &log_dir).await; let output = Command::new(witryna_bin()) .args(["--config", config_path.to_str().unwrap(), "status"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("SITE"), "should have table header"); assert!( stdout.contains("(no builds)"), "should show (no builds), got: {stdout}" ); } #[tokio::test] async fn cli_status_single_build() { let tempdir = TempDir::new().unwrap(); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&log_dir).await.unwrap(); write_test_build_log( &log_dir, "my-site", "20260126-143000-123456", "success", "abc123d", "node:20-alpine", "45s", ) .await; let config_path = write_status_config(tempdir.path(), &["my-site"], &log_dir).await; let output = Command::new(witryna_bin()) .args(["--config", config_path.to_str().unwrap(), "status"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("my-site"), "should show site name"); assert!(stdout.contains("success"), "should show status"); assert!(stdout.contains("abc123d"), "should show commit"); assert!(stdout.contains("45s"), "should show duration"); } #[tokio::test] async fn cli_status_json_output() { let tempdir = TempDir::new().unwrap(); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&log_dir).await.unwrap(); write_test_build_log( &log_dir, "json-site", "20260126-143000-123456", "success", "abc123d", "node:20-alpine", "45s", ) .await; let config_path = write_status_config(tempdir.path(), &["json-site"], &log_dir).await; let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "status", "--json", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap(); let arr = parsed.as_array().unwrap(); assert_eq!(arr.len(), 1); assert_eq!(arr[0]["site_name"], "json-site"); assert_eq!(arr[0]["status"], "success"); assert_eq!(arr[0]["git_commit"], "abc123d"); assert_eq!(arr[0]["duration"], "45s"); } #[tokio::test] async fn cli_status_site_filter() { let tempdir = TempDir::new().unwrap(); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&log_dir).await.unwrap(); // Create logs for two sites write_test_build_log( &log_dir, "site-a", "20260126-143000-000000", "success", "aaa1111", "alpine:latest", "10s", ) .await; write_test_build_log( &log_dir, "site-b", "20260126-150000-000000", "success", "bbb2222", "alpine:latest", "20s", ) .await; let config_path = write_status_config(tempdir.path(), &["site-a", "site-b"], &log_dir).await; let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "status", "--site", "site-a", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("site-a"), "should show filtered site"); assert!( !stdout.contains("site-b"), "should NOT show other site, got: {stdout}" ); } #[tokio::test] async fn cli_status_hook_failed() { let tempdir = TempDir::new().unwrap(); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&log_dir).await.unwrap(); // Build succeeded, but hook failed write_test_build_log( &log_dir, "hook-site", "20260126-143000-123456", "success", "abc123d", "alpine:latest", "12s", ) .await; write_test_hook_log( &log_dir, "hook-site", "20260126-143000-123456", "failed (exit code 1)", ) .await; let config_path = write_status_config(tempdir.path(), &["hook-site"], &log_dir).await; let output = Command::new(witryna_bin()) .args(["--config", config_path.to_str().unwrap(), "status"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("hook failed"), "should show 'hook failed', got: {stdout}" ); }