1use bytes::Bytes;
4
5use crate::{Condition, GetRequest, ObjectsError, ObjectsProvider, PutRequest, ReadTarget, wire};
6
7#[allow(
18 clippy::cognitive_complexity,
19 clippy::too_many_lines,
20 reason = "linear conformance walkthrough; each ensure() is a distinct provider-contract \
21 assertion, and splitting it would only move the same sequential checks behind \
22 indirection"
23)]
24pub async fn verify<P: ObjectsProvider + ?Sized>(
25 provider: &P,
26 namespace: &str,
27) -> Result<(), ObjectsError> {
28 let version_bucket = create(provider, &format!("{namespace}-versions"), "create-v").await?;
29 ensure(
30 provider
31 .create_bucket(format!("{namespace}-other"), Some("create-v".into()))
32 .await
33 == Err(ObjectsError::IdempotencyMismatch),
34 "bucket idempotency-key rebinding was accepted",
35 )?;
36 let first = put(
37 provider,
38 &version_bucket,
39 "dir/a",
40 b"one",
41 Some(Condition::IfAbsent),
42 Some("put-one"),
43 )
44 .await?;
45 ensure(
46 provider
47 .put(PutRequest {
48 bucket: version_bucket.clone(),
49 object_key: "dir/a".into(),
50 body: Bytes::from_static(b"wrong"),
51 metadata: metadata(),
52 condition: Some(Condition::IfAbsent),
53 idempotency_key: None,
54 })
55 .await
56 == Err(ObjectsError::PreconditionFailed),
57 "IfAbsent did not reject an existing current version",
58 )?;
59 let replay = put(
60 provider,
61 &version_bucket,
62 "dir/a",
63 b"one",
64 Some(Condition::IfAbsent),
65 Some("put-one"),
66 )
67 .await?;
68 ensure(replay == first, "idempotent put did not replay exactly")?;
69 ensure(
70 provider
71 .put(PutRequest {
72 bucket: version_bucket.clone(),
73 object_key: "dir/a".into(),
74 body: Bytes::from_static(b"different"),
75 metadata: metadata(),
76 condition: Some(Condition::IfAbsent),
77 idempotency_key: Some("put-one".into()),
78 })
79 .await
80 == Err(ObjectsError::IdempotencyMismatch),
81 "idempotency-key rebinding was accepted",
82 )?;
83 let second = put(provider, &version_bucket, "dir/a", b"two", None, None).await?;
84 ensure(
85 first.version_id != second.version_id,
86 "versions were not permanent",
87 )?;
88 ensure(
89 first.etag == format!("\"{}\"", blake3::hash(b"one").to_hex()),
90 "object validator did not bind exact bytes",
91 )?;
92 let ranged = provider
93 .get(GetRequest {
94 target: ReadTarget::Bucket(version_bucket.clone()),
95 object_key: "dir/a".into(),
96 version_id: Some(first.version_id.clone()),
97 range: Some((1, Some(2))),
98 if_match: Some(first.etag.clone()),
99 if_none_match: None,
100 maximum_bytes: 2,
101 })
102 .await?;
103 ensure(
104 ranged.body.as_ref() == b"ne",
105 "exact-version range read differed",
106 )?;
107 ensure(
108 provider
109 .get(GetRequest {
110 target: ReadTarget::Bucket(version_bucket.clone()),
111 object_key: "dir/a".into(),
112 version_id: Some(first.version_id.clone()),
113 range: None,
114 if_match: None,
115 if_none_match: None,
116 maximum_bytes: 2,
117 })
118 .await
119 == Err(ObjectsError::Capacity),
120 "read allocation bound was not enforced",
121 )?;
122 let grouped = provider
123 .list(
124 ReadTarget::Bucket(version_bucket.clone()),
125 String::new(),
126 Some("/".into()),
127 false,
128 10,
129 None,
130 )
131 .await?;
132 ensure(
133 grouped.common_prefixes == vec!["dir/"],
134 "delimiter listing did not group the exact prefix",
135 )?;
136 ensure(
137 !provider
138 .delete(
139 version_bucket.clone(),
140 "dir/a".into(),
141 Some("missing-version".into()),
142 None,
143 Some("delete-missing-version".into()),
144 )
145 .await?
146 .existed,
147 "unknown exact version was reported as deleted",
148 )?;
149 ensure(
150 provider
151 .delete_bucket(&version_bucket, Some("delete-nonempty".into()))
152 .await
153 == Err(ObjectsError::PreconditionFailed),
154 "nonempty bucket deletion was accepted",
155 )?;
156
157 let listing_bucket = create(provider, &format!("{namespace}-listing"), "create-l").await?;
158 put(provider, &listing_bucket, "a", b"a", None, None).await?;
159 put(provider, &listing_bucket, "b", b"b", None, None).await?;
160 let first_page = provider
161 .list(
162 ReadTarget::Bucket(listing_bucket.clone()),
163 String::new(),
164 None,
165 false,
166 1,
167 None,
168 )
169 .await?;
170 put(provider, &listing_bucket, "aa", b"later", None, None).await?;
171 let second_page = provider
172 .list(
173 ReadTarget::Bucket(listing_bucket),
174 String::new(),
175 None,
176 false,
177 1,
178 first_page.continuation,
179 )
180 .await?;
181 ensure(
182 first_page
183 .entries
184 .first()
185 .map(|entry| entry.object_key.as_str())
186 == Some("a")
187 && second_page
188 .entries
189 .first()
190 .map(|entry| entry.object_key.as_str())
191 == Some("b"),
192 "listing continuation did not retain its immutable view",
193 )?;
194
195 let source = create(provider, &format!("{namespace}-source"), "create-s").await?;
196 put(provider, &source, "a", b"before", None, None).await?;
197 let snapshot = provider
198 .snapshot(source.clone(), Some("snapshot".into()))
199 .await?
200 .snapshot
201 .ok_or(ObjectsError::Unavailable)?;
202 put(provider, &source, "a", b"after", None, None).await?;
203 let fork = provider
204 .fork(
205 ReadTarget::Snapshot(snapshot.clone()),
206 format!("{namespace}-fork"),
207 Some("fork".into()),
208 )
209 .await?
210 .bucket
211 .ok_or(ObjectsError::Unavailable)?;
212 let forked = get(provider, ReadTarget::Bucket(fork), "a").await?;
213 ensure(
214 forked.body.as_ref() == b"before",
215 "fork did not retain snapshot state",
216 )?;
217 ensure(
218 provider
219 .destroy_snapshot(snapshot, Some("destroy-snapshot".into()))
220 .await?,
221 "existing snapshot was not destroyed",
222 )?;
223
224 let multipart_bucket = create(provider, &format!("{namespace}-multipart"), "create-m").await?;
225 let upload = provider
226 .create_multipart(
227 multipart_bucket.clone(),
228 "large".into(),
229 metadata(),
230 None,
231 Some("create-upload".into()),
232 )
233 .await?;
234 ensure(
235 get(
236 provider,
237 ReadTarget::Bucket(multipart_bucket.clone()),
238 "large",
239 )
240 .await
241 == Err(ObjectsError::NotFound),
242 "multipart staging became visible before completion",
243 )?;
244 let part = provider
245 .upload_part(
246 multipart_bucket.clone(),
247 "large".into(),
248 upload.upload_id.clone(),
249 1,
250 Bytes::from_static(b"body"),
251 Some("upload-part".into()),
252 )
253 .await?;
254 ensure(
255 provider
256 .list_parts(
257 multipart_bucket.clone(),
258 "large".into(),
259 upload.upload_id.clone(),
260 )
261 .await?
262 == vec![part.clone()],
263 "multipart part listing differed",
264 )?;
265 let completed = provider
266 .complete_multipart(
267 multipart_bucket.clone(),
268 "large".into(),
269 upload.upload_id,
270 vec![part],
271 Some("complete-upload".into()),
272 )
273 .await?;
274 ensure(completed.size == 4, "multipart size differed")?;
275 ensure(
276 get(
277 provider,
278 ReadTarget::Bucket(multipart_bucket.clone()),
279 "large",
280 )
281 .await?
282 .body
283 .as_ref()
284 == b"body",
285 "multipart body differed",
286 )?;
287 let aborted = provider
288 .create_multipart(
289 multipart_bucket.clone(),
290 "aborted".into(),
291 metadata(),
292 None,
293 Some("create-aborted".into()),
294 )
295 .await?;
296 ensure(
297 provider
298 .abort_multipart(
299 multipart_bucket.clone(),
300 "aborted".into(),
301 aborted.upload_id.clone(),
302 Some("abort".into()),
303 )
304 .await?,
305 "existing multipart upload was not aborted",
306 )?;
307 ensure(
308 !provider
309 .abort_multipart(
310 multipart_bucket.clone(),
311 "aborted".into(),
312 aborted.upload_id,
313 Some("abort-again".into()),
314 )
315 .await?,
316 "absent multipart upload was reported as aborted",
317 )?;
318
319 let marker = provider
320 .delete(
321 multipart_bucket.clone(),
322 "large".into(),
323 None,
324 None,
325 Some("delete-marker".into()),
326 )
327 .await?;
328 ensure(
329 marker.existed && marker.marker.is_some_and(|version| version.delete_marker),
330 "delete marker was not published",
331 )?;
332 ensure(
333 provider
334 .delete(
335 multipart_bucket.clone(),
336 "large".into(),
337 Some(completed.version_id),
338 None,
339 Some("delete-version".into()),
340 )
341 .await?
342 .existed,
343 "exact retained version was not deleted",
344 )?;
345 let empty = create(provider, &format!("{namespace}-empty"), "create-e").await?;
346 ensure(
347 provider
348 .delete_bucket(&empty, Some("delete-empty".into()))
349 .await?,
350 "empty bucket was not deleted",
351 )?;
352 ensure(
353 provider
354 .delete_bucket(&empty, Some("delete-empty".into()))
355 .await?,
356 "bucket deletion did not replay exactly",
357 )?;
358 Ok(())
359}
360
361async fn create<P: ObjectsProvider + ?Sized>(
362 provider: &P,
363 name: &str,
364 idempotency_key: &str,
365) -> Result<wire::BucketRef, ObjectsError> {
366 let first = provider
367 .create_bucket(name.into(), Some(idempotency_key.into()))
368 .await?;
369 let replay = provider
370 .create_bucket(name.into(), Some(idempotency_key.into()))
371 .await?;
372 ensure(first == replay, "bucket creation did not replay exactly")?;
373 let reference = first.bucket.ok_or(ObjectsError::Unavailable)?;
374 ensure(
375 provider.head_bucket(&reference).await?.bucket == Some(reference.clone()),
376 "bucket head did not preserve exact identity",
377 )?;
378 Ok(reference)
379}
380
381async fn put<P: ObjectsProvider + ?Sized>(
382 provider: &P,
383 bucket: &wire::BucketRef,
384 object_key: &str,
385 body: &'static [u8],
386 condition: Option<Condition>,
387 idempotency_key: Option<&str>,
388) -> Result<wire::ObjectVersion, ObjectsError> {
389 provider
390 .put(PutRequest {
391 bucket: bucket.clone(),
392 object_key: object_key.into(),
393 body: Bytes::from_static(body),
394 metadata: metadata(),
395 condition,
396 idempotency_key: idempotency_key.map(str::to_owned),
397 })
398 .await
399}
400
401async fn get<P: ObjectsProvider + ?Sized>(
402 provider: &P,
403 target: ReadTarget,
404 object_key: &str,
405) -> Result<crate::BufferedObject, ObjectsError> {
406 provider
407 .get(GetRequest {
408 target,
409 object_key: object_key.into(),
410 version_id: None,
411 range: None,
412 if_match: None,
413 if_none_match: None,
414 maximum_bytes: 1024,
415 })
416 .await
417}
418
419fn metadata() -> wire::ObjectMetadata {
420 wire::ObjectMetadata {
421 content_type: "application/octet-stream".into(),
422 ..wire::ObjectMetadata::default()
423 }
424}
425
426fn ensure(condition: bool, message: &'static str) -> Result<(), ObjectsError> {
427 if condition {
428 Ok(())
429 } else {
430 Err(ObjectsError::Invalid(message))
431 }
432}