use std::fs;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn cc_hook() -> Command {
Command::cargo_bin("cc-hook").expect("binary not found")
}
fn fake_repo() -> TempDir {
let tmp = tempfile::tempdir().expect("failed to create temp dir");
fs::create_dir_all(tmp.path().join(".git").join("hooks")).unwrap();
tmp
}
fn commit_msg_file(content: &str) -> (TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().expect("failed to create temp dir");
let path = tmp.path().join("COMMIT_EDITMSG");
fs::write(&path, content).unwrap();
(tmp, path)
}
#[test]
fn no_args_shows_usage() {
cc_hook()
.assert()
.success()
.stdout(predicate::str::contains("cc-hook - Conventional Commits Hook"))
.stdout(predicate::str::contains("cc-hook install"))
.stdout(predicate::str::contains("cc-hook validate"));
}
#[test]
fn validate_valid_message_exits_0() {
let (_tmp, path) = commit_msg_file("feat: add new feature");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn validate_valid_message_with_scope_exits_0() {
let (_tmp, path) = commit_msg_file("fix(api): resolve timeout");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn validate_merge_commit_exits_0() {
let (_tmp, path) = commit_msg_file("Merge branch 'develop' into main");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn validate_invalid_message_exits_1() {
let (_tmp, path) = commit_msg_file("bad commit message");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.failure()
.code(1)
.stdout(predicate::str::contains("Conventional Commits"))
.stdout(predicate::str::contains("bad commit message"));
}
#[test]
fn validate_invalid_message_shows_valid_types() {
let (_tmp, path) = commit_msg_file("oops");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.failure()
.stdout(predicate::str::contains("feat"))
.stdout(predicate::str::contains("fix"))
.stdout(predicate::str::contains("docs"))
.stdout(predicate::str::contains("refactor"));
}
#[test]
fn validate_invalid_message_shows_examples() {
let (_tmp, path) = commit_msg_file("nope");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.failure()
.stdout(predicate::str::contains("feat(auth): adicionar"))
.stdout(predicate::str::contains("fix(api): corrigir"));
}
#[test]
fn validate_empty_file_exits_1() {
let (_tmp, path) = commit_msg_file("");
cc_hook()
.args(["validate", path.to_str().unwrap()])
.assert()
.failure()
.code(1)
.stdout(predicate::str::contains("ERRO:"))
.stdout(predicate::str::contains("vazia"));
}
#[test]
fn validate_nonexistent_file_exits_with_error() {
cc_hook()
.args(["validate", "/tmp/does_not_exist_cch_test.txt"])
.assert()
.failure();
}
#[test]
fn install_creates_hook_in_repo() {
let repo = fake_repo();
cc_hook()
.arg("install")
.current_dir(repo.path())
.assert()
.success()
.stdout(predicate::str::contains("SUCESSO:"))
.stdout(predicate::str::contains("commit-msg configurado"));
let hook = repo.path().join(".git").join("hooks").join("commit-msg");
assert!(hook.exists(), "hook file should be created");
let content = fs::read_to_string(&hook).unwrap();
assert!(content.contains("cc-hook validate"));
}
#[test]
fn install_with_existing_hook_and_no_input_cancels() {
let repo = fake_repo();
let hook = repo.path().join(".git").join("hooks").join("commit-msg");
fs::write(&hook, "#!/bin/sh\necho old").unwrap();
cc_hook()
.arg("install")
.current_dir(repo.path())
.write_stdin("")
.assert()
.success()
.stdout(predicate::str::contains("cancelada"));
let content = fs::read_to_string(&hook).unwrap();
assert_eq!(content, "#!/bin/sh\necho old");
}
#[test]
fn install_with_existing_hook_and_yes_creates_backup() {
let repo = fake_repo();
let hook = repo.path().join(".git").join("hooks").join("commit-msg");
fs::write(&hook, "#!/bin/sh\necho old").unwrap();
cc_hook()
.arg("install")
.current_dir(repo.path())
.write_stdin("y\n")
.assert()
.success()
.stdout(predicate::str::contains("Backup criado"))
.stdout(predicate::str::contains("SUCESSO:"));
let content = fs::read_to_string(&hook).unwrap();
assert!(content.contains("cc-hook validate"));
let hooks_dir = repo.path().join(".git").join("hooks");
let backups: Vec<_> = fs::read_dir(&hooks_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.starts_with("commit-msg.backup"))
})
.collect();
assert_eq!(backups.len(), 1, "exactly one backup should exist");
let backup_content = fs::read_to_string(backups[0].path()).unwrap();
assert_eq!(backup_content, "#!/bin/sh\necho old");
}
#[test]
fn install_without_git_dir_exits_1() {
let tmp = tempfile::tempdir().unwrap();
cc_hook()
.arg("install")
.current_dir(tmp.path())
.assert()
.failure()
.code(1)
.stdout(predicate::str::contains("ERRO:"))
.stdout(predicate::str::contains(".git"));
}