use std::io::Write as _;
use std::sync::Arc;
use gunnar_core::{NewRepo, RepoRef};
use gunnar_grpc::ControlPlane as _;
use gunnar_lfs::LfsStore as _;
use gunnar_proto::RepoPath;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
fn repo(stores: &ServerStores, path: &RepoRef) -> std::path::PathBuf {
let repo = stores
.create_repo(NewRepo::new(path.clone()), false)
.expect("create");
let handle = stores.repo_store(&repo).expect("open");
let git_dir = handle.git_dir().to_path_buf();
drop(handle);
git_dir
}
fn put(git_dir: &std::path::Path, body: &[u8]) -> gunnar_lfs::Oid {
let store = gunnar_lfs::FsLfsStore::open_or_init(git_dir).expect("lfs store");
let oid = gunnar_lfs::Oid::of(body);
let mut write = store.begin_write(&oid, body.len() as u64).expect("begin");
write.write_all(body).expect("write");
write.commit().expect("commit");
oid
}
fn wire(path: &RepoRef) -> RepoPath {
RepoPath {
namespace: path.namespace().to_owned(),
repo: path.name().to_owned(),
}
}
#[test]
fn an_lfs_object_is_served_whole_and_byte_identical() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.catalog()
.create_namespace("team")
.expect("namespace");
let path = RepoRef::new("team", "one").expect("legal");
let git_dir = repo(&stores, &path);
let body: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
let oid = put(&git_dir, &body);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let answer = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
.expect("the object is served");
assert_eq!(answer.oid, oid.to_string());
assert_eq!(answer.size, body.len() as u64);
assert_eq!(answer.data, body, "the bytes are the stored bytes");
assert_eq!(gunnar_lfs::Oid::of(&answer.data), oid);
}
#[test]
fn an_object_over_the_ceiling_is_refused_rather_than_truncated() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.catalog()
.create_namespace("team")
.expect("namespace");
let path = RepoRef::new("team", "one").expect("legal");
let git_dir = repo(&stores, &path);
let body = vec![0x4cu8; 300_000];
let oid = put(&git_dir, &body);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let refused = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 100_000, None)
.expect_err("100 KB is not enough for a 300 KB object");
let message = format!("{refused:?}");
assert!(message.contains("300000"), "the size is named: {message}");
assert!(
message.to_ascii_lowercase().contains("git lfs"),
"the refusal says where the bytes are: {message}"
);
let answer = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 400_000, None)
.expect("served under a ceiling that fits");
assert_eq!(answer.data.len(), body.len());
}
#[test]
fn a_missing_object_is_not_found_and_a_malformed_oid_is_invalid() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.catalog()
.create_namespace("team")
.expect("namespace");
let path = RepoRef::new("team", "one").expect("legal");
repo(&stores, &path);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let missing = plane
.read_lfs_object(&wire(&path), &"f0".repeat(32), 0, None)
.expect_err("nothing was stored");
assert!(
matches!(missing, gunnar_grpc::seam::ControlError::NotFound(_)),
"{missing:?}"
);
let malformed = plane
.read_lfs_object(&wire(&path), "not-an-oid", 0, None)
.expect_err("that is not an LFS oid");
assert!(
matches!(
malformed,
gunnar_grpc::seam::ControlError::InvalidArgument(_)
),
"{malformed:?}"
);
}
fn one_object(
body: &[u8],
) -> (
tempfile::TempDir,
CoreControlPlane,
RepoRef,
gunnar_lfs::Oid,
) {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.catalog()
.create_namespace("team")
.expect("namespace");
let path = RepoRef::new("team", "one").expect("legal");
let git_dir = repo(&stores, &path);
let oid = put(&git_dir, body);
(dir, CoreControlPlane::new(Arc::clone(&stores)), path, oid)
}
#[test]
fn an_object_over_the_ceiling_is_read_whole_range_by_range() {
let body: Vec<u8> = (0..5_968_091u32).map(|i| (i % 251) as u8).collect();
let (_dir, plane, path, oid) = one_object(&body);
let mut got = Vec::new();
let mut ranges = 0;
while (got.len() as u64) < body.len() as u64 {
let offset = got.len() as u64;
let answer = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((offset, 0)))
.expect("a range is served");
assert_eq!(answer.offset, offset, "the range says where it starts");
assert_eq!(answer.size, body.len() as u64, "and how large the whole is");
assert!(
answer.data.len() as u64 <= 4 * 1024 * 1024,
"at most the ceiling"
);
assert!(!answer.data.is_empty());
got.extend_from_slice(&answer.data);
ranges += 1;
}
assert_eq!(ranges, 2, "4 MiB, then the rest");
assert_eq!(got, body, "the ranges are the object");
assert_eq!(gunnar_lfs::Oid::of(&got), oid);
let refused = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
.expect_err("whole, it is over the ceiling");
assert!(format!("{refused:?}").contains("5968091"), "{refused:?}");
}
#[test]
fn a_range_is_exactly_the_bytes_asked_for() {
let body: Vec<u8> = (0..300_000u32).map(|i| (i % 241) as u8).collect();
let (_dir, plane, path, oid) = one_object(&body);
let mid = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 100_000, Some((1_000, 64)))
.expect("an in-range read");
assert_eq!(mid.data, &body[1_000..1_064]);
assert_eq!((mid.offset, mid.size), (1_000, 300_000));
let head = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((0, 24)))
.expect("the header alone");
assert_eq!(head.data, &body[..24]);
let tail = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 100_000, Some((250_000, 0)))
.expect("to the ceiling, which the end comes before");
assert_eq!(tail.data, &body[250_000..]);
assert_eq!(
tail.offset + tail.data.len() as u64,
tail.size,
"the last range"
);
}
#[test]
fn an_offset_past_the_end_and_a_range_over_the_ceiling_are_refused() {
let body = vec![7u8; 1_000];
let (_dir, plane, path, oid) = one_object(&body);
for offset in [1_000, 5_000] {
let past = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, Some((offset, 10)))
.expect_err("past the end");
assert!(
matches!(past, gunnar_grpc::seam::ControlError::InvalidArgument(_)),
"{past:?}"
);
let said = format!("{past:?}");
assert!(
said.contains(&offset.to_string()) && said.contains("1000"),
"{said}"
);
}
let over = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 500, Some((0, 501)))
.expect_err("a range over the caller's ceiling");
assert!(
matches!(over, gunnar_grpc::seam::ControlError::ResourceExhausted(_)),
"{over:?}"
);
let over = plane
.read_lfs_object(
&wire(&path),
&oid.to_string(),
0,
Some((0, 4 * 1024 * 1024 + 1)),
)
.expect_err("a range over the server's ceiling");
assert!(
matches!(over, gunnar_grpc::seam::ControlError::ResourceExhausted(_)),
"{over:?}"
);
}
#[test]
fn a_request_without_a_range_is_the_whole_read_it_always_was() {
let body: Vec<u8> = (0..70_000u32).map(|i| (i % 13) as u8).collect();
let (_dir, plane, path, oid) = one_object(&body);
let whole = plane
.read_lfs_object(&wire(&path), &oid.to_string(), 0, None)
.expect("whole");
assert_eq!(whole.data, body);
assert_eq!((whole.offset, whole.size), (0, body.len() as u64));
}