mod common;
use gix_hash::Kind;
use gunnar_store::git_storage_trait::GitOps;
use gunnar_wire::receive_pack::{AllowAll, Config};
use gunnar_wire::{PackCeiling, ReceivePack, RefStatus};
use common::{incompressible_pack, push_bytes, validating_ingest, Fixture, Tracked};
const CEILING: u64 = 64 * 1024;
const REFUSAL: &str = "plan 10gb would pass its pack quota with this push (cap 10737418240 \
bytes, 10737352704 already held, this push sent more than 65536 bytes): \
the transfer was stopped and nothing was stored; delete or buy more at \
https://gunnar.rs/billing/team";
struct Pushed {
outcome: gunnar_wire::PushOutcome,
taken: u64,
offered: u64,
report: String,
}
fn push(fixture: &Fixture, pack: &[u8], ceiling: Option<PackCeiling>) -> Pushed {
let zero = Kind::Sha1.null().to_string();
let input = push_bytes(
&[(&zero, &fixture.merge.to_string(), "refs/heads/fresh")],
"report-status object-format=sha1",
pack,
);
let offered = input.len() as u64;
let mut reader = Tracked::new(input);
let rp = ReceivePack::new(&fixture.git, fixture.kind).with_config(Config {
pack_ceiling: ceiling,
..Config::default()
});
let mut out = Vec::new();
let outcome = rp
.serve(
&mut reader,
&mut out,
&validating_ingest(Kind::Sha1),
&AllowAll,
)
.expect("serve")
.expect("a push happened");
Pushed {
outcome,
taken: reader.taken,
offered,
report: String::from_utf8_lossy(&out).into_owned(),
}
}
fn ceiling(bytes: u64) -> Option<PackCeiling> {
Some(PackCeiling {
bytes,
refusal: REFUSAL.to_owned(),
ledger: None,
})
}
#[test]
fn a_push_over_its_ceiling_is_cut_mid_transfer() {
let f = Fixture::build(Kind::Sha1);
let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
assert!(
pack.len() as u64 > CEILING * 8,
"the fixture pack must dwarf the ceiling or this test proves nothing: {} bytes",
pack.len()
);
let pushed = push(&f, &pack, ceiling(CEILING));
let commands = pushed.offered - pack.len() as u64;
assert_eq!(
pushed.taken,
commands + CEILING + 1,
"the server took {} of {} offered bytes; it must stop one byte past the {CEILING}-byte \
ceiling (after {commands} bytes of command list), not read the push to its end",
pushed.taken,
pushed.offered
);
assert!(
pushed.taken < pushed.offered / 4,
"the client was still asked for {} of {} bytes — that is a refusal AFTER the transfer, \
which is a different bug",
pushed.taken,
pushed.offered
);
assert_eq!(
pushed.outcome.unpack.as_ref().unwrap_err(),
REFUSAL,
"the client got the indexer's opinion of a truncated stream instead of the ceiling's \
own line"
);
assert!(
pushed.report.contains(REFUSAL),
"the refusal never reached the wire; the client read: {:?}",
pushed.report
);
assert!(
pushed.report.contains("https://gunnar.rs/billing/team"),
"a refusal that does not say where to pay: {:?}",
pushed.report
);
assert!(
pushed.outcome.pack.is_none(),
"a cut push reported an indexed pack: {:?}",
pushed.outcome.pack
);
assert!(
pushed.outcome.new_objects.is_err(),
"a cut push must not claim to know which objects it introduced"
);
assert!(
matches!(pushed.outcome.statuses[0], RefStatus::Rejected(_)),
"the reference was accepted on a push that never arrived: {:?}",
pushed.outcome.statuses
);
assert!(
!pushed.outcome.all_ok(),
"a cut push reported success: {:?}",
pushed.outcome
);
let refs = f.git.refs().expect("list refs");
assert!(
!refs.iter().any(|row| row.name == "refs/heads/fresh"),
"the cut push moved a reference: {refs:?}"
);
let mut names: Vec<&str> = refs.iter().map(|row| row.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
["refs/heads/main", "refs/heads/side", "refs/tags/v1"],
"the repository is not what it was before the cut push"
);
assert_eq!(
pushed.outcome.pack_bytes,
Some(CEILING + 1),
"`pack_bytes` must be what came off the wire, which is what the ceiling stopped at"
);
}
#[test]
fn a_push_under_its_ceiling_lands_and_the_whole_pack_is_read() {
let f = Fixture::build(Kind::Sha1);
let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
let generous = pack.len() as u64 * 2;
let pushed = push(&f, &pack, ceiling(generous));
assert_eq!(
pushed.outcome.unpack,
Ok(()),
"a push well under its ceiling was refused"
);
assert_eq!(pushed.outcome.statuses[0], RefStatus::Ok);
assert_eq!(
pushed.taken, pushed.offered,
"the whole push must be read when nothing stops it"
);
assert_eq!(pushed.outcome.pack_bytes, Some(pack.len() as u64));
assert!(f
.git
.refs()
.expect("list refs")
.iter()
.any(|row| row.name == "refs/heads/fresh"));
}
#[test]
fn a_push_of_exactly_the_ceiling_lands() {
let f = Fixture::build(Kind::Sha1);
let pack = incompressible_pack(Kind::Sha1, 4, 64 * 1024);
let exact = pack.len() as u64;
let pushed = push(&f, &pack, ceiling(exact));
assert_eq!(
pushed.outcome.unpack,
Ok(()),
"a push of exactly {exact} bytes under a {exact}-byte ceiling was refused"
);
assert_eq!(pushed.taken, pushed.offered);
}
#[test]
fn with_no_ceiling_the_same_push_lands() {
let f = Fixture::build(Kind::Sha1);
let pack = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
let pushed = push(&f, &pack, None);
assert_eq!(pushed.outcome.unpack, Ok(()));
assert_eq!(pushed.outcome.statuses[0], RefStatus::Ok);
assert_eq!(pushed.taken, pushed.offered);
}
#[test]
fn a_deletion_carries_no_pack_and_is_never_cut() {
let f = Fixture::build(Kind::Sha1);
let zero = Kind::Sha1.null().to_string();
let input = push_bytes(
&[(&f.three.to_string(), &zero, "refs/heads/side")],
"report-status object-format=sha1",
&[],
);
let mut reader = Tracked::new(input);
let rp = ReceivePack::new(&f.git, f.kind).with_config(Config {
pack_ceiling: ceiling(1),
..Config::default()
});
let mut out = Vec::new();
let outcome = rp
.serve(
&mut reader,
&mut out,
&validating_ingest(Kind::Sha1),
&AllowAll,
)
.expect("serve")
.expect("a push happened");
assert_eq!(
outcome.unpack,
Ok(()),
"a deletion was cut by a byte ceiling it never sent a byte against"
);
assert_eq!(outcome.statuses[0], RefStatus::Ok);
assert_eq!(outcome.pack_bytes, Some(0));
assert!(!f
.git
.refs()
.expect("list refs")
.iter()
.any(|row| row.name == "refs/heads/side"));
}