use std::num::NonZeroUsize; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use remora_domain::OpaqueId; use remora_stream::TargetBody; use remora_target::{ CreateObjectRequest, ObjectIdentity, RESERVED_NAMESPACE_SEGMENT, TargetError, TargetStore, TargetStoreConfig, attempt_object_key, hash_bytes, is_reserved_key, validate_attempt_key, }; fn require_env() -> bool { std::env::var("REMORA_TARGET_REQUIRE").is_ok() } fn store_from_env() -> Option { match TargetStoreConfig::from_env() { Ok(config) => Some(TargetStore::connect(config).expect("connect target")), Err(error) => { if require_env() { panic!("target env required when REMORA_TARGET_REQUIRE is set: {error}"); } eprintln!("skipping target test: {error}"); None } } } fn unique_id(prefix: &str) -> OpaqueId { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time") .as_nanos(); let value = format!("{prefix}{nanos:x}"); OpaqueId::parse(&value).unwrap_or_else(|_| { OpaqueId::parse(format!("{prefix}aaaaaaaa")).expect("fallback opaque id") }) } fn attempt_key(label: &str) -> String { attempt_object_key( &unique_id(&format!("att{label}")), &unique_id(&format!("obj{label}")), None, ) } fn create_request(key: impl Into) -> CreateObjectRequest { CreateObjectRequest::parse(key).expect("valid create-only request") } #[tokio::test] async fn create_only_independently_hashes_and_rejects_wrong_expectation() { let Some(store) = store_from_env() else { return; }; let body = b"immutable-member-v1".as_slice(); let digest = hash_bytes(body); let key = attempt_key("csha"); assert!(is_reserved_key(&key)); assert!(validate_attempt_key(&key).is_ok()); assert!(validate_attempt_key("not-reserved/key").is_err()); let wrong = store .put_create_only( &create_request(key.clone()).with_expected_content_sha256(hash_bytes(b"other-payload")), body, ) .await; assert!( matches!(wrong, Err(TargetError::Integrity(_))), "wrong expectation must fail: {wrong:?}" ); let stored = store .put_create_only( &create_request(key.clone()).with_expected_content_sha256(digest.clone()), body, ) .await .expect("create-only put"); assert_eq!(stored.identity.key, key); assert_eq!( stored.identity.target_store_id, store.config().target_store_id ); assert_eq!(stored.content_sha256, digest); assert_eq!( store.read_exact(&stored.identity).await.expect("read"), body ); let head = store.head_exact(&stored.identity).await.expect("head"); assert_eq!(head.content_sha256, Some(digest)); let conflict = store .put_create_only(&create_request(key.clone()), b"replacement".as_slice()) .await; assert!( matches!(conflict, Err(TargetError::AlreadyExists)), "expected create-only conflict, got {conflict:?}" ); // Cross-store identity must fail closed. let mut foreign = stored.identity.clone(); foreign.target_store_id = "other-store".into(); let cross = store.read_exact(&foreign).await; assert!( matches!(cross, Err(TargetError::Validation(_))), "cross-store identity: {cross:?}" ); } #[tokio::test] async fn streaming_create_only_computes_digest() { let Some(store) = store_from_env() else { return; }; let payload = vec![0x5Au8; 256 * 1024]; let expected = hash_bytes(&payload); let key = attempt_key("strm"); let mut body = store .open_create_only( &create_request(key.clone()) .with_expected_content_sha256(expected.clone()) .with_chunk_size(NonZeroUsize::new(64 * 1024).expect("non-zero chunk size")), ) .await .expect("open writer"); body.write_all(&payload).await.expect("write"); let stored = body.finish_object().await.expect("finish"); assert_eq!(stored.bytes_written, payload.len() as u64); assert_eq!(stored.content_sha256, expected); assert_eq!(stored.identity.key, key); assert_eq!( store.read_exact(&stored.identity).await.expect("read"), payload ); } #[tokio::test] async fn multipart_relist_complete_abort_and_create_only() { let Some(store) = store_from_env() else { return; }; let replaced_part_one = vec![b'x'; 5 * 1024 * 1024]; let part_one = vec![b'a'; 5 * 1024 * 1024]; let part_two = vec![b'b'; 1024]; let combined = [part_one.as_slice(), part_two.as_slice()].concat(); let digest = hash_bytes(&combined); let key = attempt_key("mpu"); let multipart_request = create_request(key.clone()).with_expected_content_sha256(digest.clone()); let session = store .begin_multipart(&multipart_request) .await .expect("begin"); let p2 = store .upload_part(&session, 2, part_two.clone().into()) .await .expect("part 2"); let replaced_p1 = store .upload_part(&session, 1, replaced_part_one.into()) .await .expect("original part 1"); let stale = store.list_parts(&session).await.expect("stale part list"); let p1 = store .upload_part(&session, 1, part_one.clone().into()) .await .expect("replacement part 1"); let durable_identity = session.identity().clone(); drop(session); let (session, listed) = store .resume_multipart(durable_identity.clone(), Some(digest.clone())) .await .expect("resume and re-list"); assert_eq!(listed.len(), 2); assert_eq!(listed[0].etag, p1.etag); assert_eq!(listed[1].etag, p2.etag); assert_ne!(listed[0].etag, replaced_p1.etag); let stale_completion = store.complete_multipart(session, &stale).await; assert!( matches!(stale_completion, Err(TargetError::Integrity(_))), "completion must reject a replaced part: {stale_completion:?}" ); let (session, listed) = store .resume_multipart(durable_identity, Some(digest.clone())) .await .expect("resume after stale completion"); let completed = store .complete_multipart(session, &listed) .await .expect("complete"); assert_eq!(completed.identity.key, key); assert_eq!(completed.content_sha256, digest); assert_eq!( store .head_exact(&completed.identity) .await .expect("head multipart") .content_sha256, None ); let read = store .read_exact(&completed.identity) .await .expect("read completed"); assert_eq!(read.as_ref(), combined.as_slice()); let occupied = store.begin_multipart(&multipart_request).await; assert!( matches!(occupied, Err(TargetError::AlreadyExists)), "multipart begin on occupied key: {occupied:?}" ); let abort_key = attempt_key("mpa"); let abort_request = create_request(abort_key); let abort_session = store .begin_multipart(&abort_request) .await .expect("begin abort"); store .upload_part(&abort_session, 1, part_one.into()) .await .expect("abort part"); store.abort_multipart(abort_session).await.expect("abort"); } #[tokio::test] async fn unicode_key_exact_delete() { let Some(store) = store_from_env() else { return; }; let key = attempt_object_key( &unique_id("attuni"), &unique_id("objuni"), Some("한글/é/member"), ); let body = "정규화".as_bytes(); let stored = store .put_create_only(&create_request(key.clone()), body) .await .expect("unicode put"); assert_eq!(stored.content_sha256, hash_bytes(body)); store .delete_exact(&stored.identity) .await .expect("exact delete"); let missing = store.read_exact(&stored.identity).await; assert!( matches!(missing, Err(TargetError::NotFound)), "expected not found after delete, got {missing:?}" ); } #[tokio::test] async fn time_limited_exact_access_rejects_tamper_and_expiry() { let Some(store) = store_from_env() else { return; }; let body = b"presign-body"; let key = attempt_key("prs"); let sibling_key = attempt_key("sib"); let stored = store .put_create_only(&create_request(key.clone()), body.as_slice()) .await .expect("put"); store .put_create_only(&create_request(sibling_key.clone()), b"sibling".as_slice()) .await .expect("sibling put"); let access = store .presign_read(&stored.identity, Duration::from_secs(2)) .await .expect("presign"); let client = reqwest::Client::new(); let ok = client .get(&access.url) .send() .await .expect("presign get") .error_for_status() .expect("presign status") .bytes() .await .expect("presign body"); assert_eq!(ok.as_ref(), body); let tampered = access.url.replacen(&key, &sibling_key, 1); assert_ne!(tampered, access.url); let tamper_status = client .get(&tampered) .send() .await .expect("tampered get") .status() .as_u16(); assert!( matches!(tamper_status, 400 | 403), "tampered URL must not authorize sibling, got {tamper_status}" ); tokio::time::sleep(Duration::from_secs(3)).await; let expired_status = client .get(&access.url) .send() .await .expect("expired get") .status() .as_u16(); assert!( matches!(expired_status, 400 | 403), "expired URL must fail, got {expired_status}" ); } #[tokio::test] async fn version_addressed_and_stale_version_matrix() { let Some(store) = store_from_env() else { return; }; if !store .enable_bucket_versioning() .await .expect("enable versioning") { if require_env() { let provider = std::env::var("REMORA_TARGET_PROVIDER").unwrap_or_else(|_| "unknown".into()); if provider == "versitygw" { panic!( "versitygw must support bucket versioning for the target compatibility matrix" ); } } eprintln!( "target provider {} cannot enable versioning; skipping version matrix", std::env::var("REMORA_TARGET_PROVIDER").unwrap_or_else(|_| "unknown".into()) ); return; } let key = attempt_key("ver"); let first = store .put_create_only(&create_request(key.clone()), b"one".as_slice()) .await .expect("first version"); let first_version = first .identity .version_id .clone() .expect("version id after enable"); let second = store .put_overwrite(&create_request(key.clone()), b"two".as_slice()) .await .expect("second version"); let second_version = second .identity .version_id .clone() .expect("second version id"); assert_ne!(first_version, second_version); let first_identity = ObjectIdentity { target_store_id: store.config().target_store_id.clone(), bucket: store.config().bucket.clone(), key: key.clone(), version_id: Some(first_version), }; let second_identity = ObjectIdentity { target_store_id: store.config().target_store_id.clone(), bucket: store.config().bucket.clone(), key: key.clone(), version_id: Some(second_version), }; assert_eq!( store.read_exact(&first_identity).await.expect("v1"), b"one".as_slice() ); assert_eq!( store.read_exact(&second_identity).await.expect("v2"), b"two".as_slice() ); let versioned_access = store .presign_read(&first_identity, Duration::from_secs(60)) .await .expect("versioned presign"); let client = reqwest::Client::new(); let body = client .get(&versioned_access.url) .send() .await .expect("versioned get") .bytes() .await .expect("versioned body"); assert_eq!(body.as_ref(), b"one"); store .delete_exact(&first_identity) .await .expect("delete v1"); assert!(matches!( store.read_exact(&first_identity).await, Err(TargetError::NotFound) )); let stale_key = attempt_key("stv"); let keep = store .put_create_only(&create_request(stale_key.clone()), b"keep".as_slice()) .await .expect("stale put"); let version = keep.identity.version_id.clone().expect("stale version id"); let bogus = ObjectIdentity { target_store_id: store.config().target_store_id.clone(), bucket: store.config().bucket.clone(), key: stale_key, version_id: Some(format!("{version}-stale")), }; let result = store.read_exact(&bogus).await; assert!( matches!( result, Err(TargetError::NotFound | TargetError::Internal(_)) ), "stale version must fail closed: {result:?}" ); store .purge_versions_with_prefix(RESERVED_NAMESPACE_SEGMENT) .await .expect("purge versions"); }