use crate::git_helpers::create_local_repo; use crate::harness::{SiteBuilder, TestServer, test_config_with_site}; use crate::runtime::{skip_without_git, skip_without_runtime}; use std::path::Path; use std::time::Duration; // --------------------------------------------------------------------------- // Tier 2 (requires container runtime + git) // --------------------------------------------------------------------------- #[tokio::test] async fn post_deploy_hook_runs_after_build() { skip_without_git!(); skip_without_runtime!(); let tempdir = tempfile::tempdir().unwrap(); let base_dir = tempdir.path().to_path_buf(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_local_repo(&repo_dir, "main").await; // The hook creates a "hook-ran" marker file in the build output directory let site = SiteBuilder::new("hook-test", &repo_url, "test-token") .overrides( "alpine:latest", "mkdir -p out && echo '

hook

' > out/index.html", "out", ) .post_deploy(vec!["touch".to_owned(), "hook-ran".to_owned()]) .build(); let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; let resp = TestServer::client() .post(server.url("/hook-test")) .header("Authorization", "Bearer test-token") .send() .await .unwrap(); assert_eq!(resp.status().as_u16(), 202); // Wait for build + hook to complete let builds_dir = base_dir.join("builds/hook-test"); let max_wait = Duration::from_secs(120); let start = std::time::Instant::now(); loop { assert!(start.elapsed() <= max_wait, "build timed out"); if builds_dir.join("current").is_symlink() { // Give the hook a moment to finish after symlink switch tokio::time::sleep(Duration::from_secs(3)).await; break; } tokio::time::sleep(Duration::from_millis(500)).await; } // Verify the hook ran — marker file should exist in the build directory let current_target = tokio::fs::read_link(builds_dir.join("current")) .await .expect("current symlink should exist"); assert!( current_target.join("hook-ran").exists(), "hook marker file should exist in build directory" ); } #[tokio::test] async fn post_deploy_hook_failure_nonfatal() { skip_without_git!(); skip_without_runtime!(); let tempdir = tempfile::tempdir().unwrap(); let base_dir = tempdir.path().to_path_buf(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_local_repo(&repo_dir, "main").await; // The hook will fail (exit 1), but the deploy should still succeed let site = SiteBuilder::new("hook-fail", &repo_url, "test-token") .overrides( "alpine:latest", "mkdir -p out && echo '

ok

