#![cfg(feature = "sparse-checkout")]
#![allow(clippy::unwrap_used)]
use std::path::PathBuf;
use mkit_core::hash::{Hash, to_hex};
use mkit_core::object::{EntryMode, Tree, TreeEntry};
use mkit_core::sparse::{
SparseResponse, build_sparse, encode_sparse_response, hash_filter, tree_hash, verify_sparse,
};
use mkit_transport_http::HttpTransport;
use mockito::{Matcher, Server};
fn entry(name: &[u8]) -> TreeEntry {
TreeEntry {
name: name.to_vec(),
mode: EntryMode::Blob,
object_hash: [0u8; 32],
}
}
fn tree_for(names: &[&[u8]]) -> Tree {
Tree {
entries: names.iter().copied().map(entry).collect(),
}
}
fn make_transport(server: &Server) -> HttpTransport {
let url = format!("mkit+{}/myproj", server.url());
HttpTransport::connect(&url).expect("connect to mockito")
}
fn sparse_path(tree: &Hash) -> String {
format!("/myproj/trees/{}/sparse", to_hex(tree))
}
#[test]
fn http_sparse_fetch_round_trip_verifies() {
let tree = tree_for(&[b"a", b"b", b"c"]);
let th = tree_hash(&tree);
let filter = vec![PathBuf::from("a")];
let fh = hash_filter(&filter);
let (entries, manifest, proof) = build_sparse(&tree, &filter).unwrap();
let body = encode_sparse_response(&SparseResponse {
manifest,
entries,
proof,
})
.unwrap();
let mut server = Server::new();
let path = sparse_path(&th);
let query = format!("sparse={}", to_hex(&fh));
let _m = server
.mock("POST", path.as_str())
.match_query(Matcher::Exact(query))
.match_header("content-type", "application/json")
.with_status(200)
.with_header("content-type", "application/x-mkit-sparse")
.with_body(body)
.create();
let tx = make_transport(&server);
let resp = tx
.fetch_sparse_tree(&th, &filter)
.expect("HTTP sparse fetch must succeed");
assert!(verify_sparse(
&resp.manifest,
&resp.entries,
&filter,
&resp.proof
));
assert_eq!(resp.manifest.tree_hash, th);
assert_eq!(resp.manifest.filter_hash, fh);
assert_eq!(resp.entries.len(), 1);
assert_eq!(resp.entries[0].name, b"a");
}
#[test]
fn http_sparse_fetch_rejects_tampered_bitmap() {
let tree = tree_for(&[b"a", b"b", b"c"]);
let th = tree_hash(&tree);
let filter = vec![PathBuf::from("a")];
let (entries, manifest, mut proof) = build_sparse(&tree, &filter).unwrap();
proof.bitmap_bytes[0] ^= 0b0000_0010; let body = encode_sparse_response(&SparseResponse {
manifest,
entries,
proof,
})
.unwrap();
let mut server = Server::new();
let path = sparse_path(&th);
let _m = server
.mock("POST", path.as_str())
.match_query(Matcher::Any)
.with_status(200)
.with_header("content-type", "application/x-mkit-sparse")
.with_body(body)
.create();
let tx = make_transport(&server);
let resp = tx.fetch_sparse_tree(&th, &filter).unwrap();
assert!(
!verify_sparse(&resp.manifest, &resp.entries, &filter, &resp.proof),
"tampered bitmap MUST be rejected by verify_sparse"
);
}
#[test]
fn http_sparse_fetch_404_is_pack_not_found() {
let tree = tree_for(&[b"a"]);
let th = tree_hash(&tree);
let filter = vec![PathBuf::from("a")];
let mut server = Server::new();
let _m = server
.mock("POST", sparse_path(&th).as_str())
.match_query(Matcher::Any)
.with_status(404)
.create();
let tx = make_transport(&server);
let err = tx.fetch_sparse_tree(&th, &filter).unwrap_err();
assert!(
matches!(err, mkit_core::protocol::TransportError::PackNotFound),
"404 must surface as PackNotFound, got {err:?}"
);
}
#[test]
fn http_sparse_fetch_garbage_body_is_invalid_response() {
let tree = tree_for(&[b"a"]);
let th = tree_hash(&tree);
let filter = vec![PathBuf::from("a")];
let mut server = Server::new();
let _m = server
.mock("POST", sparse_path(&th).as_str())
.match_query(Matcher::Any)
.with_status(200)
.with_body(b"this is not a sparse envelope")
.create();
let tx = make_transport(&server);
let err = tx.fetch_sparse_tree(&th, &filter).unwrap_err();
assert!(
matches!(err, mkit_core::protocol::TransportError::InvalidResponse),
"bad envelope must surface as InvalidResponse, got {err:?}"
);
}