use futures_util::StreamExt;
use super::*;
use crate::storage::crypt::Keyring;
use crate::storage::s3::tests::{bucket, redirecting, store};
fn keyring() -> std::sync::Arc<Keyring> {
std::sync::Arc::new(Keyring::parse(&hex::encode([7u8; crate::storage::crypt::KEY])).unwrap())
}
fn namespace() -> Namespace {
Namespace::new("FerrLabs", "Blastlands").unwrap()
}
fn bucket_store(root: &tempfile::TempDir, endpoint: &str) -> Store {
Store::bucket(store(endpoint), LocalStore::new(root.path()))
}
async fn read_back(store: &Store, ns: &Namespace, oid: &str) -> Vec<u8> {
let object = store.open(ns, oid).await.unwrap();
let size = object.size();
let mut chunks = object.stream(0, size).await.unwrap();
let mut out = Vec::new();
while let Some(chunk) = chunks.next().await {
out.extend_from_slice(&chunk.unwrap());
}
out
}
#[tokio::test]
async fn an_upload_lands_in_the_bucket_and_reads_back_through_the_same_seam() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
let store = bucket_store(&root, &endpoint);
let payload = b"an asset that never touches this disk for long".repeat(32);
let oid = hex::encode(sha2::Sha256::digest(&payload));
let written = store
.write(
&namespace(),
&oid,
Some(payload.len() as u64),
None,
futures_util::stream::iter([Ok::<_, std::io::Error>(axum::body::Bytes::from(
payload.clone(),
))]),
)
.await
.unwrap();
assert_eq!(written, payload.len() as u64);
assert!(store.exists(&namespace(), &oid).await);
assert_eq!(read_back(&store, &namespace(), &oid).await, payload);
}
#[tokio::test]
async fn a_bucket_holds_the_object_even_when_the_server_was_told_to_compress() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
let store = Store::bucket(
store(&endpoint),
LocalStore::new(root.path()).with_compression(Some(3)),
);
let payload = b"a mesh that gives up most of its ground to zstd ".repeat(4096);
let oid = hex::encode(sha2::Sha256::digest(&payload));
store
.write(
&namespace(),
&oid,
Some(payload.len() as u64),
None,
futures_util::stream::iter([Ok::<_, std::io::Error>(axum::body::Bytes::from(
payload.clone(),
))]),
)
.await
.unwrap();
let restored = read_back(&store, &namespace(), &oid).await;
assert_eq!(
hex::encode(sha2::Sha256::digest(&restored)),
oid,
"the client asked for the object named by this digest and has no way to know the server framed it on the way past: {} bytes came back",
restored.len()
);
assert_eq!(restored, payload);
}
#[tokio::test]
async fn the_staging_file_does_not_outlive_the_upload() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
let store = bucket_store(&root, &endpoint);
let payload = b"an asset passing through".to_vec();
let oid = hex::encode(sha2::Sha256::digest(&payload));
store
.write(
&namespace(),
&oid,
None,
None,
futures_util::stream::iter([Ok::<_, std::io::Error>(axum::body::Bytes::from(payload))]),
)
.await
.unwrap();
let leftovers = crate::storage::tests::staging_files(root.path());
assert!(
leftovers.is_empty(),
"local disk is a write buffer here, and one that is never emptied is a disk that \
fills: {leftovers:?}"
);
}
#[tokio::test]
async fn a_bucket_reports_no_capacity_rather_than_an_empty_one() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
assert!(
bucket_store(&root, &endpoint).capacity().await.is_none(),
"zero would be read as an empty store by every dashboard that averages it"
);
assert!(
Store::local(LocalStore::new(root.path()))
.capacity()
.await
.is_some()
);
}
#[tokio::test]
async fn the_maintenance_commands_say_they_do_not_apply_rather_than_lying() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
let store = bucket_store(&root, &endpoint);
let ns = namespace();
for outcome in [
store.dedupe(&ns, true).await.err(),
store.compress(&ns, true).await.err(),
store.verify(&ns).await.err(),
] {
assert!(
matches!(outcome, Some(Error::Unsupported(_))),
"an operator running one of these against a bucket has to be told it did nothing, not handed an empty report that reads like success: {outcome:?}"
);
}
assert!(
store
.sweep(&ns, &std::collections::HashSet::new(), Duration::ZERO, true)
.await
.is_ok(),
"collection is implemented for a bucket and must not answer Unsupported"
);
}
#[tokio::test]
async fn a_download_is_never_redirected_to_a_frame() {
for label in ["compressed", "encrypted"] {
let root = tempfile::tempdir().unwrap();
let (endpoint, objects) = bucket().await;
let staging = match label {
"compressed" => LocalStore::new(root.path()).with_compression(Some(3)),
_ => LocalStore::new(root.path()).with_encryption(Some(keyring())),
};
let store = Store::bucket(redirecting(&endpoint), staging);
let payload = b"a scene file that compresses and must still come back whole ".repeat(512);
let oid = hex::encode(sha2::Sha256::digest(&payload));
store
.write(
&namespace(),
&oid,
Some(payload.len() as u64),
None,
futures_util::stream::iter([Ok::<_, std::io::Error>(axum::body::Bytes::from(
payload.clone(),
))]),
)
.await
.unwrap();
let stored = objects
.lock()
.unwrap()
.values()
.find(|object| !object.is_empty())
.cloned()
.unwrap();
assert_ne!(
stored, payload,
"{label}: the bucket holds a frame, which is the premise of the rest of this test"
);
assert_eq!(
store.redirect(&oid),
None,
"{label}: a client sent to the bucket would hash {} bytes of frame and reject the \
object it asked for",
stored.len()
);
assert_eq!(
read_back(&store, &namespace(), &oid).await,
payload,
"{label}: giving up the redirect is only correct because the streamed path decodes"
);
}
}
#[tokio::test]
async fn a_bucket_holding_the_object_itself_still_redirects() {
let root = tempfile::tempdir().unwrap();
let (endpoint, _objects) = bucket().await;
let store = Store::bucket(redirecting(&endpoint), LocalStore::new(root.path()));
let payload = b"an object stored as it arrived".repeat(32);
let oid = hex::encode(sha2::Sha256::digest(&payload));
store
.write(
&namespace(),
&oid,
Some(payload.len() as u64),
None,
futures_util::stream::iter([Ok::<_, std::io::Error>(axum::body::Bytes::from(
payload.clone(),
))]),
)
.await
.unwrap();
assert!(store.redirect(&oid).is_some());
}