Menu
AkurAI-Build
publicLatest change 23ae2d15410185bdd0a33888ddbbf8fc1a1d8fea - queue: integrate ref-guard, wire merge-queue worker/recovery, record CI run id by Ólafur Búi Ólafsson
//! Integration tests for the protected Git ref transaction guard.
//!
//! Owned slice: this file only. Included via `#[path]` so no edit to
//! `lib.rs` (owned by the primary integration task) is needed.
#[path = "../src/ref_guard.rs"]
mod ref_guard;
use ref_guard::compare_and_swap;
use std::fs;
use std::process::Command;
fn init_repo(dir: &std::path::Path) {
assert!(
Command::new("git")
.arg("init")
.args(["-b", "main"])
.arg(dir)
.status()
.expect("run git init")
.success(),
"git init failed"
);
}
fn commit(dir: &std::path::Path, message: &str) -> String {
assert!(
Command::new("git")
.current_dir(dir)
.args([
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"--allow-empty",
"-m",
message,
])
.status()
.expect("run git commit")
.success(),
"git commit failed"
);
let output = Command::new("git")
.current_dir(dir)
.args(["rev-parse", "HEAD"])
.output()
.expect("run git rev-parse");
String::from_utf8(output.stdout)
.expect("valid utf-8 sha")
.trim()
.to_owned()
}
#[test]
fn compare_and_swap_moves_ref_when_expected_matches() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let first = commit(&repo, "first");
let second = commit(&repo, "second");
// main already points at `second` after two commits; reset it back
// to `first` so we can exercise the CAS moving it forward again.
assert!(
Command::new("git")
.current_dir(&repo)
.args(["update-ref", "refs/heads/main", &first])
.status()
.expect("reset ref")
.success()
);
let result = compare_and_swap(&repo, "refs/heads/main", &first, &second);
assert!(result.is_ok(), "CAS should succeed: {result:?}");
let output = Command::new("git")
.current_dir(&repo)
.args(["rev-parse", "refs/heads/main"])
.output()
.expect("read ref");
let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
assert_eq!(head, second, "ref should now point at second commit");
}
#[test]
fn compare_and_swap_fails_when_expected_does_not_match() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let first = commit(&repo, "first");
let second = commit(&repo, "second");
let bogus = "cafecafecafecafecafecafecafecafecafecafe";
let result = compare_and_swap(&repo, "refs/heads/main", &first, &second);
assert!(
result.is_err(),
"CAS should fail: ref already moved to second, not first"
);
let result_bogus = compare_and_swap(&repo, "refs/heads/main", bogus, &second);
assert!(result_bogus.is_err(), "CAS with wrong expected should fail");
}
#[test]
fn compare_and_swap_rejects_invalid_ref_name() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let first = commit(&repo, "first");
let zero = "0000000000000000000000000000000000000000";
let result = compare_and_swap(&repo, "not..valid..ref", zero, &first);
assert!(result.is_err(), "invalid ref name should be rejected");
}
#[test]
fn compare_and_swap_rejects_invalid_oid() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let result = compare_and_swap(&repo, "refs/heads/main", "not-hex", "also-not-hex");
assert!(result.is_err(), "invalid oid should be rejected");
}
#[test]
fn compare_and_swap_deletes_ref_when_expected_matches() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let target = commit(&repo, "target");
let zero = "0000000000000000000000000000000000000000";
assert!(
Command::new("git")
.current_dir(&repo)
.args(["branch", "throwaway"])
.status()
.expect("create branch")
.success()
);
let result = compare_and_swap(&repo, "refs/heads/throwaway", &target, zero);
assert!(result.is_ok(), "CAS delete should succeed: {result:?}");
let output = Command::new("git")
.current_dir(&repo)
.args(["show-ref", "refs/heads/throwaway"])
.output()
.expect("show-ref");
assert!(
!output.status.success(),
"ref should no longer exist after CAS delete"
);
}
#[test]
fn compare_and_swap_delete_fails_when_expected_stale() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let first = commit(&repo, "first");
let second = commit(&repo, "second");
let zero = "0000000000000000000000000000000000000000";
// main is now at `second`; try to delete it under the stale
// expectation that it is still at `first`.
let result = compare_and_swap(&repo, "refs/heads/main", &first, zero);
assert!(
result.is_err(),
"CAS delete with stale expected oid must fail"
);
let output = Command::new("git")
.current_dir(&repo)
.args(["rev-parse", "refs/heads/main"])
.output()
.expect("read ref");
let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
assert_eq!(
head, second,
"ref must be untouched after failed CAS delete"
);
}
#[test]
fn concurrent_compare_and_swap_exactly_one_writer_wins() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let base = commit(&repo, "base");
// Two independent target shas both racing to CAS refs/heads/main
// forward from the same observed `base`. Exactly one may win;
// the loser must fail, and the ref must land on the winner's sha.
assert!(
Command::new("git")
.current_dir(&repo)
.args(["checkout", "-b", "side"])
.status()
.expect("checkout side branch")
.success()
);
let side_target = commit(&repo, "side-commit");
assert!(
Command::new("git")
.current_dir(&repo)
.args(["checkout", "main"])
.status()
.expect("checkout main")
.success()
);
let main_target = commit(&repo, "main-commit");
// Reset main back to base so both CAS attempts race from the same
// observed starting point.
assert!(
Command::new("git")
.current_dir(&repo)
.args(["update-ref", "refs/heads/main", &base])
.status()
.expect("reset ref")
.success()
);
let repo_a = repo.clone();
let repo_b = repo.clone();
let base_a = base.clone();
let base_b = base.clone();
let target_a = main_target.clone();
let target_b = side_target.clone();
let handle_a = std::thread::spawn(move || {
compare_and_swap(&repo_a, "refs/heads/main", &base_a, &target_a)
});
let handle_b = std::thread::spawn(move || {
compare_and_swap(&repo_b, "refs/heads/main", &base_b, &target_b)
});
let result_a = handle_a.join().expect("thread a joins");
let result_b = handle_b.join().expect("thread b joins");
let ok_count = [&result_a, &result_b].iter().filter(|r| r.is_ok()).count();
assert_eq!(
ok_count, 1,
"exactly one concurrent CAS writer must win: a={result_a:?} b={result_b:?}"
);
let output = Command::new("git")
.current_dir(&repo)
.args(["rev-parse", "refs/heads/main"])
.output()
.expect("read ref");
let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
let winner = if result_a.is_ok() {
&main_target
} else {
&side_target
};
assert_eq!(&head, winner, "ref must land on the winning CAS target");
}
/// Capture the literal wire bytes a stock `git send-pack` sends for a
/// push, by substituting `--receive-pack` with a relay script that
/// tees stdin to disk before handing it to the real `git receive-pack`.
/// This is the durable equivalent of the throwaway `.tmp/protocol_probe`
/// used during review: it exercises the actual Git binary on this host
/// (2.55.0 at time of writing) rather than a synthetic byte layout, so
/// a future Git wire-format change would be caught by CI instead of
/// only being caught by manual review probing.
///
/// Bounded: the underlying `git push` is wall-clock timed and this
/// panics if it exceeds `CAPTURE_TIMEOUT`, so a hung subprocess (stuck
/// waiting on a credential prompt, an unreachable transport, etc.)
/// fails the test loudly instead of hanging the suite indefinitely.
/// `GIT_TERMINAL_PROMPT=0` additionally prevents interactive prompts
/// from ever blocking on stdin.
fn capture_real_send_pack_wire_bytes(branch: &str) -> Vec<u8> {
const CAPTURE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
let dir = tempfile::tempdir().expect("create temp dir");
let remote = dir.path().join("remote.git");
assert!(
Command::new("git")
.args(["init", "--bare", "-b", "main"])
.arg(&remote)
.status()
.expect("git init --bare")
.success(),
"bare remote init failed"
);
let client = dir.path().join("client");
fs::create_dir(&client).expect("create client dir");
init_repo(&client);
commit(&client, "initial");
if branch != "main" {
assert!(
Command::new("git")
.current_dir(&client)
.args(["checkout", "-b", branch])
.status()
.expect("checkout branch")
.success(),
"branch checkout failed"
);
commit(&client, "on branch");
}
let request_path = dir.path().join("request.bin");
let relay_script = dir.path().join("relay-receive-pack.sh");
fs::write(
&relay_script,
format!(
"#!/bin/sh\nset -e\ntee \"{}\" | git receive-pack \"$1\"\n",
request_path.display()
),
)
.expect("write relay script");
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&relay_script)
.expect("stat relay script")
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&relay_script, perms).expect("chmod relay script");
}
let start = std::time::Instant::now();
let output = Command::new("git")
.current_dir(&client)
.env("GIT_TERMINAL_PROMPT", "0")
.args([
"push",
&format!("--receive-pack={}", relay_script.display()),
remote.to_str().expect("utf-8 remote path"),
branch,
])
.output()
.expect("run git push through capture relay");
let elapsed = start.elapsed();
assert!(
elapsed < CAPTURE_TIMEOUT,
"capturing a real send-pack request must be bounded: took {elapsed:?}"
);
assert!(
output.status.success(),
"real git push through the capture relay must succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
fs::read(&request_path).expect("read captured wire bytes")
}
#[test]
fn real_send_pack_wire_request_for_unprotected_ref_is_accepted() {
let body = capture_real_send_pack_wire_bytes("feature");
let result = ref_guard::protected_receive_commands(&body, &[]);
assert!(
result.is_ok(),
"a real stock git send-pack request to an unprotected ref must parse and pass: {result:?}"
);
}
#[test]
fn real_send_pack_wire_request_denied_for_protected_ref() {
let body = capture_real_send_pack_wire_bytes("main");
let protected = vec!["refs/heads/main".to_owned()];
let result = ref_guard::protected_receive_commands(&body, &protected);
assert!(
result.is_err(),
"a real stock git send-pack request targeting a protected ref must be denied"
);
}
#[test]
fn real_send_pack_wire_request_truncated_is_rejected() {
let body = capture_real_send_pack_wire_bytes("feature");
// Truncate mid-stream (well before the flush-pkt / packfile) to
// simulate a cut connection or a malicious short write; parsing
// real captured bytes this way must still fail closed rather than
// panic or silently accept a partial command set.
let truncated = &body[..body.len().min(40)];
let result = ref_guard::protected_receive_commands(truncated, &[]);
assert!(
result.is_err(),
"a truncated real wire request must be rejected, not silently accepted"
);
}
#[test]
fn compare_and_swap_creates_new_ref_from_zero_oid() {
let dir = tempfile::tempdir().expect("create temp dir");
let repo = dir.path().join("repo");
fs::create_dir(&repo).expect("create repo dir");
init_repo(&repo);
let target = commit(&repo, "target");
let zero = "0000000000000000000000000000000000000000";
let result = compare_and_swap(&repo, "refs/heads/newbranch", zero, &target);
assert!(
result.is_ok(),
"creating a new ref via CAS should succeed: {result:?}"
);
let output = Command::new("git")
.current_dir(&repo)
.args(["rev-parse", "refs/heads/newbranch"])
.output()
.expect("read new ref");
let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
assert_eq!(head, target);
}