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