mod common;
use std::io::Cursor;
use gix_hash::Kind;
use gunnar_wire::{ProtocolVersion, Session, UploadPack};
use common::{Fixture, Response};
fn upload<'a>(fixture: &'a Fixture) -> UploadPack<'a> {
UploadPack::new(&fixture.git, fixture.kind)
}
fn v0_body_with_done(want: &str, capabilities: &str) -> Vec<u8> {
let mut body = Vec::new();
line(&mut body, &format!("want {want} {capabilities}\n"));
body.extend_from_slice(b"0000");
line(&mut body, "done\n");
body
}
fn v0_body_no_done(want: &str, capabilities: &str, haves: &[String]) -> Vec<u8> {
let mut body = Vec::new();
line(&mut body, &format!("want {want} {capabilities}\n"));
body.extend_from_slice(b"0000");
for have in haves {
line(&mut body, &format!("have {have}\n"));
}
body.extend_from_slice(b"0000");
body
}
fn line(out: &mut Vec<u8>, text: &str) {
out.extend_from_slice(format!("{:04x}", text.len() + 4).as_bytes());
out.extend_from_slice(text.as_bytes());
}
fn v2_body(command: &str, capabilities: &[&str], arguments: &[&str]) -> Vec<u8> {
common::v2_request(command, capabilities, arguments)
}
#[test]
fn a_stateless_v0_post_does_not_re_advertise() {
let fixture = Fixture::build(Kind::Sha1);
let upload = upload(&fixture);
let body = v0_body_with_done(&fixture.four.to_string(), "side-band-64k ofs-delta");
let mut stateless = Vec::new();
upload
.serve_session(
ProtocolVersion::V0,
Session::StatelessRpc,
&mut Cursor::new(body.clone()),
&mut stateless,
)
.expect("serve one stateless v0 round");
let first = Response::parse(&stateless).lines().first().cloned();
assert_eq!(
first.as_deref().map(str::trim),
Some("NAK"),
"a stateless-rpc POST must answer with the negotiation, not with a ref \
advertisement; got {:?}",
Response::parse(&stateless).lines()
);
let mut connected = Vec::new();
upload
.serve_session(
ProtocolVersion::V0,
Session::Connected,
&mut Cursor::new(body),
&mut connected,
)
.expect("serve the same body on a connection");
let first_connected = Response::parse(&connected).lines().first().cloned();
assert!(
first_connected
.as_deref()
.is_some_and(|line| line.contains("refs/heads/main")),
"the connected path is supposed to advertise first; if it does not, the \
assertion above proves nothing. Got {first_connected:?}"
);
}
#[test]
fn a_stateless_v2_post_does_not_re_advertise() {
let fixture = Fixture::build(Kind::Sha1);
let upload = upload(&fixture);
let body = v2_body(
"ls-refs",
&["object-format=sha1"],
&["peel", "symrefs", "ref-prefix refs/heads/"],
);
let mut stateless = Vec::new();
upload
.serve_session(
ProtocolVersion::V2,
Session::StatelessRpc,
&mut Cursor::new(body.clone()),
&mut stateless,
)
.expect("serve one stateless v2 command");
let lines = Response::parse(&stateless).lines();
assert!(
!lines.iter().any(|line| line.trim() == "version 2"),
"a stateless-rpc POST must not repeat the capability advertisement: {lines:?}"
);
assert!(
lines.iter().any(|line| line.contains("refs/heads/main")),
"…but it must still answer the command: {lines:?}"
);
let mut connected = Vec::new();
upload
.serve_session(
ProtocolVersion::V2,
Session::Connected,
&mut Cursor::new(body),
&mut connected,
)
.expect("serve the same body on a connection");
assert!(
Response::parse(&connected)
.lines()
.iter()
.any(|line| line.trim() == "version 2"),
"the connected path is supposed to advertise; without that the check \
above is vacuous"
);
}
#[test]
fn the_http_advertisement_is_byte_identical_to_the_connected_one() {
for kind in [Kind::Sha1, Kind::Sha256] {
let fixture = Fixture::build(kind);
let upload = upload(&fixture);
for version in [ProtocolVersion::V0, ProtocolVersion::V2] {
let mut standalone = Vec::new();
upload
.advertise(version, &mut standalone)
.expect("the standalone advertisement");
let mut inline = Vec::new();
upload
.serve_session(
version,
Session::Connected,
&mut Cursor::new(Vec::new()),
&mut inline,
)
.expect("the inline advertisement");
assert_eq!(
standalone, inline,
"{version:?}/{kind:?}: UploadPack::advertise and the inline \
advertisement disagree, so smart HTTP and SSH would serve \
different capabilities from the same repository"
);
assert!(
!standalone.is_empty(),
"{version:?}/{kind:?}: both advertisements are EMPTY, which \
would make the equality above true and meaningless"
);
}
}
}
#[test]
fn a_v0_round_without_done_answers_and_stops() {
let fixture = Fixture::build(Kind::Sha1);
let upload = upload(&fixture);
let body = v0_body_no_done(
&fixture.four.to_string(),
"side-band-64k ofs-delta",
&[fixture.two.to_string()],
);
let mut out = Vec::new();
let summary = upload
.serve_session(
ProtocolVersion::V0,
Session::StatelessRpc,
&mut Cursor::new(body),
&mut out,
)
.expect("a negotiation round with no done");
let response = Response::parse(&out);
let lines = response.lines();
assert_eq!(
lines.len(),
1,
"one round, one answer, nothing else: {lines:?}"
);
assert_eq!(
lines[0].trim(),
format!("ACK {}", fixture.two),
"`have {}` is in the store, so the round must ACK it",
fixture.two
);
assert!(
!out.windows(4).any(|w| w == b"PACK"),
"no packfile may be sent on a round the client has not ended with `done`"
);
assert_eq!(
summary.common, 1,
"the summary must report the common object it found, or an operator \
reading metrics cannot tell negotiation from a blind full send"
);
assert_eq!(summary.objects_sent, 0);
}
#[test]
fn a_stateless_v2_call_serves_one_command_and_returns() {
let fixture = Fixture::build(Kind::Sha1);
let upload = upload(&fixture);
let mut body = v2_body("ls-refs", &["object-format=sha1"], &["ref-prefix HEAD"]);
body.extend_from_slice(&v2_body(
"ls-refs",
&["object-format=sha1"],
&["ref-prefix refs/heads/"],
));
let mut out = Vec::new();
let summary = upload
.serve_session(
ProtocolVersion::V2,
Session::StatelessRpc,
&mut Cursor::new(body.clone()),
&mut out,
)
.expect("serve");
assert_eq!(
summary.ls_refs, 1,
"stateless-rpc must serve ONE command per call; served {}",
summary.ls_refs
);
let mut connected_out = Vec::new();
let connected = upload
.serve_session(
ProtocolVersion::V2,
Session::Connected,
&mut Cursor::new(body),
&mut connected_out,
)
.expect("serve");
assert_eq!(
connected.ls_refs, 2,
"the connected path is supposed to serve both commands off one stream; \
if it does not, the assertion above cannot fail"
);
}
#[test]
fn a_stateless_v0_post_does_not_count_an_advertisement() {
let fixture = Fixture::build(Kind::Sha1);
let upload = upload(&fixture);
let body = v0_body_with_done(&fixture.four.to_string(), "side-band-64k ofs-delta");
let stateless = upload
.serve_session(
ProtocolVersion::V0,
Session::StatelessRpc,
&mut Cursor::new(body.clone()),
&mut Vec::new(),
)
.expect("serve");
assert_eq!(stateless.ls_refs, 0);
let connected = upload
.serve_session(
ProtocolVersion::V0,
Session::Connected,
&mut Cursor::new(body),
&mut Vec::new(),
)
.expect("serve");
assert_eq!(
connected.ls_refs, 1,
"the connected path advertises exactly once; without that the zero \
above is not a measurement of anything"
);
}