use crate::git_helpers::create_bare_repo; use crate::runtime::{detect_container_runtime, skip_without_git, skip_without_runtime}; 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 { // cargo test sets CARGO_BIN_EXE_witryna when the binary exists, // but for integration tests we use the debug build path directly. let mut path = std::path::PathBuf::from(env!("CARGO_BIN_EXE_witryna")); if !path.exists() { // Fallback to target/debug/witryna path = std::path::PathBuf::from("target/debug/witryna"); } path } /// Write a minimal witryna.toml config file. async fn write_config( dir: &std::path::Path, site_name: &str, repo_url: &str, base_dir: &std::path::Path, log_dir: &std::path::Path, command: &str, public: &str, ) -> std::path::PathBuf { let config_path = dir.join("witryna.toml"); let runtime = detect_container_runtime(); let config = format!( r#"listen_address = "127.0.0.1:0" container_runtime = "{runtime}" base_dir = "{base_dir}" log_dir = "{log_dir}" log_level = "debug" [[sites]] name = "{site_name}" repo_url = "{repo_url}" branch = "main" webhook_token = "unused" image = "alpine:latest" command = "{command}" public = "{public}" "#, base_dir = base_dir.display(), log_dir = log_dir.display(), ); tokio::fs::write(&config_path, config).await.unwrap(); config_path } // --------------------------------------------------------------------------- // Tier 1: no container runtime needed // --------------------------------------------------------------------------- #[tokio::test] async fn cli_run_site_not_found_exits_nonzero() { let tempdir = TempDir::new().unwrap(); let base_dir = tempdir.path().join("data"); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); tokio::fs::create_dir_all(&log_dir).await.unwrap(); // Write config with no sites matching "nonexistent" let config_path = tempdir.path().join("witryna.toml"); let config = format!( r#"listen_address = "127.0.0.1:0" container_runtime = "podman" base_dir = "{}" log_dir = "{}" log_level = "info" sites = [] "#, base_dir.display(), log_dir.display(), ); tokio::fs::write(&config_path, config).await.unwrap(); let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "run", "nonexistent", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( !output.status.success(), "should exit non-zero for unknown site" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "stderr should mention site not found, got: {stderr}" ); } #[tokio::test] async fn cli_run_build_failure_exits_nonzero() { skip_without_git!(); skip_without_runtime!(); let tempdir = TempDir::new().unwrap(); let base_dir = tempdir.path().join("data"); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); tokio::fs::create_dir_all(&log_dir).await.unwrap(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_bare_repo(&repo_dir, "main").await; let config_path = write_config( tempdir.path(), "fail-site", &repo_url, &base_dir, &log_dir, "exit 42", "dist", ) .await; let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "run", "fail-site", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( !output.status.success(), "should exit non-zero on build failure" ); // Verify a log file was created let logs_dir = log_dir.join("fail-site"); if logs_dir.is_dir() { let mut entries = tokio::fs::read_dir(&logs_dir).await.unwrap(); let mut found_log = false; while let Some(entry) = entries.next_entry().await.unwrap() { if entry.file_name().to_string_lossy().ends_with(".log") { found_log = true; break; } } assert!(found_log, "should have a .log file after failed build"); } } // --------------------------------------------------------------------------- // Tier 2: requires git + container runtime // --------------------------------------------------------------------------- #[tokio::test] async fn cli_run_builds_site_successfully() { skip_without_git!(); skip_without_runtime!(); let tempdir = TempDir::new().unwrap(); let base_dir = tempdir.path().join("data"); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); tokio::fs::create_dir_all(&log_dir).await.unwrap(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_bare_repo(&repo_dir, "main").await; let config_path = write_config( tempdir.path(), "test-site", &repo_url, &base_dir, &log_dir, "mkdir -p out && echo hello > out/index.html", "out", ) .await; let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "run", "test-site", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "should exit 0 on success, stderr: {stderr}" ); // Verify symlink exists let current = base_dir.join("builds/test-site/current"); assert!(current.is_symlink(), "current symlink should exist"); // Verify published content let target = tokio::fs::read_link(¤t).await.unwrap(); let content = tokio::fs::read_to_string(target.join("index.html")) .await .unwrap(); assert!(content.contains("hello"), "published content should match"); // Verify log file exists let logs_dir = log_dir.join("test-site"); assert!(logs_dir.is_dir(), "logs directory should exist"); } #[tokio::test] async fn cli_run_verbose_shows_build_output() { skip_without_git!(); skip_without_runtime!(); let tempdir = TempDir::new().unwrap(); let base_dir = tempdir.path().join("data"); let log_dir = tempdir.path().join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); tokio::fs::create_dir_all(&log_dir).await.unwrap(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_bare_repo(&repo_dir, "main").await; let config_path = write_config( tempdir.path(), "verbose-site", &repo_url, &base_dir, &log_dir, "echo VERBOSE_MARKER && mkdir -p out && echo ok > out/index.html", "out", ) .await; let output = Command::new(witryna_bin()) .args([ "--config", config_path.to_str().unwrap(), "run", "verbose-site", "--verbose", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); assert!(output.status.success(), "should exit 0, stderr: {stderr}"); // In verbose mode, build output should appear in stderr assert!( stderr.contains("VERBOSE_MARKER"), "stderr should contain build output in verbose mode, got: {stderr}" ); }