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::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"); }