use super::commit::{remove_settled, wrapped_commit};
use super::support::{Fixture, Observed, fixture, git, repository, run_direct, run_wrapped, start_fixture_service, FixtureService};
use git_policy_cli::iso_time::now_iso;
use git_policy_cli::push_failure_notice::notice_directory;
use git_policy_cli::service_client::{Activation, ClientConfig, wake};
use git_policy_cli::service_route::SOCKET_RELATIVE;
use git_policy_cli::service_server::Bind;
use git_policy_cli::service_work::work_directory;
use git_policy_cli::work_item::{WorkItem, WorkKind, enqueue};
use std::ffi::OsStr;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{Duration, Instant};
const REFUSAL: &str = "refusing this push by test policy";
const PATIENCE: Duration = Duration::from_secs(60);
struct Setup {
fixture: Fixture,
repo: PathBuf,
remote: PathBuf,
service: FixtureService,
}
fn setup(name: &str) -> Setup {
let fixture: Fixture = fixture(name);
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
let remote: PathBuf = fixture.root.join("remote.git");
git(&fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", remote.to_str().expect("UTF-8")]);
std::fs::write(repo.join("a.txt"), "base\n").expect("a");
git(&fixture, &repo, &["add", "a.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(&fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
git(&fixture, &repo, &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
git(&fixture, &repo, &["push", "--quiet", "--set-upstream", "origin", "main"]);
let service: FixtureService = start_fixture_service(&fixture);
return Setup { fixture, repo, remote, service };
}
fn refuse_pushes(remote: &Path) {
let hook: PathBuf = remote.join("hooks/pre-receive");
std::fs::create_dir_all(hook.parent().expect("hooks directory")).expect("hooks");
std::fs::write(&hook, format!("#!/bin/sh\necho '{REFUSAL}' >&2\nexit 1\n")).expect("hook");
std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).expect("executable");
}
fn accept_pushes(remote: &Path) {
std::fs::remove_file(remote.join("hooks/pre-receive")).expect("hook removed");
}
fn commit_through_service(setup: &Setup, content: &str) -> String {
std::fs::write(setup.repo.join("a.txt"), content).expect("edit");
let mut command: Command = wrapped_commit(&setup.fixture, &setup.repo, &["commit", "--quiet", "-m", content.trim(), "--", "a.txt"]);
command.env("XDG_RUNTIME_DIR", &setup.service.runtime);
let output: Output = command.output().expect("the wrapped commit runs");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let named = git(&setup.fixture, &setup.repo, &["rev-parse", "HEAD"]);
return String::from_utf8_lossy(&named.stdout).trim().to_string();
}
fn wait_until(what: &str, condition: &dyn Fn() -> bool) {
let began: Instant = Instant::now();
while !condition() {
assert!(began.elapsed() < PATIENCE, "never happened: {what}");
std::thread::sleep(Duration::from_millis(10));
}
}
fn common_dir(setup: &Setup) -> PathBuf {
return setup.repo.join(".git");
}
fn waiting_records(setup: &Setup) -> usize {
return std::fs::read_dir(notice_directory(common_dir(setup).as_path())).map_or(0, |listing| { return listing.flatten().count(); });
}
fn wait_until_settled(setup: &Setup, commit: &str) {
let work: PathBuf = common_dir(setup).join("cli-git/work");
wait_until("the service settled the push", &|| {
return ["done", "failed"].iter().any(|state| { return work.join(state).join(format!("push-{commit}")).exists(); });
});
}
fn stderr_text(observed: &Observed) -> String {
return String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
}
#[test]
fn a_rejected_service_push_is_shown_on_the_next_command() {
let setup: Setup = setup("push-failure-shown");
refuse_pushes(setup.remote.as_path());
let commit: String = commit_through_service(&setup, "second\n");
wait_until_settled(&setup, commit.as_str());
let shown: Observed = run_wrapped(&setup.fixture, &setup.repo, &["status", "--short"]);
let text: String = stderr_text(&shown);
assert_eq!(shown.code, Some(0), "{text}");
for expected in ["branch main", &commit[..7], REFUSAL, "saved in this repository", "`git push`"] {
assert!(text.contains(expected), "missing {expected:?} in: {text}");
}
remove_settled(&setup.fixture);
}
#[test]
fn a_shown_failure_is_not_shown_again() {
let setup: Setup = setup("push-failure-once");
refuse_pushes(setup.remote.as_path());
let commit: String = commit_through_service(&setup, "second\n");
wait_until_settled(&setup, commit.as_str());
let first: String = stderr_text(&run_wrapped(&setup.fixture, &setup.repo, &["status", "--short"]));
assert!(first.contains(REFUSAL), "{first}");
let second: Observed = run_wrapped(&setup.fixture, &setup.repo, &["status", "--short"]);
assert_eq!(stderr_text(&second), "", "the same failure again");
let third: Observed = run_wrapped(&setup.fixture, &setup.repo, &["log", "--oneline"]);
assert_eq!(stderr_text(&third), "", "the same failure again, in another command");
assert_eq!(waiting_records(&setup), 0);
remove_settled(&setup.fixture);
}
fn push_through_service(setup: &Setup, commit: &str) {
let item: WorkItem = WorkItem {
kind: WorkKind::Push,
repository: setup.repo.display().to_string(),
git_dir: common_dir(setup).display().to_string(),
branch_ref: String::from("refs/heads/main"),
oid: commit.to_string(),
parent: String::new(),
created: now_iso(),
tries: 0,
};
enqueue(work_directory(common_dir(setup).as_path()).as_path(), &item).expect("the push is recorded for the service");
let config: ClientConfig = ClientConfig {
address: Bind::Path(setup.service.runtime.join(SOCKET_RELATIVE)),
activation: Activation::Managed,
start_wait: PATIENCE,
commit_wait: PATIENCE,
};
wake(&config, common_dir(setup).as_path(), None, &mut std::io::sink()).expect("the service takes the wake");
wait_until_settled(setup, commit);
}
#[test]
fn a_later_successful_push_clears_the_unshown_failure() {
let setup: Setup = setup("push-failure-cleared");
refuse_pushes(setup.remote.as_path());
let failed: String = commit_through_service(&setup, "second\n");
wait_until_settled(&setup, failed.as_str());
assert_eq!(waiting_records(&setup), 1, "the refusal was recorded");
accept_pushes(setup.remote.as_path());
std::fs::write(setup.repo.join("a.txt"), "third\n").expect("edit");
git(&setup.fixture, &setup.repo, &["commit", "--quiet", "--all", "--message=third"]);
let named = git(&setup.fixture, &setup.repo, &["rev-parse", "HEAD"]);
let pushed: String = String::from_utf8_lossy(&named.stdout).trim().to_string();
push_through_service(&setup, pushed.as_str());
assert_eq!(waiting_records(&setup), 0, "the push that went through cleared the record");
let after: Observed = run_wrapped(&setup.fixture, &setup.repo, &["status", "--short"]);
assert_eq!(stderr_text(&after), "");
remove_settled(&setup.fixture);
}
#[test]
fn machine_read_output_is_untouched_and_keeps_the_failure_for_a_person() {
let setup: Setup = setup("push-failure-porcelain");
refuse_pushes(setup.remote.as_path());
let commit: String = commit_through_service(&setup, "second\n");
wait_until_settled(&setup, commit.as_str());
std::fs::write(setup.repo.join("untracked.txt"), "new\n").expect("untracked");
for arguments in [&["status", "--porcelain=v2", "--branch"][..], &["status", "-z"], &["rev-parse", "HEAD"], &["log", "-1", "--format=%H"]] {
let wrapped: Observed = run_wrapped(&setup.fixture, &setup.repo, arguments);
let direct: Observed = run_direct(&setup.fixture, &setup.repo, arguments);
assert_eq!(wrapped.stdout, direct.stdout, "{arguments:?}");
assert_eq!(stderr_text(&wrapped), stderr_text(&direct), "{arguments:?}");
assert_eq!(waiting_records(&setup), 1, "{arguments:?} took the record meant for a person");
}
let human: String = stderr_text(&run_wrapped(&setup.fixture, &setup.repo, &["status", "--short"]));
assert!(human.contains(REFUSAL), "{human}");
remove_settled(&setup.fixture);
}