use aptu_core::git::patch::{PatchError, PatchStep, apply_patch_and_push};
use serial_test::serial;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
const SIMPLE_DIFF: &str = include_str!("patch_fixtures/simple.diff");
fn setup_repo() -> (TempDir, TempDir) {
let work = TempDir::new().expect("create work tmpdir");
let bare = TempDir::new().expect("create bare tmpdir");
run(
&[
"git",
"init",
"--bare",
bare.path().to_str().expect("bare path"),
],
bare.path(),
);
let w = work.path();
run(
&["git", "init", "-b", "main", w.to_str().expect("work path")],
w,
);
git(w, &["config", "user.email", "test@example.com"]);
git(w, &["config", "user.name", "Test User"]);
git(w, &["config", "commit.gpgSign", "false"]);
let hooks_dir = work.path().join("hooks");
std::fs::create_dir_all(&hooks_dir).expect("create hooks dir");
git(
w,
&[
"config",
"core.hooksPath",
hooks_dir.to_str().expect("hooks path"),
],
);
git(
w,
&[
"remote",
"add",
"origin",
bare.path().to_str().expect("bare path"),
],
);
std::fs::write(w.join("hello.txt"), "hello world\n").expect("write hello.txt");
git(w, &["add", "hello.txt"]);
git(w, &["commit", "-m", "initial commit"]);
git(w, &["push", "origin", "main"]);
(work, bare)
}
fn run(cmd: &[&str], cwd: &Path) {
let status = Command::new(cmd[0])
.args(&cmd[1..])
.current_dir(cwd)
.status()
.expect("spawn command");
assert!(status.success(), "command failed: {cmd:?}");
}
fn git(cwd: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(cwd)
.status()
.expect("spawn git");
assert!(status.success(), "git {args:?} failed in {cwd:?}");
}
fn write_diff(dir: &Path) -> std::path::PathBuf {
let p = dir.join("patch.diff");
std::fs::write(&p, SIMPLE_DIFF).expect("write diff fixture");
p
}
fn collecting_progress() -> (
std::sync::Arc<std::sync::Mutex<Vec<PatchStep>>>,
impl Fn(PatchStep),
) {
let steps = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let steps_clone = steps.clone();
let cb = move |s: PatchStep| {
steps_clone.lock().expect("lock steps").push(s);
};
(steps, cb)
}
#[tokio::test]
async fn test_apply_patch_happy_path() {
let (work, _bare) = setup_repo();
let w = work.path();
let diff_path = write_diff(w);
let (steps, progress) = collecting_progress();
let branch = apply_patch_and_push(
&diff_path,
w,
Some("test/happy-path"),
"main",
"test: happy path",
false,
false,
progress,
)
.await
.expect("apply_patch_and_push should succeed");
assert_eq!(branch, "test/happy-path");
let log = Command::new("git")
.args(["log", "--oneline", "test/happy-path"])
.current_dir(w)
.output()
.expect("git log");
let log_str = String::from_utf8_lossy(&log.stdout);
assert!(
log_str.contains("test: happy path"),
"commit not found in log: {log_str}"
);
let recorded = steps.lock().expect("lock").clone();
assert!(
recorded.contains(&PatchStep::Pushing),
"expected Pushing step, got: {recorded:?}"
);
}
#[tokio::test]
async fn test_apply_patch_bad_patch_rejected() {
let (work, _bare) = setup_repo();
let w = work.path();
let bad_diff =
"--- a/hello.txt\n+++ b/hello.txt\n@@ -1 +1 @@\n-nonexistent line\n+hello aptu\n";
let diff_path = w.join("bad.diff");
std::fs::write(&diff_path, bad_diff).expect("write bad diff");
let result = apply_patch_and_push(
&diff_path,
w,
Some("test/bad-patch"),
"main",
"test: bad patch",
false,
false,
|_| {},
)
.await;
assert!(
matches!(result, Err(PatchError::ApplyCheckFailed { .. })),
"expected ApplyCheckFailed, got: {result:?}"
);
}
#[tokio::test]
async fn test_apply_patch_branch_collision_suffix() {
let (work, _bare) = setup_repo();
let w = work.path();
git(w, &["checkout", "-b", "test/collision", "origin/main"]);
git(w, &["push", "origin", "test/collision"]);
git(w, &["checkout", "main"]);
let diff_path = write_diff(w);
let branch = apply_patch_and_push(
&diff_path,
w,
Some("test/collision"),
"main",
"test: collision",
false,
false,
|_| {},
)
.await
.expect("apply_patch_and_push should succeed with suffixed branch");
assert_ne!(
branch, "test/collision",
"expected a suffixed branch name, got: {branch}"
);
assert!(
branch.starts_with("test/collision-"),
"expected suffix after 'test/collision', got: {branch}"
);
}
#[tokio::test]
async fn test_apply_patch_dco_signoff() {
let (work, _bare) = setup_repo();
let w = work.path();
let diff_path = write_diff(w);
apply_patch_and_push(
&diff_path,
w,
Some("test/dco"),
"main",
"test: dco signoff",
true, false,
|_| {},
)
.await
.expect("apply_patch_and_push should succeed");
let log = Command::new("git")
.args(["log", "--format=%B", "-n", "1", "test/dco"])
.current_dir(w)
.output()
.expect("git log");
let log_str = String::from_utf8_lossy(&log.stdout);
assert!(
log_str.contains("Signed-off-by:"),
"expected Signed-off-by trailer in commit, got: {log_str}"
);
}
#[tokio::test]
#[ignore]
async fn test_apply_patch_signing_gate() {
let (work, _bare) = setup_repo();
let w = work.path();
git(w, &["config", "commit.gpgSign", "true"]);
let diff_path = write_diff(w);
let branch = apply_patch_and_push(
&diff_path,
w,
Some("test/gpg"),
"main",
"test: gpg signing",
false,
false,
|_| {},
)
.await
.expect("apply_patch_and_push should succeed with GPG signing");
assert_eq!(branch, "test/gpg");
}
#[tokio::test]
#[serial]
async fn test_apply_patch_security_force_flag() {
let (work, _bare) = setup_repo();
let w = work.path();
let patch_with_secret =
"--- a/hello.txt\n+++ b/hello.txt\n@@ -1 +1 @@\n-hello world\n+password=\"secret123\"\n";
let patch_path = w.join("security.diff");
std::fs::write(&patch_path, patch_with_secret).expect("write security patch");
let result_no_force = apply_patch_and_push(
&patch_path,
w,
Some("test/security-reject"),
"main",
"test: security reject",
false,
false, |_| {},
)
.await;
assert!(
matches!(result_no_force, Err(PatchError::SecurityFindings { .. })),
"expected SecurityFindings error with force=false, got: {result_no_force:?}"
);
let result_force = apply_patch_and_push(
&patch_path,
w,
Some("test/security-accept"),
"main",
"test: security accept",
false,
true, |_| {},
)
.await;
assert!(
matches!(result_force, Ok(ref branch) if branch == "test/security-accept"),
"expected Ok with force=true, got: {result_force:?}"
);
}