mod common;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use std::time::Duration;
use gix_hash::ObjectId;
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use alkcall::core::Identity;
use alkgit::{
serve_duplex, serve_duplex_resolved, AccessAction, AuthorizedRepo, GitPackGen, GitPackIngest,
GitRefs, GitRegistry, GitRegistryStore, Limits, PreparedPush, PushOptions, RefUpdate,
RepoRecord, Service, StorageError, SubstrateError, Visibility,
};
use common::{
blob_fixture, chain_pack, commit_fixture, create_command, decode_pack, delete_command,
push_commands, sideband_report_lines, tag_fixture, tree_fixture, MemObjects, MemPackGen,
MemPackIngest, MemRefs, MemRegistry,
};
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: BTreeMap::new(),
}
}
fn identity(id: &str) -> Identity {
Identity {
id: id.to_string(),
scopes: Vec::new(),
resources: std::collections::HashMap::new(),
}
}
fn marker(record: RepoRecord, identity: &Identity, action: AccessAction) -> AuthorizedRepo {
AuthorizedRepo::authorize(record, Some(identity), action).expect("the adapter'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 command_request(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()
}
async fn read_pkts(reader: &mut tokio::io::DuplexStream) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
loop {
let mut header = [0u8; 4];
reader
.read_exact(&mut header)
.await
.expect("pkt-line header");
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
if len < 4 {
packets.push(Vec::new());
break;
}
let mut payload = vec![0u8; len - 4];
reader
.read_exact(&mut payload)
.await
.expect("pkt-line payload");
packets.push(payload);
}
packets
}
async fn read_band(reader: &mut tokio::io::DuplexStream) -> Vec<u8> {
let mut header = [0u8; 4];
reader.read_exact(&mut header).await.expect("band header");
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
let mut payload = vec![0u8; len - 4];
reader.read_exact(&mut payload).await.expect("band payload");
payload
}
fn text(payload: &[u8]) -> String {
String::from_utf8_lossy(payload)
.trim_end_matches('\n')
.to_owned()
}
fn text_lines(pkts: &[Vec<u8>]) -> Vec<String> {
pkts.iter().map(|p| text(p)).collect()
}
fn advertisement_lines() -> Vec<String> {
#[cfg(not(feature = "sha256"))]
let lines: [&str; 4] = [
"version 2",
"ls-refs=unborn",
"fetch=wait-for-done",
"object-format=sha1",
];
#[cfg(feature = "sha256")]
let lines: [&str; 5] = [
"version 2",
"ls-refs=unborn",
"fetch=wait-for-done",
"object-format=sha1",
"object-format=sha256",
];
lines.iter().map(|l| l.to_string()).collect()
}
fn advertisement_pkts() -> Vec<String> {
let mut lines = advertisement_lines();
lines.push(String::new());
lines
}
struct Fixture {
identity: Identity,
read_marker: AuthorizedRepo,
write_marker: AuthorizedRepo,
refs: Arc<MemRefs>,
gen: Arc<MemPackGen>,
ingest: Arc<MemPackIngest>,
objects: MemObjects,
}
fn fixture() -> Fixture {
let record = repo("owner/doubled");
let mut read_record = record.clone();
read_record
.grants
.insert("u-7731".to_string(), vec![AccessAction::Read]);
let mut write_record = record;
write_record
.grants
.insert("u-7731".to_string(), vec![AccessAction::Write]);
let holder = identity("u-7731");
let objects = MemObjects::default();
Fixture {
identity: holder.clone(),
read_marker: marker(read_record, &holder, AccessAction::Read),
write_marker: marker(write_record, &holder, AccessAction::Write),
refs: Arc::new(MemRefs::default()),
gen: Arc::new(MemPackGen {
objects: objects.clone(),
}),
ingest: Arc::new(MemPackIngest {
objects: objects.clone(),
}),
objects,
}
}
struct TwoCommits {
commit_a: ObjectId,
commit_b: ObjectId,
tree_b: ObjectId,
blob_b: ObjectId,
}
async fn seed_two_commits(fx: &Fixture) -> TwoCommits {
let doubled = repo("owner/doubled");
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");
let (tag, tag_bytes) = tag_fixture(commit_b, gix_object::Kind::Commit, "v1");
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),
(tag, tag_bytes),
] {
fx.objects.put(&doubled, id, bytes);
}
fx.refs
.apply_updates(
&doubled,
vec![
RefUpdate {
name: "refs/heads/old".to_string(),
expected: None,
new: Some(commit_a),
},
RefUpdate {
name: "refs/heads/main".to_string(),
expected: None,
new: Some(commit_b),
},
RefUpdate {
name: "refs/tags/v1".to_string(),
expected: None,
new: Some(tag),
},
],
false,
)
.await
.unwrap();
TwoCommits {
commit_a,
commit_b,
tree_b,
blob_b,
}
}
#[allow(clippy::too_many_arguments)]
async fn serve(
marker: &AuthorizedRepo,
refs: Arc<dyn GitRefs>,
gen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
limits: Limits,
service: Service,
) -> (
tokio::io::DuplexStream,
JoinHandle<Result<(), SubstrateError>>,
) {
let (peer, stream) = duplex(1 << 16);
let joined = tokio::spawn({
let marker = marker.clone();
async move {
serve_duplex(
&marker,
stream,
limits,
service,
refs,
gen,
ingest,
push_options_negotiated,
Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks)),
)
.await
}
});
(peer, joined)
}
async fn serve_fixture(
fx: &Fixture,
limits: Limits,
) -> (
tokio::io::DuplexStream,
JoinHandle<Result<(), SubstrateError>>,
) {
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
let ingest: Arc<dyn GitPackIngest> = fx.ingest.clone();
serve(
&fx.read_marker,
refs,
gen,
ingest,
false,
limits,
Service::UploadPack,
)
.await
}
async fn serve_push(
fx: &Fixture,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
limits: Limits,
) -> (
tokio::io::DuplexStream,
JoinHandle<Result<(), SubstrateError>>,
) {
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
serve(
&fx.write_marker,
refs,
gen,
ingest,
push_options_negotiated,
limits,
Service::ReceivePack,
)
.await
}
#[tokio::test]
async fn advertisement_once_with_three_commands_dispatching_over_the_reset_discipline() {
let fx = fixture();
let commits = seed_two_commits(&fx).await;
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
let mut conversation = text_lines(&advertisement);
assert_eq!(
conversation.as_slice(),
advertisement_pkts().as_slice(),
"the advertisement, emitted once at session start"
);
for round in 0..2 {
peer.write_all(&command_request(
b"command=ls-refs\n",
&["agent=git/2.43"],
&["ref-prefix refs/heads/"],
))
.await
.unwrap();
let response = read_pkts(&mut peer).await;
let lines = text_lines(&response);
conversation.extend(lines.iter().cloned());
assert_eq!(
lines.last(),
Some(&String::new()),
"round {round}'s response ends at flush"
);
assert_eq!(
lines[..lines.len() - 1]
.iter()
.map(|line| line
.split_once(' ')
.map_or(line.to_owned(), |(_, name)| name.to_owned()))
.collect::<Vec<_>>(),
vec!["refs/heads/main", "refs/heads/old"],
"round {round}'s ref-prefix-filtered ls-refs response"
);
}
peer.write_all(&command_request(
b"command=fetch\n",
&["object-format=sha1"],
&[
&format!("want {}", commits.commit_b),
&format!("have {}", commits.commit_a),
],
))
.await
.unwrap();
let response = read_pkts(&mut peer).await;
let lines = text_lines(&response);
conversation.extend(lines.clone());
assert_eq!(lines[0], "acknowledgments", "the no-done round's shape");
assert_eq!(lines[1], format!("ACK {}", commits.commit_a));
assert_eq!(lines.last(), Some(&String::new()));
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close ends the session quietly");
assert_eq!(
conversation
.iter()
.filter(|line| line == &"version 2")
.count(),
1,
"the advertisement-once rule across the whole conversation"
);
}
#[tokio::test]
async fn a_second_advertisement_never_occurs_between_commands() {
let fx = fixture();
seed_two_commits(&fx).await;
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
peer.write_all(&command_request(b"command=ls-refs\n", &[], &[]))
.await
.unwrap();
let first = text_lines(&read_pkts(&mut peer).await);
peer.write_all(&command_request(b"command=ls-refs\n", &[], &[]))
.await
.unwrap();
let second = text_lines(&read_pkts(&mut peer).await);
assert_eq!(first, second, "both rounds list the same refs");
let listing: Vec<String> = first
.iter()
.filter(|line| !line.is_empty())
.map(|line| {
line.split_once(' ')
.map_or(line.to_owned(), |(_, name)| name.to_owned())
})
.collect();
assert_eq!(
listing,
vec!["refs/heads/main", "refs/heads/old", "refs/tags/v1"],
"the unfiltered ls-refs listing"
);
assert!(
first
.iter()
.chain(second.iter())
.all(|line| line != "version 2"),
"no capability dump rode between the commands"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close ends the session quietly");
}
#[tokio::test]
async fn a_full_fetch_conversation_passes_end_to_end() {
let fx = fixture();
let commits = seed_two_commits(&fx).await;
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
peer.write_all(&command_request(
b"command=ls-refs\n",
&["symrefs", "unborn"],
&["ref-prefix refs/heads/"],
))
.await
.unwrap();
let ref_lines = text_lines(&read_pkts(&mut peer).await);
assert_eq!(
ref_lines[0],
format!("{} refs/heads/main", commits.commit_b)
);
assert_eq!(ref_lines[1], format!("{} refs/heads/old", commits.commit_a));
assert_eq!(ref_lines.last(), Some(&String::new()));
peer.write_all(&command_request(
b"command=fetch\n",
&["object-format=sha1", "agent=git/2.43"],
&[
"thin-pack",
"no-progress",
&format!("want {}", commits.commit_b),
&format!("have {}", commits.commit_a),
"done",
],
))
.await
.unwrap();
let fetch_response = read_pkts(&mut peer).await;
assert_eq!(text(&fetch_response[0]), "packfile");
let band_chunks: Vec<&[u8]> = fetch_response[1..fetch_response.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([commits.commit_b, commits.tree_b, commits.blob_b]),
"closure(want) − closure(recognized have) over the entry"
);
let _ = decoded.trailer;
assert!(
fetch_response.last().unwrap().is_empty(),
"flush-terminated"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close ends the session quietly");
}
#[tokio::test]
async fn a_multi_have_session_acks_each_recognized_have_and_the_done_boundary_is_the_recognized_subset(
) {
let fx = fixture();
let doubled = repo("owner/doubled");
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 missing = ObjectId::from([9u8; 20]);
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
peer.write_all(&command_request(
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),
],
))
.await
.unwrap();
let ack_round = text_lines(&read_pkts(&mut peer).await);
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"
);
peer.write_all(&command_request(
b"command=fetch\n",
&["object-format=sha1", "agent=git/2.43"],
&[
"thin-pack",
"no-progress",
&format!("want {}", commit_c),
&format!("have {}", commit_a),
&format!("have {}", missing),
&format!("have {}", blob_a),
&format!("have {}", commit_b),
"done",
],
))
.await
.unwrap();
let done_round = read_pkts(&mut peer).await;
assert_eq!(text(&done_round[0]), "packfile");
let band_chunks: Vec<&[u8]> = done_round[1..done_round.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;
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close ends the session quietly");
}
#[tokio::test]
async fn an_unknown_command_declines_via_the_error_band() {
let fx = fixture();
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
peer.write_all(&command_request(
b"command=object-info\n",
&[],
&["size 0000"],
))
.await
.unwrap();
let band = text(&read_band(&mut peer).await);
assert!(
band.starts_with("ERR unknown command \"object-info\""),
"the decline is the band: {band}"
);
let result = joined
.await
.expect("the session task joins")
.expect_err("the decline is a typed failure");
match &result {
SubstrateError::Protocol(message) => {
assert!(message.contains("unknown command"), "{message}");
assert!(message.contains("object-info"), "{message}");
}
other => panic!("an unknown command is protocol-shaped: {other:?}"),
}
let mut remaining = [0u8; 1];
let n = peer.read(&mut remaining).await.expect("read after close");
assert_eq!(n, 0, "nothing follows the decline band");
}
#[tokio::test]
async fn a_malformed_request_bands_and_ends_the_session() {
let fx = fixture();
let (mut peer, joined) = serve_fixture(&fx, Limits::default()).await;
read_pkts(&mut peer).await;
peer.write_all(b"zzzz").await.unwrap();
let band = text(&read_band(&mut peer).await);
assert!(band.starts_with("ERR "), "the failure bands");
assert!(
band.contains("malformed pkt-line"),
"the machinery's protocol failure bands: {band}"
);
let result = joined
.await
.expect("the session task joins")
.expect_err("a machinery protocol failure is a typed failure");
assert!(
matches!(result, SubstrateError::Protocol(_)),
"the typed failure returns to the caller: {result:?}"
);
}
#[tokio::test]
async fn the_wall_clock_budget_ends_a_session_that_exceeds_it() {
let fx = fixture();
let limits = Limits {
session_wall_clock: Duration::from_millis(100),
..Limits::default()
};
let (mut peer, joined) = serve_fixture(&fx, limits).await;
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
let band = text(&read_band(&mut peer).await);
assert!(
band.starts_with("ERR ") && band.contains("session_wall_clock"),
"the breach bands the session: {band}"
);
let mut remaining = [0u8; 1];
let n = peer.read(&mut remaining).await.expect("read after close");
assert_eq!(n, 0, "the breached session closed");
let result = joined
.await
.expect("the session task joins")
.expect_err("the breach is a typed failure");
match &result {
SubstrateError::Protocol(message) => {
assert!(message.contains("session_wall_clock"), "{message}");
}
other => panic!("a wall-clock breach is protocol-shaped: {other:?}"),
}
}
#[tokio::test]
async fn unknown_repo_and_unauthorized_denials_are_indistinguishable() {
let fx = fixture();
let holder = fx.identity.clone();
let unknown_registry: Arc<dyn GitRegistry> = Arc::new(MemRegistry::default());
let unauthorized_registry = MemRegistry::default();
unauthorized_registry
.put_repo(&repo("owner/doubled"))
.await
.unwrap();
assert_eq!(
AuthorizedRepo::authorize(
unauthorized_registry
.resolve("owner/doubled")
.await
.unwrap(),
Some(&holder),
AccessAction::Read
),
None,
"the grantless identity on the private repo denies"
);
let unauthorized_registry: Arc<dyn GitRegistry> = Arc::new(unauthorized_registry);
let (unknown, unknown_wire) = denial_wire(unknown_registry, Service::UploadPack).await;
let (unauthorized, unauthorized_wire) =
denial_wire(unauthorized_registry, Service::UploadPack).await;
match (&unknown, &unauthorized) {
(Err(SubstrateError::AccessDenied), Err(SubstrateError::AccessDenied)) => {}
other => panic!("both denials collapse to AccessDenied: {other:?}"),
}
assert!(!unknown_wire.is_empty());
assert_eq!(
unknown_wire, unauthorized_wire,
"the denials are byte-identical"
);
let denial = pkt_payloads(&unknown_wire);
assert_eq!(
denial,
vec![b"ERR access denied: owner/doubled".to_vec()],
"the collapsed band names only what the client sent, and no protocol byte precedes it"
);
assert!(
!String::from_utf8_lossy(&unknown_wire).contains("not found")
&& !String::from_utf8_lossy(&unknown_wire).contains("grant"),
"no cause leaks through the denial band"
);
}
fn pkt_payloads(wire: &[u8]) -> Vec<Vec<u8>> {
let mut payloads = Vec::new();
let mut i = 0;
while i < wire.len() {
assert!(i + 4 <= wire.len(), "trailing partial header");
let len = usize::from_str_radix(
std::str::from_utf8(&wire[i..i + 4]).expect("hex header"),
16,
)
.expect("hex length");
assert!(len >= 4 && i + len <= wire.len(), "partial pkt-line");
payloads.push(wire[i + 4..i + len].to_vec());
i += len;
}
payloads
}
async fn denial_wire(
registry: Arc<dyn GitRegistry>,
service: Service,
) -> (Result<(), SubstrateError>, Vec<u8>) {
let (mut peer, stream) = duplex(1 << 16);
let holder = identity("u-7731");
let refs: Arc<dyn GitRefs> = Arc::new(MemRefs::default());
let gen: Arc<dyn GitPackGen> = Arc::new(MemPackGen::default());
let ingest: Arc<dyn GitPackIngest> = Arc::new(MemPackIngest::default());
let joined = tokio::spawn(async move {
serve_duplex_resolved(
"owner/doubled",
Some(&holder),
service,
stream,
Limits::default(),
registry,
refs,
gen,
ingest,
false,
Arc::new(Semaphore::new(
Limits::default().max_concurrent_blocking_tasks,
)),
)
.await
});
let mut wire = Vec::new();
let mut chunk = [0u8; 256];
let drained = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let n = peer.read(&mut chunk).await.expect("the denial wire");
if n == 0 {
break;
}
wire.extend_from_slice(&chunk[..n]);
}
})
.await;
if drained.is_err() {
panic!("the denial never closed the stream");
}
(joined.await.expect("the front task joins"), wire)
}
async fn read_body_until_flush(reader: &mut tokio::io::DuplexStream) -> Vec<u8> {
let mut body = Vec::new();
loop {
let mut header = [0u8; 4];
reader.read_exact(&mut header).await.expect("header");
body.extend_from_slice(&header);
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
if len < 4 {
return body;
}
let mut payload = vec![0u8; len - 4];
reader.read_exact(&mut payload).await.expect("payload");
body.extend_from_slice(&payload);
}
}
struct CountedIngest {
inner: Arc<MemPackIngest>,
runs: Arc<std::sync::atomic::AtomicUsize>,
}
fn counting_ingest(
inner: Arc<MemPackIngest>,
runs: Arc<std::sync::atomic::AtomicUsize>,
) -> Arc<dyn GitPackIngest> {
Arc::new(CountedIngest { inner, runs })
}
#[async_trait::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> {
use std::sync::atomic::Ordering::SeqCst;
self.runs.fetch_add(1, SeqCst);
self.inner
.prepare(record, updates, pack, push_options, limits)
.await
}
}
struct OptionsRecorder {
inner: Arc<MemPackIngest>,
seen: Arc<std::sync::Mutex<Option<PushOptions>>>,
}
#[async_trait::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 a_full_push_conversation_applies_and_reports_over_the_duplex_entry() {
let fx = fixture();
let record = repo("owner/doubled");
let chain = chain_pack();
let (mut peer, joined) = serve_push(&fx, fx.ingest.clone(), false, Limits::default()).await;
let advertisement = read_pkts(&mut peer).await;
let lines = text_lines(&advertisement);
assert_eq!(
lines.len(),
2,
"an empty repo advertises exactly the sentinel line + flush: {lines:?}"
);
let caps_line = &lines[0];
assert!(
caps_line.contains("capabilities^{}"),
"the empty repo's sentinel: {caps_line}"
);
assert!(
caps_line.contains('\0'),
"the caps ride NUL-attached on the first emitted line: {caps_line}"
);
assert!(
caps_line.contains("agent=alkgit/"),
"the agent token is advertised: {caps_line}"
);
for served in ["report-status", "side-band-64k", "atomic"] {
assert!(
caps_line.contains(served),
"{served} stays served: {caps_line}"
);
}
assert!(
!caps_line.contains("quiet"),
"quiet is not advertised (ADR-013 §2): {caps_line}"
);
assert!(
!lines.iter().all(|line| line.contains("# service=")),
"no service prefix rides the duplex push advertisement"
);
assert_eq!(*lines.last().expect("the flush"), "", "flush-terminated");
let request = [
push_commands(
"report-status side-band-64k",
&[create_command("refs/heads/main", chain.commit_b)],
),
vec![
chain.pack[..1].to_vec(),
chain.pack[1..24].to_vec(),
chain.pack[24..].to_vec(),
],
]
.concat()
.concat();
peer.write_all(&request).await.unwrap();
let report_body = read_body_until_flush(&mut peer).await;
assert_eq!(
sideband_report_lines(&report_body),
vec!["unpack ok".to_owned(), "ok refs/heads/main".to_owned()],
"the report is unpack + per-ref in request order"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close ends the session quietly after the report");
assert_eq!(
fx.refs
.list_refs(&record)
.await
.unwrap()
.iter()
.filter_map(|line| Some((line.name.clone(), line.oid?)))
.collect::<BTreeMap<String, ObjectId>>(),
BTreeMap::from([("refs/heads/main".to_owned(), chain.commit_b)]),
"the CAS applied the pushed update"
);
assert!(
fx.ingest.objects().contains(&record, &chain.commit_b),
"the pushed objects landed via the ingest handle"
);
}
#[tokio::test]
async fn a_packless_delete_push_takes_the_peek_packless_path_and_drains_cleanly() {
use std::sync::atomic::Ordering::SeqCst;
let fx = fixture();
let record = repo("owner/doubled");
let current = ObjectId::from([0x1b; 20]);
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(std::sync::atomic::AtomicUsize::new(0));
let (mut peer, joined) = serve_push(
&fx,
counting_ingest(fx.ingest.clone(), Arc::clone(&runs)),
false,
Limits::default(),
)
.await;
read_pkts(&mut peer).await;
peer.write_all(
&push_commands(
"report-status side-band-64k",
&[delete_command("refs/heads/gone", current)],
)
.concat(),
)
.await
.unwrap();
let report_body = read_body_until_flush(&mut peer).await;
assert_eq!(
sideband_report_lines(&report_body),
vec!["unpack ok".to_owned(), "ok refs/heads/gone".to_owned()],
"the packless report equals the capture's shape, while the client stays open"
);
assert_eq!(
runs.load(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-only path deleted the ref"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the packless tail's drain ends cleanly, no mid-stream RST");
}
#[tokio::test]
async fn a_push_nothing_to_do_flush_replies_flush_with_no_report() {
use std::sync::atomic::Ordering::SeqCst;
let fx = fixture();
let runs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let (mut peer, joined) = serve_push(
&fx,
counting_ingest(fx.ingest.clone(), Arc::clone(&runs)),
false,
Limits::default(),
)
.await;
read_pkts(&mut peer).await;
peer.write_all(FLUSH).await.unwrap();
let reply = read_pkts(&mut peer).await;
assert_eq!(
reply,
vec![Vec::<u8>::new()],
"exactly one reply flush, no report"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the session ends quietly after the reply");
assert_eq!(
runs.load(SeqCst),
0,
"the nothing-to-do shape runs no prepare call"
);
}
#[tokio::test]
async fn a_partial_pack_header_at_the_peek_timeout_is_a_malformed_push() {
use std::sync::atomic::Ordering::SeqCst;
let fx = fixture();
let record = repo("owner/doubled");
let runs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let limits = Limits {
packless_peek: Duration::from_millis(150),
..Limits::default()
};
let (mut peer, joined) = serve_push(
&fx,
counting_ingest(fx.ingest.clone(), Arc::clone(&runs)),
false,
limits,
)
.await;
read_pkts(&mut peer).await;
peer.write_all(
&push_commands(
"report-status",
&[create_command(
"refs/heads/main",
ObjectId::from([0x2c; 20]),
)],
)
.concat(),
)
.await
.unwrap();
peer.write_all(b"PA").await.unwrap();
let band = text(&read_band(&mut peer).await);
assert!(
band.starts_with("ERR ") && band.contains("half-sent pack"),
"the partial header is the malformed-push band: {band}"
);
let result = joined
.await
.expect("the session task joins")
.expect_err("the malformed push is a typed failure");
assert!(
matches!(result, SubstrateError::Protocol(_)),
"a malformed push is protocol-shaped (banded): {result:?}"
);
assert_eq!(runs.load(SeqCst), 0, "no prepare call ran");
assert!(
fx.refs.list_refs(&record).await.unwrap().is_empty(),
"no ref applied"
);
}
#[tokio::test]
async fn the_negotiated_push_options_section_is_consumed_before_the_pack_peek() {
let fx = fixture();
let record = repo("owner/doubled");
let chain = chain_pack();
let seen = Arc::new(std::sync::Mutex::new(None));
let ingest: Arc<dyn GitPackIngest> = Arc::new(OptionsRecorder {
inner: Arc::clone(&fx.ingest),
seen: Arc::clone(&seen),
});
let (mut peer, joined) = serve_push(&fx, ingest, true, Limits::default()).await;
let advertisement = text_lines(&read_pkts(&mut peer).await);
assert!(
advertisement[0].contains("push-options"),
"the gate's token rides the gate-on advertisement: {:?}",
advertisement[0]
);
let request = [
push_commands(
"report-status side-band-64k push-options",
&[create_command("refs/heads/main", chain.commit_b)],
),
vec![pkt(b"deploy-key=abc"), FLUSH.to_vec(), chain.pack.clone()],
]
.concat()
.concat();
peer.write_all(&request).await.unwrap();
let report_body = read_body_until_flush(&mut peer).await;
assert_eq!(
sideband_report_lines(&report_body),
vec!["unpack ok".to_owned(), "ok refs/heads/main".to_owned()],
);
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"
);
drop(peer);
joined
.await
.expect("the session task joins")
.expect("the clean close after the report");
assert!(
fx.ingest.objects().contains(&record, &chain.commit_b),
"the pack after the options flush ingested byte-exact"
);
}
#[tokio::test]
async fn a_denied_push_is_the_generic_band_before_any_advertisement_byte() {
let unauthorized_registry = MemRegistry::default();
unauthorized_registry
.put_repo(&repo("owner/doubled"))
.await
.unwrap();
let unauthorized_registry: Arc<dyn GitRegistry> = Arc::new(unauthorized_registry);
let (result, wire) = denial_wire(unauthorized_registry, Service::ReceivePack).await;
match &result {
Err(SubstrateError::AccessDenied) => {}
other => panic!("the denial is the typed AccessDenied: {other:?}"),
}
assert_eq!(
pkt_payloads(&wire),
vec![b"ERR access denied: owner/doubled".to_vec()],
"the generic band names only the client-sent id; no advertisement preceded it"
);
}
#[tokio::test]
async fn the_wall_clock_bounds_the_push_session() {
let fx = fixture();
let limits = Limits {
session_wall_clock: Duration::from_millis(200),
..Limits::default()
};
let (mut peer, joined) = serve_push(&fx, fx.ingest.clone(), false, limits).await;
read_pkts(&mut peer).await;
let band = text(&read_band(&mut peer).await);
assert!(
band.starts_with("ERR ") && band.contains("session_wall_clock"),
"the breach bands the push session: {band}"
);
let mut remaining = [0u8; 1];
assert_eq!(
peer.read(&mut remaining).await.expect("read after close"),
0,
"the breached session closed"
);
let result = joined
.await
.expect("the session task joins")
.expect_err("the breach is a typed failure");
assert!(
matches!(
&result,
SubstrateError::Protocol(message) if message.contains("session_wall_clock")
),
"the typed failure returns: {result:?}"
);
}
#[tokio::test]
async fn a_push_advertisement_listing_failure_bands_the_session() {
struct ListingFailure;
#[async_trait::async_trait]
impl GitRefs for ListingFailure {
async fn list_refs(
&self,
_repo: &RepoRecord,
) -> Result<Vec<alkgit::RefLine>, StorageError> {
Err(StorageError::Invalid("listing unavailable".to_string()))
}
async fn apply_updates(
&self,
_repo: &RepoRecord,
_updates: Vec<RefUpdate>,
_atomic: bool,
) -> Result<Vec<alkgit::RefOutcome>, StorageError> {
Err(StorageError::Invalid("listing unavailable".to_string()))
}
}
let fx = fixture();
let refs: Arc<dyn GitRefs> = Arc::new(ListingFailure);
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
let ingest: Arc<dyn GitPackIngest> = fx.ingest.clone();
let (mut peer, joined) = serve(
&fx.write_marker,
refs,
gen,
ingest,
false,
Limits::default(),
Service::ReceivePack,
)
.await;
let band = text(&read_band(&mut peer).await);
assert!(band.starts_with("ERR "), "a listing failure bands: {band}");
assert!(
band.contains("listing unavailable"),
"the backend failure's own text bands through the map: {band}"
);
assert_eq!(
peer.read(&mut [0u8; 1]).await.expect("read after close"),
0,
"the banded session closed"
);
let result = joined
.await
.expect("the session task joins")
.expect_err("the failure is typed");
assert!(
matches!(result, SubstrateError::Protocol(_)),
"a backend failure band-maps to a protocol-shaped typed failure: {result:?}"
);
}
#[tokio::test]
async fn the_resolved_front_serves_the_anonymous_public_read_session() {
let fx = fixture();
let mut public = repo("owner/doubled");
public.visibility = Visibility::Public;
let registry = MemRegistry::default();
registry.put_repo(&public).await.unwrap();
let registry: Arc<dyn GitRegistry> = Arc::new(registry);
let (mut peer, stream) = duplex(1 << 16);
let refs: Arc<dyn GitRefs> = fx.refs.clone();
let gen: Arc<dyn GitPackGen> = fx.gen.clone();
let ingest: Arc<dyn GitPackIngest> = fx.ingest.clone();
let joined = tokio::spawn(async move {
serve_duplex_resolved(
"owner/doubled",
None,
Service::UploadPack,
stream,
Limits::default(),
registry,
refs,
gen,
ingest,
false,
Arc::new(Semaphore::new(
Limits::default().max_concurrent_blocking_tasks,
)),
)
.await
});
let advertisement = read_pkts(&mut peer).await;
assert_eq!(text_lines(&advertisement), advertisement_pkts());
peer.write_all(&command_request(b"command=ls-refs\n", &[], &[]))
.await
.unwrap();
let _ = read_pkts(&mut peer).await;
drop(peer);
joined
.await
.expect("the front task joins")
.expect("the clean close ends the session quietly");
}
#[test]
fn only_the_checked_marker_path_feeds_the_entries() {
let private = repo("owner/doubled");
let holder = identity("u-7731");
assert_eq!(
AuthorizedRepo::authorize(private.clone(), None, AccessAction::Read),
None,
"anonymous on a private repo is no marker"
);
assert_eq!(
AuthorizedRepo::authorize(private.clone(), Some(&holder), AccessAction::Read),
None,
"a grantless identity is no marker"
);
let mut granted = private;
granted
.grants
.insert("u-7731".to_string(), vec![AccessAction::Read]);
assert!(
AuthorizedRepo::authorize(granted, Some(&holder), AccessAction::Read).is_some(),
"the granted identity is the entry's input"
);
}