' > out/index.html", "out", ) .post_deploy(vec!["false".to_owned()]) .build(); let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; let resp = TestServer::client() .post(server.url("/hook-fail")) .header("Authorization", "Bearer test-token") .send() .await .unwrap(); assert_eq!(resp.status().as_u16(), 202); // Wait for build to complete let builds_dir = base_dir.join("builds/hook-fail"); let max_wait = Duration::from_secs(120); let start = std::time::Instant::now(); loop { assert!(start.elapsed() <= max_wait, "build timed out"); if builds_dir.join("current").is_symlink() { tokio::time::sleep(Duration::from_secs(3)).await; break; } tokio::time::sleep(Duration::from_millis(500)).await; } // Deploy succeeded despite hook failure let current_target = tokio::fs::read_link(builds_dir.join("current")) .await .expect("current symlink should exist"); assert!( current_target.join("index.html").exists(), "built assets should exist despite hook failure" ); // Hook log should have been written with failure status let logs_dir = base_dir.join("logs/hook-fail"); let mut found_hook_log = false; let mut entries = tokio::fs::read_dir(&logs_dir).await.unwrap(); while let Some(entry) = entries.next_entry().await.unwrap() { let name = entry.file_name(); if name.to_string_lossy().ends_with("-hook.log") { found_hook_log = true; let content = tokio::fs::read_to_string(entry.path()).await.unwrap(); assert!(content.contains("=== HOOK LOG ===")); assert!(content.contains("Status: failed")); break; } } assert!(found_hook_log, "hook log should exist for failed hook"); } #[tokio::test] async fn post_deploy_hook_runs_on_build_failure() { skip_without_git!(); skip_without_runtime!(); let tempdir = tempfile::tempdir().unwrap(); let base_dir = tempdir.path().to_path_buf(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_local_repo(&repo_dir, "main").await; // Build command fails (exit 1); hook writes WITRYNA_BUILD_STATUS to a file in clone dir let site = SiteBuilder::new("hook-on-fail", &repo_url, "test-token") .overrides("alpine:latest", "exit 1", "out") .post_deploy(vec![ "sh".to_owned(), "-c".to_owned(), "echo \"$WITRYNA_BUILD_STATUS\" > hook-status.txt".to_owned(), ]) .build(); let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; let resp = TestServer::client() .post(server.url("/hook-on-fail")) .header("Authorization", "Bearer test-token") .send() .await .unwrap(); assert_eq!(resp.status().as_u16(), 202); // Wait for state.json to show "failed" (no current symlink on build failure) let state_path = base_dir.join("builds/hook-on-fail/state.json"); let max_wait = Duration::from_secs(120); let start = std::time::Instant::now(); loop { assert!(start.elapsed() <= max_wait, "build timed out"); if state_path.exists() { let content = tokio::fs::read_to_string(&state_path) .await .unwrap_or_default(); if content.contains("\"failed\"") { // Give the hook a moment to finish writing tokio::time::sleep(Duration::from_secs(2)).await; break; } } tokio::time::sleep(Duration::from_millis(500)).await; } // Verify hook ran and received build_status=failed let clone_dir = base_dir.join("clones/hook-on-fail"); let hook_status_path = clone_dir.join("hook-status.txt"); assert!( hook_status_path.exists(), "hook should have created hook-status.txt in clone dir" ); let status = tokio::fs::read_to_string(&hook_status_path).await.unwrap(); assert_eq!( status.trim(), "failed", "hook should receive build_status=failed" ); // Verify state.json says "failed" (not "hook failed") let state_content = tokio::fs::read_to_string(&state_path).await.unwrap(); assert!( state_content.contains("\"failed\""), "state.json should show failed status" ); // No current symlink should exist (build failed) assert!( !Path::new(&base_dir.join("builds/hook-on-fail/current")).is_symlink(), "current symlink should not exist on build failure" ); } #[tokio::test] async fn post_deploy_hook_receives_success_status() { skip_without_git!(); skip_without_runtime!(); let tempdir = tempfile::tempdir().unwrap(); let base_dir = tempdir.path().to_path_buf(); let repo_dir = tempdir.path().join("repos"); tokio::fs::create_dir_all(&repo_dir).await.unwrap(); let repo_url = create_local_repo(&repo_dir, "main").await; // Successful build; hook writes WITRYNA_BUILD_STATUS to build dir let site = SiteBuilder::new("hook-success-status", &repo_url, "test-token") .overrides( "alpine:latest", "mkdir -p out && echo test > out/index.html", "out", ) .post_deploy(vec![ "sh".to_owned(), "-c".to_owned(), "echo \"$WITRYNA_BUILD_STATUS\" > \"$WITRYNA_BUILD_DIR/build-status.txt\"".to_owned(), ]) .build(); let server = TestServer::start(test_config_with_site(base_dir.clone(), site)).await; let resp = TestServer::client() .post(server.url("/hook-success-status")) .header("Authorization", "Bearer test-token") .send() .await .unwrap(); assert_eq!(resp.status().as_u16(), 202); // Wait for current symlink let builds_dir = base_dir.join("builds/hook-success-status"); let max_wait = Duration::from_secs(120); let start = std::time::Instant::now(); loop { assert!(start.elapsed() <= max_wait, "build timed out"); if builds_dir.join("current").is_symlink() { tokio::time::sleep(Duration::from_secs(3)).await; break; } tokio::time::sleep(Duration::from_millis(500)).await; } // Read build-status.txt from build dir let current_target = tokio::fs::read_link(builds_dir.join("current")) .await .expect("current symlink should exist"); let status_path = current_target.join("build-status.txt"); assert!( status_path.exists(), "hook should have created build-status.txt" ); let status = tokio::fs::read_to_string(&status_path).await.unwrap(); assert_eq!( status.trim(), "success", "hook should receive build_status=success" ); }