use super::*;
#[test]
fn list_object_versions_rejects_orphan_version_id_marker() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.set_bucket_versioning("alice", "archive-001", BucketVersioningStatus::Enabled)
.expect("versioning");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"payload".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put");
let error = runtime
.list_object_versions(
"alice",
ListObjectVersionsRequest {
bucket: "archive-001".to_string(),
version_id_marker: Some("v1".to_string()),
..ListObjectVersionsRequest::default()
},
)
.expect_err("orphan version marker must fail");
assert!(matches!(
error,
RuntimeError::InvalidListParameter { ref name, ref value }
if name == "version-id-marker" && value == "v1"
));
}
#[test]
fn list_object_versions_resumes_same_key_after_double_digit_version_ids() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.set_bucket_versioning("alice", "archive-001", BucketVersioningStatus::Enabled)
.expect("versioning");
for index in 1..=11 {
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: format!("payload-{index}").into_bytes(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put version");
}
let versions = runtime
.list_object_versions(
"alice",
ListObjectVersionsRequest {
bucket: "archive-001".to_string(),
prefix: Some("records/".to_string()),
key_marker: Some("records/a.txt".to_string()),
version_id_marker: Some("v9".to_string()),
..ListObjectVersionsRequest::default()
},
)
.expect("resumed versions");
let listed_versions = versions
.versions
.iter()
.map(|version| version.version_id.as_str())
.collect::<Vec<_>>();
assert_eq!(
listed_versions,
vec!["v8", "v7", "v6", "v5", "v4", "v3", "v2", "v1"]
);
}
#[test]
fn object_version_ids_are_independent_per_object() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
let first_a = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"a-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put a v1");
let first_b = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/b.txt".to_string(),
body: b"b-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put b v1");
let second_a = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"a-v2".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put a v2");
let delete_a = runtime
.delete_object("alice", "archive-001", "records/a.txt", false)
.expect("delete a");
assert_eq!(first_a.version_id, "v1");
assert_eq!(first_b.version_id, "v1");
assert_eq!(second_a.version_id, "v2");
assert_eq!(delete_a.delete_marker_version_id, "v3");
let snapshot = runtime.snapshot();
let bucket = snapshot.buckets.get("archive-001").expect("bucket");
let a_ordinals = bucket
.objects
.get("records/a.txt")
.expect("object a")
.versions
.iter()
.map(|version| version.local_ordinal)
.collect::<Vec<_>>();
let b_ordinals = bucket
.objects
.get("records/b.txt")
.expect("object b")
.versions
.iter()
.map(|version| version.local_ordinal)
.collect::<Vec<_>>();
assert_eq!(a_ordinals, vec![1, 2, 3]);
assert_eq!(b_ordinals, vec![1]);
assert_eq!(
bucket
.objects
.get("records/a.txt")
.expect("object a")
.versions
.iter()
.map(|version| version.version_id.as_str())
.collect::<Vec<_>>(),
vec!["v1", "v2", "v3"]
);
assert_eq!(
bucket
.objects
.get("records/b.txt")
.expect("object b")
.versions
.iter()
.map(|version| version.version_id.as_str())
.collect::<Vec<_>>(),
vec!["v1"]
);
}
#[test]
fn object_local_ordinals_cover_copy_and_multipart_write_paths() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.create_bucket("alice", "archive-002")
.expect("bucket");
let source = runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "source.txt".to_string(),
body: b"source-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("source put");
let copied = runtime
.copy_object(
"alice",
CopyObjectRequest {
source_bucket: "archive-001".to_string(),
source_key: "source.txt".to_string(),
source_version_id: Some(source.version_id.clone()),
destination_bucket: "archive-002".to_string(),
destination_key: "copy.txt".to_string(),
metadata_directive: MetadataDirective::Copy,
destination_encryption: None,
},
)
.expect("copy to new object");
let copied_again = runtime
.copy_object(
"alice",
CopyObjectRequest {
source_bucket: "archive-001".to_string(),
source_key: "source.txt".to_string(),
source_version_id: Some(source.version_id),
destination_bucket: "archive-002".to_string(),
destination_key: "copy.txt".to_string(),
metadata_directive: MetadataDirective::Copy,
destination_encryption: None,
},
)
.expect("copy to existing object");
let upload = runtime
.create_multipart_upload(
"alice",
CreateMultipartUploadRequest {
bucket: "archive-002".to_string(),
key: "large.bin".to_string(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("init large");
let part = runtime
.upload_part(
"alice",
UploadPartRequest {
bucket: "archive-002".to_string(),
key: "large.bin".to_string(),
upload_id: upload.upload_id.clone(),
part_number: 1,
body: b"large-v1".to_vec(),
},
)
.expect("part");
let completed = runtime
.complete_multipart_upload(
"alice",
CompleteMultipartUploadRequest {
bucket: "archive-002".to_string(),
key: "large.bin".to_string(),
upload_id: upload.upload_id,
parts: vec![CompletedPart {
part_number: 1,
etag: part.etag,
}],
},
)
.expect("complete large");
assert_eq!(copied.version_id, "v1");
assert_eq!(copied_again.version_id, "v2");
assert_eq!(completed.version_id, "v1");
let snapshot = runtime.snapshot();
let bucket = snapshot.buckets.get("archive-002").expect("bucket");
assert_eq!(
bucket.objects["copy.txt"]
.versions
.iter()
.map(|version| version.local_ordinal)
.collect::<Vec<_>>(),
vec![1, 2]
);
assert_eq!(bucket.objects["large.bin"].versions[0].local_ordinal, 1);
assert_eq!(bucket.objects["large.bin"].versions[0].version_id, "v1");
}
#[test]
fn restored_snapshots_reconstruct_missing_object_local_ordinals() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: b"a-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put a v1");
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/b.txt".to_string(),
body: b"b-v1".to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put b v1");
let mut snapshot = runtime.snapshot();
snapshot.schema_version = 15;
for object in snapshot
.buckets
.values_mut()
.flat_map(|bucket| bucket.objects.values_mut())
{
for version in &mut object.versions {
version.local_ordinal = 0;
}
}
let restored =
BucketWarden::restore(RuntimeConfig::development(), snapshot).expect("restore snapshot");
let restored_snapshot = restored.snapshot();
let bucket = restored_snapshot
.buckets
.get("archive-001")
.expect("bucket");
assert_eq!(bucket.objects["records/a.txt"].versions[0].local_ordinal, 1);
assert_eq!(bucket.objects["records/b.txt"].versions[0].local_ordinal, 1);
assert_eq!(bucket.objects["records/a.txt"].versions[0].version_id, "v1");
assert_eq!(bucket.objects["records/b.txt"].versions[0].version_id, "v1");
}
#[test]
fn restored_snapshots_recover_mixed_missing_ordinals_without_duplicates() {
let mut runtime = runtime();
runtime
.create_bucket("alice", "archive-001")
.expect("bucket");
for body in [b"a-v1".as_slice(), b"a-v2".as_slice(), b"a-v3".as_slice()] {
runtime
.put_object(
"alice",
PutObjectRequest {
bucket: "archive-001".to_string(),
key: "records/a.txt".to_string(),
body: body.to_vec(),
metadata: ObjectMetadata::default(),
},
ObjectLock::none(),
)
.expect("put");
}
let mut snapshot = runtime.snapshot();
snapshot.schema_version = 15;
let versions = &mut snapshot
.buckets
.get_mut("archive-001")
.expect("bucket")
.objects
.get_mut("records/a.txt")
.expect("object")
.versions;
versions[0].local_ordinal = 1;
versions[0].version_id = "v10".to_string();
versions[1].local_ordinal = 0;
versions[1].version_id = "v11".to_string();
versions[2].local_ordinal = 3;
versions[2].version_id = "v12".to_string();
let restored =
BucketWarden::restore(RuntimeConfig::development(), snapshot).expect("restore snapshot");
let restored_snapshot = restored.snapshot();
let versions = &restored_snapshot.buckets["archive-001"].objects["records/a.txt"].versions;
assert_eq!(
versions
.iter()
.map(|version| version.local_ordinal)
.collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert_eq!(
versions
.iter()
.map(|version| version.version_id.as_str())
.collect::<Vec<_>>(),
vec!["v1", "v2", "v3"]
);
}