mod common;
use std::collections::BTreeSet;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex as StdMutex};
use std::task::{Context, Poll};
use std::time::Duration;
use async_trait::async_trait;
use futures::io::AsyncWrite as FuturesAsyncWrite;
use futures::stream;
use gix_hash::ObjectId;
use tokio::sync::{oneshot, Semaphore};
use alkgit::{
serve_stateless_advertisement, serve_stateless_post, AccessAction, AuthorizedRepo, BodyReader,
GitPackGen, GitPackIngest, GitRefs, Limits, PreparedPush, PushOptions, RefLine, RefOutcome,
RefUpdate, RepoRecord, Service, StorageError, SubstrateError, Visibility,
};
use common::{
blob_fixture, chain_pack, commit_fixture, create_command, decode_pack, delete_command,
parse_pkt_lines, push_commands, sideband_report_lines, tree_fixture, wired_text as text,
MemObjects, MemPackGen, MemPackIngest, MemRefs,
};
const FLUSH: &[u8] = b"0000";
const DELIMITER: &[u8] = b"0001";
fn repo(id: &str) -> RepoRecord {
RepoRecord {
repo_id: id.to_string(),
visibility: Visibility::Private,
grants: std::collections::BTreeMap::new(),
}
}
fn oid(b: u8) -> ObjectId {
ObjectId::from([b; 20])
}
fn hex_oid(b: u8) -> String {
let mut hex = String::with_capacity(40);
for _ in 0..20 {
hex.push_str(&format!("{b:02x}"));
}
hex
}
fn marker(record: RepoRecord, identity: Option<&alkcall::core::Identity>) -> AuthorizedRepo {
AuthorizedRepo::authorize(record, identity, AccessAction::Read)
.expect("the door's check passed")
}
fn pkt(payload: &[u8]) -> Vec<u8> {
let total = payload.len() + 4;
let mut out = Vec::with_capacity(total);
out.extend_from_slice(format!("{total:04x}").as_bytes());
out.extend_from_slice(payload);
out
}
fn first_post_body(command: &[u8], pre: &[&str], args: &[&str]) -> Vec<u8> {
[
pkt(b"# service=git-upload-pack\n"),
FLUSH.to_vec(),
pkt(b"version 2\n"),
pkt(b"agent=git/2.43\n"),
pkt(b"object-format=sha1\n"),
FLUSH.to_vec(),
pkt(command),
pre.iter()
.flat_map(|l| pkt(l.as_bytes()))
.collect::<Vec<u8>>(),
DELIMITER.to_vec(),
args.iter()
.flat_map(|l| pkt(l.as_bytes()))
.collect::<Vec<u8>>(),
FLUSH.to_vec(),
]
.concat()
}
fn command_post_body(command: &[u8], pre: &[&str], args: &[&str]) -> Vec<u8> {
[
pkt(command),
pre.iter()
.flat_map(|l| pkt(l.as_bytes()))
.collect::<Vec<u8>>(),
DELIMITER.to_vec(),
args.iter()
.flat_map(|l| pkt(l.as_bytes()))
.collect::<Vec<u8>>(),
FLUSH.to_vec(),
]
.concat()
}
#[derive(Clone, Default)]
struct VecSink(Arc<StdMutex<Vec<u8>>>);
impl VecSink {
fn body(&self) -> Vec<u8> {
self.0.lock().expect("sink is not poisoned").clone()
}
}
impl FuturesAsyncWrite for VecSink {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.0
.lock()
.expect("sink is not poisoned")
.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.poll_flush(cx)
}
}
struct PendingReader;
impl futures::AsyncRead for PendingReader {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
Poll::Pending
}
}
type ChunkStream = stream::Iter<std::vec::IntoIter<std::io::Result<bytes::Bytes>>>;
type PostBody = BodyReader<ChunkStream, std::io::Error>;
fn body_parts(parts: Vec<Vec<u8>>) -> PostBody {
let items: Vec<std::io::Result<bytes::Bytes>> = parts
.into_iter()
.map(|c| Ok(bytes::Bytes::from(c)))
.collect();
BodyReader::new(stream::iter(items))
}
struct Fixture {
marker: AuthorizedRepo,
refs: Arc<MemRefs>,
gen: Arc<dyn GitPackGen>,
ingest: Arc<MemPackIngest>,
objects: MemObjects,
repo_id: &'static str,
}
fn fixture() -> Fixture {
let repo_id = "owner/doubled";
let mut record = repo(repo_id);
record
.grants
.insert("u-7731".to_string(), vec![AccessAction::Read]);
let holder = alkcall::core::Identity {
id: "u-7731".to_string(),
scopes: Vec::new(),
resources: std::collections::HashMap::new(),
};
let objects = MemObjects::default();
Fixture {
marker: marker(record, Some(&holder)),
refs: Arc::new(MemRefs::default()),
gen: Arc::new(MemPackGen {
objects: objects.clone(),
}),
ingest: Arc::new(MemPackIngest {
objects: objects.clone(),
}),
objects,
repo_id,
}
}
impl Fixture {
fn record(&self) -> RepoRecord {
repo(self.repo_id)
}
fn with_gen(&self, gen: Arc<dyn GitPackGen>) -> Fixture {
Fixture {
marker: self.marker.clone(),
refs: Arc::clone(&self.refs),
gen,
ingest: Arc::clone(&self.ingest),
objects: self.objects.clone(),
repo_id: self.repo_id,
}
}
async fn serve(
&self,
body: PostBody,
limits: Limits,
service: Service,
pipeline: Arc<Semaphore>,
) -> (Result<(), SubstrateError>, VecSink) {
let sink = VecSink::default();
let refs: Arc<dyn GitRefs> = self.refs.clone();
let result = serve_stateless_post(
&self.marker,
body,
sink.clone(),
limits,
service,
refs,
Arc::clone(&self.gen),
self.ingest.clone() as Arc<dyn GitPackIngest>,
false,
pipeline,
)
.await;
(result, sink)
}
async fn serve_push(
&self,
body: PostBody,
limits: Limits,
push_options_negotiated: bool,
refs: Arc<dyn GitRefs>,
ingest: Arc<dyn GitPackIngest>,
pipeline: Arc<Semaphore>,
) -> (Result<(), SubstrateError>, VecSink) {
let sink = VecSink::default();
let result = serve_stateless_post(
&self.marker,
body,
sink.clone(),
limits,
Service::ReceivePack,
refs,
Arc::clone(&self.gen),
ingest,
push_options_negotiated,
pipeline,
)
.await;
(result, sink)
}
}
fn default_pipeline() -> Arc<Semaphore> {
Arc::new(Semaphore::new(
Limits::default().max_concurrent_blocking_tasks,
))
}
async fn assert_protocol_error(result: Result<(), SubstrateError>, needle: &str) {
let error = result.expect_err("expected a typed protocol failure");
assert!(
error.is_terminal(),
"the typed failure is terminal: {error:?}"
);
match &error {
SubstrateError::Protocol(message) => {
assert!(message.contains(needle), "{message}");
}
other => panic!("expected a protocol-shaped failure, got {other:?}"),
}
}
#[tokio::test]
async fn the_first_posts_capability_dump_is_skipped_and_ls_refs_serves() {
let fx = fixture();
let doubled = fx.record();
fx.refs
.apply_updates(
&doubled,
vec![
RefUpdate {
name: "refs/heads/main".to_string(),
expected: None,
new: Some(oid(1)),
},
RefUpdate {
name: "refs/tags/v1".to_string(),
expected: None,
new: Some(oid(2)),
},
],
false,
)
.await
.unwrap();
fx.refs
.set_head_symref(&doubled, Some("refs/heads/main".to_string()));
let body = body_parts(vec![first_post_body(
b"command=ls-refs\n",
&["agent=git/2.43"],
&["symrefs", "unborn"],
)]);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "unborn HEAD symref-target:refs/heads/main");
assert_eq!(text(&lines[1]), format!("{} refs/heads/main", oid(1)));
assert_eq!(text(&lines[2]), format!("{} refs/tags/v1", oid(2)));
assert!(lines[3].is_empty(), "the response ends at flush");
assert_eq!(lines.len(), 4);
}
#[tokio::test]
async fn two_consecutive_posts_re_derive_identically() {
let fx = fixture();
let body = || {
body_parts(vec![first_post_body(
b"command=ls-refs\n",
&[],
&["peel", "ref-prefix refs/heads/"],
)])
};
let (first, first_sink) = fx
.serve(
body(),
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
first.unwrap();
let (second, second_sink) = fx
.serve(
body(),
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
second.unwrap();
assert_eq!(
first_sink.body(),
second_sink.body(),
"two POSTs, one deterministic response"
);
}
#[tokio::test]
async fn a_full_first_post_fetch_round_serves_the_done_round_pack() {
let fx = fixture();
let doubled = fx.record();
let (blob_a, blob_a_bytes) = blob_fixture(b"alpha");
let (blob_b, blob_b_bytes) = blob_fixture(b"beta");
let (tree_a, tree_a_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "a.txt", blob_a)]);
let (tree_b, tree_b_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "b.txt", blob_b)]);
let (commit_a, commit_a_bytes) = commit_fixture(tree_a, &[], "alpha");
let (commit_b, commit_b_bytes) = commit_fixture(tree_b, &[commit_a], "beta");
for (id, bytes) in [
(blob_a, blob_a_bytes),
(blob_b, blob_b_bytes),
(tree_a, tree_a_bytes),
(tree_b, tree_b_bytes),
(commit_a, commit_a_bytes),
(commit_b, commit_b_bytes),
] {
fx.objects.put(&doubled, id, bytes);
}
let fx = fx.with_gen(Arc::new(MemPackGen {
objects: fx.objects.clone(),
}));
let body = body_parts(vec![first_post_body(
b"command=fetch\n",
&["object-format=sha1"],
&[
&format!("want {commit_b}"),
&format!("have {commit_a}"),
"thin-pack",
"no-progress",
"done",
],
)]);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "packfile");
let band_chunks: Vec<&[u8]> = lines[1..lines.len() - 1].iter().map(|p| &p[1..]).collect();
let pack: Vec<u8> = band_chunks.concat();
let decoded = decode_pack(&pack);
let ids: BTreeSet<ObjectId> = decoded.objects.iter().map(|(id, _, _)| *id).collect();
assert_eq!(
ids,
BTreeSet::from([commit_b, tree_b, blob_b]),
"closure(commit_b) − closure(commit_a) over the stateless entry"
);
let _ = decoded.trailer;
assert!(
lines.last().unwrap().is_empty(),
"the response ends at flush"
);
}
#[tokio::test]
async fn a_multi_have_post_acks_each_recognized_have_and_the_done_boundary_is_the_recognized_subset(
) {
let fx = fixture();
let doubled = fx.record();
let (blob_a, blob_a_bytes) = blob_fixture(b"alpha");
let (blob_b, blob_b_bytes) = blob_fixture(b"beta");
let (blob_c, blob_c_bytes) = blob_fixture(b"gamma");
let (tree_a, tree_a_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "a.txt", blob_a)]);
let (tree_b, tree_b_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "b.txt", blob_b)]);
let (tree_c, tree_c_bytes) =
tree_fixture(&[(gix_object::tree::EntryKind::Blob, "c.txt", blob_c)]);
let (commit_a, commit_a_bytes) = commit_fixture(tree_a, &[], "alpha");
let (commit_b, commit_b_bytes) = commit_fixture(tree_b, &[commit_a], "beta");
let (commit_c, commit_c_bytes) = commit_fixture(tree_c, &[commit_b], "gamma");
for (id, bytes) in [
(blob_a, blob_a_bytes),
(blob_b, blob_b_bytes),
(blob_c, blob_c_bytes),
(tree_a, tree_a_bytes),
(tree_b, tree_b_bytes),
(tree_c, tree_c_bytes),
(commit_a, commit_a_bytes),
(commit_b, commit_b_bytes),
(commit_c, commit_c_bytes),
] {
fx.objects.put(&doubled, id, bytes);
}
let fx = fx.with_gen(Arc::new(MemPackGen {
objects: fx.objects.clone(),
}));
let missing = oid(9u8);
let body = body_parts(vec![command_post_body(
b"command=fetch\n",
&["object-format=sha1", "agent=git/2.43"],
&[
&format!("want {}", commit_c),
&format!("have {}", commit_a),
&format!("have {}", missing),
&format!("have {}", commit_b),
],
)]);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
let ack_round: Vec<String> = parse_pkt_lines(&sink.body())
.iter()
.map(|p| text(p))
.collect();
assert_eq!(
ack_round,
vec![
"acknowledgments".to_owned(),
format!("ACK {}", commit_a),
format!("ACK {}", commit_b),
String::new(),
],
"per-have ACKs in request order, unrecognized haves silent, flush-terminated"
);
let body = body_parts(vec![command_post_body(
b"command=fetch\n",
&["object-format=sha1", "agent=git/2.43"],
&[
&format!("want {}", commit_c),
&format!("have {}", commit_a),
&format!("have {}", missing),
&format!("have {}", blob_a),
&format!("have {}", commit_b),
"done",
],
)]);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "packfile");
let band_chunks: Vec<&[u8]> = lines[1..lines.len() - 1].iter().map(|p| &p[1..]).collect();
let pack: Vec<u8> = band_chunks.concat();
let decoded = decode_pack(&pack);
let ids: BTreeSet<ObjectId> = decoded.objects.iter().map(|(id, _, _)| *id).collect();
assert_eq!(
ids,
BTreeSet::from([commit_c, tree_c, blob_c]),
"the boundary set is the recognized subset: both commons bounded the \
walk, the missing and the non-commit have subtracted nothing"
);
let _ = decoded.trailer;
}
#[tokio::test]
async fn a_no_done_fetch_post_re_derives_the_acknowledgments_per_post() {
let fx = fixture();
let doubled = fx.record();
let (blob, blob_bytes) = blob_fixture(b"paid");
let (tree, tree_bytes) = tree_fixture(&[(gix_object::tree::EntryKind::Blob, "a.txt", blob)]);
let (commit_a, commit_a_bytes) = commit_fixture(tree, &[], "alpha");
let (commit_b, commit_b_bytes) = commit_fixture(tree, &[commit_a], "beta");
for (id, bytes) in [
(blob, blob_bytes),
(tree, tree_bytes),
(commit_a, commit_a_bytes),
(commit_b, commit_b_bytes),
] {
fx.objects.put(&doubled, id, bytes);
}
let fx = fx.with_gen(Arc::new(MemPackGen {
objects: fx.objects.clone(),
}));
let body = body_parts(vec![command_post_body(
b"command=fetch\n",
&[],
&[
&format!("want {}", hex_oid(0x0d)),
&format!("have {commit_a}"),
],
)]);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "acknowledgments");
assert_eq!(text(&lines[1]), format!("ACK {commit_a}"));
assert!(lines.last().unwrap().is_empty());
}
#[tokio::test]
async fn the_probe_resolves_to_the_empty_ok_shape_with_nothing_written() {
let fx = fixture();
let (result, sink) = fx
.serve(
body_parts(vec![FLUSH.to_vec()]),
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
assert!(sink.body().is_empty(), "nothing written on the probe");
let (result, sink) = fx
.serve(
body_parts(vec![Vec::new()]),
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
result.unwrap();
assert!(sink.body().is_empty(), "the body-less probe as well");
}
#[tokio::test]
async fn an_unknown_command_declines_as_the_typed_failure() {
let fx = fixture();
let (result, sink) = fx
.serve(
body_parts(vec![command_post_body(b"command=object-info\n", &[], &[])]),
Limits::default(),
Service::UploadPack,
default_pipeline(),
)
.await;
assert_protocol_error(result, "unknown command").await;
assert!(sink.body().is_empty(), "the decline writes nothing");
}
#[tokio::test]
async fn the_flush_only_receive_pack_post_is_the_probe_shape_before_any_dispatch() {
let fx = fixture();
let runs = Arc::new(AtomicUsize::new(0));
let ingest = counting_ingest(fx.ingest.clone(), Arc::clone(&runs));
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let (result, sink) = fx
.serve_push(
body_parts(vec![FLUSH.to_vec()]),
Limits::default(),
false,
Arc::clone(&refs),
ingest,
default_pipeline(),
)
.await;
result.unwrap();
assert!(
sink.body().is_empty(),
"the probe answer writes nothing (the empty-OK shape)"
);
assert_eq!(
runs.load(AtomicOrdering::SeqCst),
0,
"nothing was dispatched to any backend handle"
);
}
#[tokio::test]
async fn the_wall_clock_budget_fails_closed_per_request() {
let fx = fixture();
let limits = Limits {
session_wall_clock: Duration::from_millis(20),
..Limits::default()
};
let sink = VecSink::default();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let result = serve_stateless_post(
&fx.marker,
PendingReader,
sink.clone(),
limits,
Service::UploadPack,
refs,
Arc::clone(&fx.gen),
fx.ingest.clone() as Arc<dyn GitPackIngest>,
false,
default_pipeline(),
)
.await;
match result {
Err(SubstrateError::Protocol(message)) => {
assert!(error_contains(&message, "session_wall_clock"), "{message}");
}
other => panic!("a wall-clock breach is a typed failure: {other:?}"),
}
assert!(sink.body().is_empty(), "the breach wrote nothing");
}
fn error_contains(message: &str, needle: &str) -> bool {
message.contains(needle)
}
#[tokio::test]
async fn a_saturated_concurrency_permit_delays_the_entry_generate() {
let fx = fixture();
let doubled = fx.record();
let (blob, blob_bytes) = blob_fixture(b"paid");
let (tree, tree_bytes) = tree_fixture(&[(gix_object::tree::EntryKind::Blob, "a.txt", blob)]);
let (commit, commit_bytes) = commit_fixture(tree, &[], "paid");
for (id, bytes) in [
(blob, blob_bytes),
(tree, tree_bytes),
(commit, commit_bytes),
] {
fx.objects.put(&doubled, id, bytes);
}
let gen: Arc<dyn GitPackGen> = Arc::new(MemPackGen {
objects: fx.objects.clone(),
});
let pipeline = Arc::new(Semaphore::new(1));
let held = pipeline.clone().acquire_owned().await.unwrap();
let body = body_parts(vec![command_post_body(
b"command=fetch\n",
&[],
&[&format!("want {commit}"), "done"],
)]);
let sink = VecSink::default();
let (done_tx, mut done_rx) = oneshot::channel();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let marker = fx.marker.clone();
let ingest = fx.ingest.clone() as Arc<dyn GitPackIngest>;
tokio::spawn(async move {
let result = serve_stateless_post(
&marker,
body,
sink.clone(),
Limits::default(),
Service::UploadPack,
refs,
gen,
ingest,
false,
pipeline,
)
.await;
done_tx.send((result, sink)).ok();
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
done_rx.try_recv().is_err(),
"the saturated permit delays the done round's generate"
);
drop(held);
let (result, sink) = done_rx
.await
.expect("the entry completes once a permit releases");
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "packfile");
}
#[tokio::test]
async fn the_entry_runs_over_arbitrary_reader_writer_ends() {
use tokio::io::AsyncWriteExt as TokioAsyncWriteExt;
use tokio_util::compat::TokioAsyncReadCompatExt;
let fx = fixture();
let (mut peer, stream) = tokio::io::duplex(1 << 15);
TokioAsyncWriteExt::write_all(
&mut peer,
&command_post_body(b"command=ls-refs\n", &[], &["ref-prefix refs/heads/"]),
)
.await
.unwrap();
drop(peer);
let sink = VecSink::default();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let result = serve_stateless_post(
&fx.marker,
stream.compat(),
sink.clone(),
Limits::default(),
Service::UploadPack,
refs,
Arc::clone(&fx.gen),
fx.ingest.clone() as Arc<dyn GitPackIngest>,
false,
default_pipeline(),
)
.await;
result.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert!(
lines.last().unwrap().is_empty(),
"the response ends at flush"
);
}
#[tokio::test]
async fn the_info_refs_advertisement_emits_the_smart_prefix_and_the_capability_dump() {
let fx = fixture();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::UploadPack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "# service=git-upload-pack");
assert!(lines[1].is_empty(), "the prefix section ends at flush");
assert_eq!(text(&lines[2]), "version 2");
#[cfg(not(feature = "sha256"))]
{
assert_eq!(text(&lines[3]), "ls-refs=unborn");
assert_eq!(text(&lines[4]), "fetch=wait-for-done");
assert_eq!(text(&lines[5]), "object-format=sha1");
assert!(lines[6].is_empty(), "the dump ends at flush");
assert_eq!(lines.len(), 7);
}
#[cfg(feature = "sha256")]
{
assert_eq!(text(&lines[3]), "ls-refs=unborn");
assert_eq!(text(&lines[4]), "fetch=wait-for-done");
assert_eq!(text(&lines[5]), "object-format=sha1");
assert_eq!(text(&lines[6]), "object-format=sha256");
assert!(lines[7].is_empty(), "the dump ends at flush");
assert_eq!(lines.len(), 8);
}
assert!(
!sink.body().windows(4).any(|w| w == b"0002"),
"no 0002 anywhere in the response body"
);
}
#[tokio::test]
async fn the_receive_pack_advertisement_emits_the_prefix_and_the_v0_ref_list() {
let fx = fixture();
let record = fx.record();
let outcomes = fx
.refs
.apply_updates(
&record,
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(oid(1)),
}],
false,
)
.await
.unwrap();
assert_eq!(outcomes.len(), 1, "the fixture's ref was created");
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "# service=git-receive-pack");
assert!(lines[1].is_empty(), "the prefix section ends at flush");
#[allow(unused_variables)]
let rider = if cfg!(feature = "sha256") {
" object-format=sha256"
} else {
""
};
assert_eq!(
text(&lines[2]),
format!("{} refs/heads/main\0report-status report-status-v2 delete-refs side-band-64k atomic ofs-delta object-format=sha1{rider} agent=alkgit/{}", hex_oid(1), env!("CARGO_PKG_VERSION")),
"caps NUL-attached on the FIRST ref line (the served set, agent=alkgit/<version>)"
);
assert!(lines[3].is_empty(), "the advertisement ends at flush");
assert_eq!(lines.len(), 4);
assert!(
!sink.body().windows(4).any(|w| w == b"0002"),
"no 0002 anywhere in the response body"
);
}
#[tokio::test]
async fn the_receive_pack_advertisement_for_an_empty_repo_emits_the_sentinel() {
let fx = fixture();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
let lines = parse_pkt_lines(&sink.body());
assert_eq!(text(&lines[0]), "# service=git-receive-pack");
assert!(lines[1].is_empty(), "the prefix section ends at flush");
#[allow(unused_variables)]
let rider = if cfg!(feature = "sha256") {
" object-format=sha256"
} else {
""
};
assert_eq!(
text(&lines[2]),
format!(
"{} capabilities^{{}}\0report-status report-status-v2 delete-refs side-band-64k atomic ofs-delta object-format=sha1{rider} agent=alkgit/{}",
"0".repeat(40),
env!("CARGO_PKG_VERSION"),
),
"the sentinel line carries the caps"
);
assert!(lines[3].is_empty(), "the advertisement ends at flush");
assert_eq!(lines.len(), 4);
}
#[tokio::test]
async fn the_push_options_token_rides_the_gate_only() {
let fx = fixture();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let gated = {
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
Arc::clone(&refs),
&Limits::default(),
true,
)
.await
.unwrap();
String::from_utf8_lossy(&sink.body()).into_owned()
};
assert!(
gated.contains("\0report-status"),
"caps ride the first ref line"
);
assert!(
gated.contains(" push-options "),
"the gate adds the push-options token: {gated}"
);
let ungated = {
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
String::from_utf8_lossy(&sink.body()).into_owned()
};
assert!(
!ungated.contains("push-options"),
"push-options is absent by default (v1's gate is off): {ungated}"
);
}
#[tokio::test]
async fn the_unborn_head_row_is_filtered_from_the_push_advertisement() {
let fx = fixture();
let record = fx.record();
fx.refs
.set_head_symref(&record, Some("refs/heads/main".to_owned()));
let outcomes = fx
.refs
.apply_updates(
&record,
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(oid(1)),
}],
false,
)
.await
.unwrap();
assert_eq!(outcomes.len(), 1, "the fixture's ref was created");
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let sink = VecSink::default();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
let lines = parse_pkt_lines(&sink.body());
let body = String::from_utf8_lossy(&sink.body()).into_owned();
assert!(
!body.contains("symref-target"),
"a V0 advertisement line has no symref shape — the unborn row is filtered: {body}"
);
assert!(
!lines
.iter()
.any(|line| line.len() >= 4 && String::from_utf8_lossy(&line[4..]).starts_with("HEAD")),
"the no-oid HEAD row cannot be advertised: {body}"
);
}
#[test]
fn only_the_checked_marker_path_feeds_the_entries() {
let mut private = repo("owner/doubled");
private
.grants
.insert("u-7731".to_string(), vec![AccessAction::Read]);
let holder = alkcall::core::Identity {
id: "u-7731".to_string(),
scopes: Vec::new(),
resources: std::collections::HashMap::new(),
};
assert_eq!(
AuthorizedRepo::authorize(private.clone(), None, AccessAction::Read),
None,
"anonymous on a private repo is no marker"
);
assert!(
AuthorizedRepo::authorize(private, Some(&holder), AccessAction::Read).is_some(),
"the granted identity is the entry's input"
);
}
#[tokio::test]
async fn a_backend_listing_failure_serves_nothing() {
struct ListingFailure;
#[async_trait::async_trait]
impl GitRefs for ListingFailure {
async fn list_refs(&self, _repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError> {
Err(StorageError::Invalid("listing unavailable".to_string()))
}
async fn apply_updates(
&self,
_repo: &RepoRecord,
_updates: Vec<RefUpdate>,
_atomic: bool,
) -> Result<Vec<RefOutcome>, StorageError> {
Err(StorageError::Invalid("listing unavailable".to_string()))
}
}
let fx = fixture();
let refs: Arc<dyn GitRefs> = Arc::new(ListingFailure);
let sink = VecSink::default();
let result = serve_stateless_post(
&fx.marker,
body_parts(vec![command_post_body(b"command=ls-refs\n", &[], &[])]),
sink.clone(),
Limits::default(),
Service::UploadPack,
refs,
Arc::clone(&fx.gen),
fx.ingest.clone() as Arc<dyn GitPackIngest>,
false,
default_pipeline(),
)
.await;
let error = result.expect_err("a listing failure must not serve");
assert!(
error.is_terminal(),
"backend failures band-map as terminal: {error:?}"
);
assert!(sink.body().is_empty(), "a failed listing wrote nothing");
}
#[tokio::test]
async fn the_advertisement_listing_hangs_until_the_wall_clock_not_beyond() {
struct HangingListing;
#[async_trait::async_trait]
impl GitRefs for HangingListing {
async fn list_refs(&self, _repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError> {
futures::future::pending().await
}
async fn apply_updates(
&self,
_repo: &RepoRecord,
_updates: Vec<RefUpdate>,
_atomic: bool,
) -> Result<Vec<RefOutcome>, StorageError> {
futures::future::pending().await
}
}
let fx = fixture();
let refs: Arc<dyn GitRefs> = Arc::new(HangingListing);
let sink = VecSink::default();
let limits = Limits {
session_wall_clock: std::time::Duration::from_millis(100),
..Limits::default()
};
let started = std::time::Instant::now();
let result = serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&limits,
false,
)
.await;
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"the listing did not hang past the wall clock"
);
let error = result.expect_err("a hung listing breaches the wall clock");
match &error {
SubstrateError::Protocol(message) => {
assert!(message.contains("session_wall_clock"), "{message}");
}
other => panic!("a wall-clock breach is protocol-shaped: {other:?}"),
}
assert!(sink.body().is_empty(), "a breached emission wrote nothing");
}
struct CountedIngest {
inner: Arc<MemPackIngest>,
runs: Arc<AtomicUsize>,
}
fn counting_ingest(inner: Arc<MemPackIngest>, runs: Arc<AtomicUsize>) -> Arc<dyn GitPackIngest> {
Arc::new(CountedIngest { inner, runs })
}
#[async_trait]
impl GitPackIngest for CountedIngest {
async fn prepare(
&self,
record: &RepoRecord,
updates: Vec<RefUpdate>,
pack: Box<dyn std::io::Read + Send>,
push_options: Option<PushOptions>,
limits: Limits,
) -> Result<PreparedPush, StorageError> {
self.runs.fetch_add(1, AtomicOrdering::SeqCst);
self.inner
.prepare(record, updates, pack, push_options, limits)
.await
}
}
struct OptionsRecorder {
inner: Arc<MemPackIngest>,
seen: Arc<StdMutex<Option<PushOptions>>>,
}
#[async_trait]
impl GitPackIngest for OptionsRecorder {
async fn prepare(
&self,
record: &RepoRecord,
updates: Vec<RefUpdate>,
pack: Box<dyn std::io::Read + Send>,
push_options: Option<PushOptions>,
limits: Limits,
) -> Result<PreparedPush, StorageError> {
*self.seen.lock().expect("seen is not poisoned") = push_options.clone();
self.inner
.prepare(record, updates, pack, push_options, limits)
.await
}
}
#[tokio::test]
async fn an_ordinary_push_post_applies_and_reports_sideband_framed() {
let fx = fixture();
let record = fx.record();
let chain = chain_pack();
let commands = [
create_command("refs/heads/main", chain.commit_b),
create_command("refs/heads/dev", chain.commit_a),
];
let body = body_parts(
push_commands("report-status side-band-64k", &commands)
.into_iter()
.chain([
chain.pack[..1].to_vec(),
chain.pack[1..24].to_vec(),
chain.pack[24..].to_vec(),
])
.collect::<Vec<Vec<u8>>>(),
);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::ReceivePack,
default_pipeline(),
)
.await;
result.unwrap();
let packets = parse_pkt_lines(&sink.body());
assert_eq!(
sideband_report_lines(&sink.body()),
vec![
"unpack ok".to_owned(),
"ok refs/heads/main".to_owned(),
"ok refs/heads/dev".to_owned(),
]
);
assert!(
!sink.body().windows(4).any(|w| w == b"0002"),
"no 0002 anywhere in the response body"
);
assert!(packets.last().unwrap().is_empty(), "one flush ends it");
assert_eq!(
fx.refs
.list_refs(&record)
.await
.unwrap()
.iter()
.map(|line| (line.name.clone(), line.oid))
.collect::<BTreeSet<_>>(),
BTreeSet::from([
("refs/heads/main".to_owned(), Some(chain.commit_b)),
("refs/heads/dev".to_owned(), Some(chain.commit_a)),
]),
"the CAS applied the pushed updates"
);
assert!(
fx.ingest.objects().contains(&record, &chain.commit_b),
"the pushed objects landed via the ingest handle"
);
}
#[tokio::test]
async fn a_packless_delete_post_takes_the_eof_driven_packless_path() {
let fx = fixture();
let record = fx.record();
let current = oid(0x1b);
fx.refs
.apply_updates(
&record,
vec![RefUpdate {
name: "refs/heads/gone".to_owned(),
expected: None,
new: Some(current),
}],
false,
)
.await
.unwrap();
let runs = Arc::new(AtomicUsize::new(0));
let ingest = counting_ingest(Arc::clone(&fx.ingest), Arc::clone(&runs));
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let body = body_parts(vec![push_commands(
"report-status side-band-64k",
&[delete_command("refs/heads/gone", current)],
)
.concat()]);
let (result, sink) = fx
.serve_push(
body,
Limits::default(),
false,
refs,
ingest,
default_pipeline(),
)
.await;
result.unwrap();
assert_eq!(
sideband_report_lines(&sink.body()),
vec!["unpack ok".to_owned(), "ok refs/heads/gone".to_owned()],
"the same report shape the duplex capture pinned"
);
assert_eq!(
runs.load(AtomicOrdering::SeqCst),
0,
"the packless path runs no prepare call"
);
assert!(
!fx.refs
.list_refs(&record)
.await
.unwrap()
.iter()
.any(|line| line.name == "refs/heads/gone"),
"the CAS deleted the ref"
);
}
#[tokio::test]
async fn the_negotiated_push_options_section_is_consumed_before_the_pack_tail() {
let fx = fixture();
let record = fx.record();
let chain = chain_pack();
let seen = Arc::new(StdMutex::new(None));
let ingest = Arc::new(OptionsRecorder {
inner: Arc::clone(&fx.ingest),
seen: Arc::clone(&seen),
}) as Arc<dyn GitPackIngest>;
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let body = body_parts(
push_commands(
"report-status push-options side-band-64k",
&[create_command("refs/heads/main", chain.commit_b)],
)
.into_iter()
.chain(vec![
pkt(b"deploy-key=abc"),
FLUSH.to_vec(),
chain.pack.clone(),
])
.collect::<Vec<Vec<u8>>>(),
);
let (result, sink) = fx
.serve_push(
body,
Limits::default(),
true,
refs,
ingest,
default_pipeline(),
)
.await;
result.unwrap();
assert_eq!(
*seen.lock().expect("seen is not poisoned"),
Some(PushOptions {
pairs: vec![("deploy-key".to_owned(), "abc".to_owned())]
}),
"the negotiated options section routed into prepare"
);
assert_eq!(
sideband_report_lines(&sink.body()),
vec!["unpack ok".to_owned(), "ok refs/heads/main".to_owned()],
);
assert!(
fx.ingest.objects().contains(&record, &chain.commit_b),
"the pack after the options flush ingested byte-exact"
);
}
#[tokio::test]
async fn a_push_post_body_budget_breach_fails_closed() {
let fx = fixture();
let runs = Arc::new(AtomicUsize::new(0));
let ingest = counting_ingest(Arc::clone(&fx.ingest), Arc::clone(&runs));
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let limits = Limits {
max_request_body: 5,
..Limits::default()
};
let (result, sink) = fx
.serve_push(
body_parts(vec![push_commands(
"report-status",
&[create_command("refs/heads/main", oid(0x2c))],
)
.concat()]),
limits,
false,
refs,
ingest,
default_pipeline(),
)
.await;
assert_protocol_error(result, "max_request_body").await;
assert!(sink.body().is_empty(), "the breach wrote nothing");
assert_eq!(
runs.load(AtomicOrdering::SeqCst),
0,
"the breach failed before any backend call"
);
}
#[tokio::test]
async fn a_push_post_pack_size_breach_fails_closed() {
let fx = fixture();
let record = fx.record();
let limits = Limits {
max_pack_size: 1000,
..Limits::default()
};
let oversized: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8).collect();
let body = body_parts(
push_commands(
"report-status side-band-64k",
&[create_command("refs/heads/main", oid(0x2c))],
)
.into_iter()
.chain(oversized.chunks(300).map(|c| c.to_vec()))
.collect::<Vec<Vec<u8>>>(),
);
let (result, sink) = fx
.serve(body, limits, Service::ReceivePack, default_pipeline())
.await;
assert_protocol_error(result, "max_pack_size").await;
assert!(sink.body().is_empty(), "no report over the broken stream");
assert!(
fx.refs.list_refs(&record).await.unwrap().is_empty(),
"the breach applied no ref"
);
}
#[tokio::test]
async fn the_advertisement_get_and_push_post_pair_serve_the_v0_flow() {
let fx = fixture();
let record = fx.record();
let chain = chain_pack();
async fn advert(fx: &Fixture) -> Vec<String> {
let sink = VecSink::default();
let refs: Arc<dyn GitRefs> = fx.refs.clone();
serve_stateless_advertisement(
&fx.marker,
sink.clone(),
Service::ReceivePack,
refs,
&Limits::default(),
false,
)
.await
.unwrap();
parse_pkt_lines(&sink.body())
.iter()
.map(|p| text(p))
.collect()
}
let before = advert(&fx).await;
assert_eq!(
*before.last().expect("the advertisement is non-empty"),
String::new(),
"the advertisement ends at flush"
);
assert!(
before[2].contains("capabilities^{"),
"the empty repo's sentinel line: {:?}",
before[2]
);
let body = body_parts(
push_commands(
"report-status side-band-64k",
&[create_command("refs/heads/main", chain.commit_b)],
)
.into_iter()
.chain([chain.pack.clone()])
.collect::<Vec<Vec<u8>>>(),
);
let (result, sink) = fx
.serve(
body,
Limits::default(),
Service::ReceivePack,
default_pipeline(),
)
.await;
result.unwrap();
assert_eq!(
sideband_report_lines(&sink.body()),
vec!["unpack ok".to_owned(), "ok refs/heads/main".to_owned()],
);
assert_eq!(
fx.refs
.list_refs(&record)
.await
.unwrap()
.iter()
.find(|line| line.name == "refs/heads/main")
.and_then(|line| line.oid),
Some(chain.commit_b),
"the CAS applied the pushed update"
);
let mut after = advert(&fx).await;
assert_eq!(
after.pop(),
Some(String::new()),
"the advertisement ends at flush"
);
assert_eq!(
after.len(),
3,
"the prefix flush + one ref line follow the prefix section"
);
let pushed_line = format!("{} refs/heads/main", chain.commit_b);
assert!(
after[2]
.split('\0')
.next()
.expect("the ref line has its caps section")
.starts_with(&pushed_line),
"the pushed ref appears in the next advertisement: {:?}",
after[2]
);
}