tests/lease_cli_test.rs
Ref: Size: 6.8 KiB History
mod common;
use std::path::Path;
use std::process::Output;
use common::ServerHarness;
/// Run `git-collab issue …` in the harness work repo against the harness SSH
/// server, as the default client key.
fn issue_cmd(harness: &ServerHarness, args: &[&str]) -> Output {
issue_cmd_as(harness, &harness.ssh_client_key(), args)
}
/// Same, as a specific key — a lease conflict needs two principals.
fn issue_cmd_as(harness: &ServerHarness, key: &Path, args: &[&str]) -> Output {
let mut cmd = harness.work_repo().cli_command();
cmd.env(
"GIT_COLLAB_SSH_COMMAND",
harness.ssh_command_string_for(key),
);
cmd.args(["issue"]).args(args);
cmd.output().expect("failed to run git-collab issue")
}
fn out(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).to_string()
}
fn err(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
/// A harness whose work repo has an `srv` SSH remote and one open issue,
/// already pushed. Returns the issue id.
fn setup(name: &str) -> (ServerHarness, String) {
let harness = ServerHarness::new(name);
harness.push_head();
let url = harness.repo_ssh_url();
harness.work_repo().git(&["remote", "add", "srv", &url]);
let (_ref_name, id) = common::open_issue(
&harness.work_repo_git2(),
&common::alice(),
"parser chokes on empty input",
);
harness.push_collab_refs();
(harness, id)
}
#[test]
fn claim_then_claims_lists_it() {
let (harness, id) = setup("cli-lease-claim");
// Use the same abbreviated id that `issue list` prints.
let claim = issue_cmd(&harness, &["claim", &id[..8], "--remote", "srv"]);
assert!(
claim.status.success(),
"claim failed: {}{}",
out(&claim),
err(&claim)
);
assert!(out(&claim).contains("Claimed issue"), "{}", out(&claim));
let claims = issue_cmd(&harness, &["claims", "--remote", "srv"]);
assert!(claims.status.success());
assert!(
out(&claims).contains(&id[..8]),
"claims should list the issue: {}",
out(&claims)
);
// No --ttl, so the claim is an assignment rather than a lease.
assert!(out(&claims).contains("assigned"), "{}", out(&claims));
// Prose abbreviates like every other command, even though the server
// answers with the full id — which `--json` still carries. This is the
// house convention, not an accident of formatting.
assert!(
!out(&claim).contains(&id),
"prose should abbreviate the id, not print all 40 characters: {}",
out(&claim)
);
assert!(
!out(&claims).contains(&id),
"prose should abbreviate the id in the claims list: {}",
out(&claims)
);
}
#[test]
fn claims_with_no_claims_says_so() {
let (harness, _id) = setup("cli-lease-empty");
let claims = issue_cmd(&harness, &["claims", "--remote", "srv"]);
assert!(claims.status.success());
assert!(out(&claims).contains("No claims."), "{}", out(&claims));
}
#[test]
fn claim_conflict_exits_4_and_names_the_holder() {
let (harness, id) = setup("cli-lease-conflict");
let first = harness.ssh_client_key();
let second = harness.second_authorized_key();
let mine = issue_cmd_as(
&harness,
&first,
&["claim", &id, "--ttl", "300", "--remote", "srv"],
);
assert!(mine.status.success(), "{}{}", out(&mine), err(&mine));
let theirs = issue_cmd_as(
&harness,
&second,
&["claim", &id, "--ttl", "300", "--remote", "srv"],
);
// 4, not 1: losing a race is an answer a script can branch on.
assert_eq!(
theirs.status.code(),
Some(4),
"stdout {} stderr {}",
out(&theirs),
err(&theirs)
);
assert!(
err(&theirs).contains("is claimed by"),
"stderr should name the holder: {}",
err(&theirs)
);
}
#[test]
fn unclaim_frees_the_issue() {
let (harness, id) = setup("cli-lease-unclaim");
let second = harness.second_authorized_key();
issue_cmd(&harness, &["claim", &id, "--ttl", "300", "--remote", "srv"]);
// The CLI prints an abbreviation, so the other side of the pair must
// accept one rather than silently leaving the full-id lease held.
let unclaim = issue_cmd(&harness, &["unclaim", &id[..8], "--remote", "srv"]);
assert!(
unclaim.status.success(),
"{}{}",
out(&unclaim),
err(&unclaim)
);
let theirs = issue_cmd_as(
&harness,
&second,
&["claim", &id, "--ttl", "300", "--remote", "srv"],
);
assert!(
theirs.status.success(),
"a released issue must be claimable: {}{}",
out(&theirs),
err(&theirs)
);
}
#[test]
fn renew_updates_the_expiry() {
let (harness, id) = setup("cli-lease-renew");
issue_cmd(&harness, &["claim", &id, "--ttl", "60", "--remote", "srv"]);
let renew = issue_cmd(
&harness,
&["renew", &id[..8], "--ttl", "600", "--remote", "srv"],
);
assert!(renew.status.success(), "{}{}", out(&renew), err(&renew));
assert!(out(&renew).contains("Renewed"), "{}", out(&renew));
}
#[test]
fn renew_without_a_claim_exits_4() {
let (harness, id) = setup("cli-lease-renew-none");
let renew = issue_cmd(&harness, &["renew", &id, "--ttl", "600", "--remote", "srv"]);
assert_eq!(
renew.status.code(),
Some(4),
"{}{}",
out(&renew),
err(&renew)
);
assert!(
err(&renew).contains("claim it first"),
"stderr should say how to fix it: {}",
err(&renew)
);
}
#[test]
fn claim_json_is_the_servers_own_reply() {
let (harness, id) = setup("cli-lease-json");
let claim = issue_cmd(&harness, &["claim", &id, "--remote", "srv", "--json"]);
assert!(claim.status.success(), "{}{}", out(&claim), err(&claim));
let json: serde_json::Value = serde_json::from_str(out(&claim).trim()).unwrap();
assert_eq!(json["status"], "acquired");
assert_eq!(json["issue"], id);
assert_eq!(json["token"], 1);
}
#[test]
fn claim_rejects_a_non_hex_id_locally() {
let (harness, _id) = setup("cli-lease-badid");
// Client-side: a quote must never reach the single-quoted remote command.
let claim = issue_cmd(&harness, &["claim", "nope'; true", "--remote", "srv"]);
assert!(!claim.status.success());
assert!(
err(&claim).contains("not an issue id"),
"stderr: {}",
err(&claim)
);
}
#[test]
fn claim_against_a_non_ssh_remote_fails_clearly() {
let (harness, id) = setup("cli-lease-localremote");
// `origin` is the local bare path the harness set up, not an SSH remote.
let claim = issue_cmd(&harness, &["claim", &id, "--remote", "origin"]);
assert!(!claim.status.success());
assert!(
err(&claim).contains("not an SSH remote"),
"stderr: {}",
err(&claim)
);
}