#![cfg(feature = "test-util")]
mod common;
use std::sync::Arc;
use bytes::Bytes;
use git_remote_object_store::object_store::mock::MockStore;
use git_remote_object_store::object_store::{ObjectStore, PutOpts};
use git_remote_object_store::protocol::ProtocolError;
use git_remote_object_store::url::RemoteUrl;
use time::Duration;
use time::OffsetDateTime;
use common::{drive_in, s3_url};
const SHA_A: &str = "0000000000000000000000000000000000000001";
const SHA_B: &str = "0000000000000000000000000000000000000002";
const SHA_C: &str = "0000000000000000000000000000000000000003";
async fn drive(
remote: RemoteUrl,
store: Arc<dyn ObjectStore>,
script: &str,
) -> (Vec<u8>, Result<(), ProtocolError>) {
drive_in(remote, store, script, std::env::temp_dir()).await
}
#[tokio::test]
async fn packchain_capabilities_succeeds() {
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let store: Arc<dyn ObjectStore> = Arc::new(MockStore::new());
let (out, result) = drive(remote, store, "capabilities\n").await;
result.expect("capabilities must succeed for packchain");
assert_eq!(&out, b"*push\n*fetch\noption\n\n");
}
#[tokio::test]
async fn packchain_capabilities_advertises_bundle_uri_when_opted_in() {
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain&bundle_uri=1";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let store: Arc<dyn ObjectStore> = Arc::new(MockStore::new());
let (out, result) = drive(remote, store, "capabilities\n").await;
result.expect("capabilities must succeed");
assert_eq!(
&out, b"*push\n*fetch\noption\nbundle-uri\n\n",
"bundle-uri line must precede the trailing terminator",
);
}
#[tokio::test]
async fn bundle_engine_with_bundle_uri_flag_does_not_advertise() {
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?bundle_uri=1";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let store: Arc<dyn ObjectStore> = Arc::new(MockStore::new());
let (out, result) = drive(remote, store, "capabilities\n").await;
result.expect("capabilities must succeed");
assert_eq!(
&out, b"*push\n*fetch\noption\n\n",
"bundle-engine remote must not advertise bundle-uri",
);
}
#[tokio::test]
async fn bundle_uri_command_emits_per_ref_entries_with_creation_token() {
let store = MockStore::new();
store.insert("repo/FORMAT", Bytes::from_static(b"packchain"));
store.insert(
"repo/refs/heads/main/chain.json",
Bytes::from(
format!(
r#"{{"v":1,"tip":"{SHA_A}","full_at":"{SHA_B}","segments":[{{"sha":"{SHA_A}","parent_sha":null,"pack":"packs/{SHA_C}.pack","bytes":1024}}]}}"#,
)
.into_bytes(),
),
);
let store: Arc<dyn ObjectStore> = Arc::new(store);
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain&bundle_uri=1";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let (out, result) = drive(remote, store, "capabilities\nbundle-uri\n").await;
result.expect("capabilities + bundle-uri must succeed");
let text = std::str::from_utf8(&out).unwrap();
assert!(
text.starts_with("*push\n*fetch\noption\nbundle-uri\n\n"),
"{text}"
);
assert!(
text.contains(&format!(
"bundle.refs/heads/main.uri=https://my-bucket.s3.us-west-2.amazonaws.com/repo/refs/heads/main/{SHA_B}.bundle\n"
)),
"{text}",
);
assert!(
text.contains(&format!("bundle.refs/heads/main.creationToken={SHA_B}\n")),
"{text}",
);
assert!(!text.contains(&format!("creationToken={SHA_A}")), "{text}");
}
#[tokio::test]
async fn bundle_uri_command_when_capability_not_advertised_emits_terminator_only() {
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let store: Arc<dyn ObjectStore> = Arc::new(MockStore::new());
let (out, result) = drive(remote, store, "bundle-uri\n").await;
result.expect("bundle-uri command must succeed");
assert_eq!(&out, b"\n");
}
#[tokio::test]
async fn packchain_fetch_against_empty_bucket_surfaces_chain_absent() {
use git_remote_object_store::PackchainError;
use git_remote_object_store::protocol::fetch::FetchError;
let raw = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain";
let remote = git_remote_object_store::url::parse(raw).expect("URL parses");
let store: Arc<dyn ObjectStore> = Arc::new(MockStore::new());
let (_out, result) = drive(
remote,
store,
"fetch 0123456789abcdef0123456789abcdef01234567 refs/heads/main\n\n",
)
.await;
let err = result.expect_err("packchain fetch on empty bucket must error");
assert!(
matches!(
err,
ProtocolError::Fetch(FetchError::Packchain(PackchainError::ChainAbsent { .. }))
),
"expected Fetch(Packchain(ChainAbsent)), got {err:?}",
);
}
#[tokio::test]
async fn packchain_format_resolves_engine_even_without_url_flag() {
use git_remote_object_store::PackchainError;
use git_remote_object_store::protocol::fetch::FetchError;
let store = MockStore::new();
store.insert("repo/FORMAT", Bytes::from_static(b"packchain"));
let store: Arc<dyn ObjectStore> = Arc::new(store);
let (_out, result) = drive(
s3_url(Some("repo")),
store,
"fetch 0123456789abcdef0123456789abcdef01234567 refs/heads/main\n\n",
)
.await;
let err = result.expect_err("packchain fetch on empty bucket must error");
assert!(
matches!(
err,
ProtocolError::Fetch(FetchError::Packchain(PackchainError::ChainAbsent { .. }))
),
"FORMAT must drive engine resolution to packchain fetch; got {err:?}",
);
}
#[tokio::test]
async fn packchain_list_returns_chain_tip_not_full_at() {
let store = MockStore::new();
let chain_json = format!(
r#"{{"v":1,"tip":"{SHA_B}","full_at":"{SHA_A}","segments":[{{"sha":"{SHA_B}","parent_sha":"{SHA_A}","pack":"packs/{SHA_C}.pack","bytes":1024}}]}}"#
);
store.insert(
"repo/refs/heads/main/chain.json",
Bytes::from(chain_json.into_bytes()),
);
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"baseline"),
);
store.insert("repo/FORMAT", Bytes::from_static(b"packchain"));
store.insert("repo/HEAD", Bytes::from_static(b"refs/heads/main"));
let url_str = "s3+https://my-bucket.s3.us-west-2.amazonaws.com/repo?engine=packchain";
let remote = git_remote_object_store::url::parse(url_str).expect("URL parses");
let (out, result) = drive(remote, Arc::new(store), "list\n").await;
result.expect("packchain list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(
text,
format!("@refs/heads/main HEAD\n{SHA_B} refs/heads/main\n\n"),
"packchain list must report chain.tip ({SHA_B}), not full_at ({SHA_A})",
);
assert!(
!text.contains(SHA_A),
"list output must not include the baseline `full_at` sha; got {text:?}",
);
}
#[tokio::test]
async fn capabilities_emits_exact_block() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"capabilities\n",
)
.await;
result.expect("capabilities should succeed");
assert_eq!(&out, b"*push\n*fetch\noption\n\n");
}
#[tokio::test]
async fn list_empty_bucket_emits_terminator() {
let (out, result) = drive(s3_url(Some("repo")), Arc::new(MockStore::new()), "list\n").await;
result.expect("list should succeed");
assert_eq!(&out, b"\n");
}
#[tokio::test]
async fn list_for_push_skips_head_lookup() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle a"),
);
store.insert("repo/HEAD", Bytes::from_static(b"refs/heads/main"));
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list for-push\n").await;
result.expect("list for-push should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
}
#[tokio::test]
async fn list_emits_head_pointer_when_ref_present() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert("repo/HEAD", Bytes::from_static(b"refs/heads/main\n"));
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(
text,
format!("@refs/heads/main HEAD\n{SHA_A} refs/heads/main\n\n")
);
}
#[tokio::test]
async fn list_omits_head_when_pointed_ref_has_no_bundle() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/feature/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert("repo/HEAD", Bytes::from_static(b"refs/heads/main"));
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/feature\n\n"));
}
#[tokio::test]
async fn list_swallows_missing_head_silently() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed even without HEAD");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
}
#[tokio::test]
async fn list_sorts_bundles_by_last_modified_desc() {
let store = MockStore::new();
let now = OffsetDateTime::now_utc();
store.insert_with(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"old"),
now - Duration::seconds(60),
PutOpts::default(),
);
store.insert_with(
format!("repo/refs/heads/main/{SHA_B}.bundle"),
Bytes::from_static(b"new"),
now,
PutOpts::default(),
);
store.insert_with(
format!("repo/refs/heads/main/{SHA_C}.bundle"),
Bytes::from_static(b"middle"),
now - Duration::seconds(30),
PutOpts::default(),
);
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
let expected =
format!("{SHA_B} refs/heads/main\n{SHA_C} refs/heads/main\n{SHA_A} refs/heads/main\n\n");
assert_eq!(text, expected);
}
#[tokio::test]
async fn list_filters_non_bundle_keys() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
let upper_sha = SHA_A.to_uppercase();
store.insert(
format!("repo/refs/heads/main/{upper_sha}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert(format!("repo/lfs/{SHA_A}"), Bytes::from_static(b"lfs"));
store.insert(
"repo/refs/heads/main/LOCK#.lock",
Bytes::from_static(b"lock"),
);
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list for-push\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
}
#[tokio::test]
async fn list_rejects_sibling_prefix_collision() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert(
format!("repo-other/refs/heads/main/{SHA_B}.bundle"),
Bytes::from_static(b"bundle"),
);
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list for-push\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
assert!(!text.contains(SHA_B));
}
#[tokio::test]
async fn list_works_with_no_prefix() {
let store = MockStore::new();
store.insert(
format!("refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
let (out, result) = drive(s3_url(None), Arc::new(store), "list for-push\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
}
#[tokio::test]
async fn option_verbosity_two_responds_ok() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option verbosity 2\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"ok\n");
}
#[tokio::test]
async fn option_verbosity_zero_responds_unsupported() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option verbosity 0\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"unsupported\n");
}
#[tokio::test]
async fn option_verbosity_one_responds_unsupported() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option verbosity 1\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"unsupported\n");
}
#[tokio::test]
async fn option_verbosity_three_responds_ok() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option verbosity 3\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"ok\n");
}
#[tokio::test]
async fn option_verbosity_four_responds_ok() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option verbosity 4\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"ok\n");
}
#[tokio::test]
async fn option_unknown_key_responds_unsupported() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"option progress true\n",
)
.await;
result.expect("option should succeed");
assert_eq!(&out, b"unsupported\n");
}
#[tokio::test]
async fn empty_line_emits_terminator_in_idle_mode() {
let (out, result) = drive(s3_url(Some("repo")), Arc::new(MockStore::new()), "\n").await;
result.expect("blank line should succeed");
assert_eq!(&out, b"\n");
}
#[tokio::test]
async fn invalid_command_returns_error() {
let (_out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"nonsense\n",
)
.await;
match result {
Err(ProtocolError::InvalidCommand(line)) => assert_eq!(line, "nonsense"),
other => panic!("expected InvalidCommand error, got {other:?}"),
}
}
#[tokio::test]
async fn push_with_malformed_args_returns_parse_error() {
use git_remote_object_store::protocol::push::PushError;
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"push not-a-refspec\n\n",
)
.await;
match result {
Err(ProtocolError::Push(PushError::Parse { .. })) => {}
other => panic!("expected Push(Parse) error, got {other:?}"),
}
assert!(
out.is_empty(),
"push must not write on parse error: {out:?}"
);
}
#[tokio::test]
async fn stdin_eof_exits_cleanly() {
let (out, result) = drive(s3_url(Some("repo")), Arc::new(MockStore::new()), "").await;
result.expect("EOF should be a clean exit");
assert!(out.is_empty());
}
#[tokio::test]
async fn batched_command_then_blank_line_emits_terminator() {
let (out, result) = drive(
s3_url(Some("repo")),
Arc::new(MockStore::new()),
"capabilities\n\n",
)
.await;
result.expect("script should succeed");
assert_eq!(&out, b"*push\n*fetch\noption\n\n\n");
}
#[tokio::test]
async fn head_with_trailing_whitespace_is_trimmed() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert("repo/HEAD", Bytes::from_static(b" refs/heads/main\n \n"));
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert_eq!(
text,
format!("@refs/heads/main HEAD\n{SHA_A} refs/heads/main\n\n")
);
}
#[tokio::test]
async fn head_with_empty_body_is_ignored() {
let store = MockStore::new();
store.insert(
format!("repo/refs/heads/main/{SHA_A}.bundle"),
Bytes::from_static(b"bundle"),
);
store.insert("repo/HEAD", Bytes::from_static(b" \n"));
let (out, result) = drive(s3_url(Some("repo")), Arc::new(store), "list\n").await;
result.expect("list should succeed");
let text = std::str::from_utf8(&out).unwrap();
assert!(!text.contains("HEAD\n"));
assert_eq!(text, format!("{SHA_A} refs/heads/main\n\n"));
}
#[tokio::test]
async fn fetch_then_push_mode_flip_drops_buffered_fetch() {
let script = format!("fetch {SHA_A} refs/heads/main\npush :refs/heads/main\n\n");
let (out, result) = drive(s3_url(Some("repo")), Arc::new(MockStore::new()), &script).await;
result.expect("mode flip script should succeed");
let text = std::str::from_utf8(&out).expect("stdout utf-8");
assert_eq!(
text, "error refs/heads/main \"not found\"?\n\n",
"expected only the push outcome line, got {text:?}",
);
}