1/*
  2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
  3 *
  4 * See the NOTICE file(s) distributed with this work for additional
  5 * information regarding copyright ownership.
  6 *
  7 * This program and the accompanying materials are made available under the
  8 * terms of the Apache License Version 2.0 which is available at
  9 * https://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * SPDX-License-Identifier: Apache-2.0
 12 */
 13
 14//! Integration tests for the local filesystem storage backend.
 15
 16use cda_interfaces::storage_api::{
 17    Collection as _, CollectionName, RandomAccessData as _, Storage, StorageError,
 18};
 19use cda_storage::LocalStorage;
 20
 21/// Create a fresh `LocalStorage` in a unique temp directory.
 22fn create_test_storage() -> (LocalStorage, tempfile::TempDir) {
 23    let dir = tempfile::tempdir().expect("Failed to create temp dir");
 24    let storage = LocalStorage::new(dir.path()).expect("Failed to create LocalStorage");
 25    (storage, dir)
 26}
 27
 28#[tokio::test]
 29async fn write_read_roundtrip() {
 30    let (storage, _dir) = create_test_storage();
 31    let name = CollectionName::DiagnosticDatabase;
 32    let collection = storage.get_or_create_collection(&name).await.unwrap();
 33
 34    let mut tx = storage.begin_transaction().unwrap();
 35    let mut data: &[u8] = b"hello world";
 36    collection
 37        .write(&mut tx, "greeting", &mut data)
 38        .await
 39        .unwrap();
 40    tx.commit().await.unwrap();
 41
 42    let handle = collection.read("greeting").await.unwrap();
 43    let mut buf = vec![0u8; 11];
 44    let n = handle.read_at(0, &mut buf).unwrap();
 45    assert_eq!(n, 11);
 46    assert_eq!(&buf, b"hello world");
 47}
 48
 49#[tokio::test]
 50async fn read_nonexistent_key_returns_not_found() {
 51    let (storage, _dir) = create_test_storage();
 52    let name = CollectionName::DiagnosticDatabase;
 53    let collection = storage.get_or_create_collection(&name).await.unwrap();
 54
 55    let result = collection.read("nonexistent").await;
 56    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
 57}
 58
 59#[tokio::test]
 60async fn delete_removes_key_after_commit() {
 61    let (storage, _dir) = create_test_storage();
 62    let name = CollectionName::DiagnosticDatabase;
 63    let collection = storage.get_or_create_collection(&name).await.unwrap();
 64
 65    // Write a key.
 66    let mut tx = storage.begin_transaction().unwrap();
 67    let mut data: &[u8] = b"to be deleted";
 68    collection
 69        .write(&mut tx, "doomed", &mut data)
 70        .await
 71        .unwrap();
 72    tx.commit().await.unwrap();
 73
 74    // Delete the key.
 75    let mut tx = storage.begin_transaction().unwrap();
 76    collection.delete(&mut tx, "doomed").await.unwrap();
 77    tx.commit().await.unwrap();
 78
 79    let result = collection.read("doomed").await;
 80    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
 81}
 82
 83#[tokio::test]
 84async fn delete_all_removes_all_keys() {
 85    let (storage, _dir) = create_test_storage();
 86    let name = CollectionName::DiagnosticDatabase;
 87    let collection = storage.get_or_create_collection(&name).await.unwrap();
 88
 89    // Write multiple keys.
 90    let mut tx = storage.begin_transaction().unwrap();
 91    let mut d1: &[u8] = b"one";
 92    let mut d2: &[u8] = b"two";
 93    collection.write(&mut tx, "key1", &mut d1).await.unwrap();
 94    collection.write(&mut tx, "key2", &mut d2).await.unwrap();
 95    tx.commit().await.unwrap();
 96
 97    // Delete all.
 98    let mut tx = storage.begin_transaction().unwrap();
 99    collection.delete_all(&mut tx).await.unwrap();
100    tx.commit().await.unwrap();
101
102    let is_empty = collection.is_empty().await.unwrap();
103    assert!(is_empty);
104}
105
106// Transaction semantics
107#[tokio::test]
108async fn rollback_discards_writes() {
109    let (storage, _dir) = create_test_storage();
110    let name = CollectionName::DiagnosticDatabase;
111    let collection = storage.get_or_create_collection(&name).await.unwrap();
112
113    let mut tx = storage.begin_transaction().unwrap();
114    let mut data: &[u8] = b"should not persist";
115    collection
116        .write(&mut tx, "ephemeral", &mut data)
117        .await
118        .unwrap();
119    tx.rollback();
120
121    let result = collection.read("ephemeral").await;
122    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
123}
124
125#[tokio::test]
126async fn drop_without_commit_is_implicit_rollback() {
127    let (storage, _dir) = create_test_storage();
128    let name = CollectionName::DiagnosticDatabase;
129    let collection = storage.get_or_create_collection(&name).await.unwrap();
130
131    {
132        let mut tx = storage.begin_transaction().unwrap();
133        let mut data: &[u8] = b"dropped";
134        collection.write(&mut tx, "ghost", &mut data).await.unwrap();
135        // tx is dropped here without commit.
136    }
137
138    let result = collection.read("ghost").await;
139    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
140}
141
142#[tokio::test]
143async fn no_read_your_writes() {
144    let (storage, _dir) = create_test_storage();
145    let name = CollectionName::DiagnosticDatabase;
146    let collection = storage.get_or_create_collection(&name).await.unwrap();
147
148    let mut tx = storage.begin_transaction().unwrap();
149    let mut data: &[u8] = b"uncommitted";
150    collection
151        .write(&mut tx, "pending", &mut data)
152        .await
153        .unwrap();
154
155    // Read should not see the uncommitted write.
156    let result = collection.read("pending").await;
157    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
158
159    tx.commit().await.unwrap();
160
161    // Now it should be visible.
162    let handle = collection.read("pending").await.unwrap();
163    assert_eq!(handle.data_size().unwrap(), 11);
164}
165
166#[tokio::test]
167async fn single_transaction_enforcement() {
168    let (storage, _dir) = create_test_storage();
169
170    let _tx = storage.begin_transaction().unwrap();
171    let result = storage.begin_transaction();
172    assert!(matches!(result, Err(StorageError::TransactionBusy)));
173}
174
175#[tokio::test]
176async fn can_begin_transaction_after_previous_commits() {
177    let (storage, _dir) = create_test_storage();
178
179    let tx = storage.begin_transaction().unwrap();
180    tx.commit().await.unwrap();
181
182    // Should be able to begin a new transaction now.
183    let tx2 = storage.begin_transaction().unwrap();
184    tx2.commit().await.unwrap();
185}
186
187#[tokio::test]
188async fn can_begin_transaction_after_previous_rollback() {
189    let (storage, _dir) = create_test_storage();
190
191    let tx = storage.begin_transaction().unwrap();
192    tx.rollback();
193
194    let tx2 = storage.begin_transaction().unwrap();
195    tx2.commit().await.unwrap();
196}
197
198#[tokio::test]
199async fn opening_missing_root_fails_without_creating_it() {
200    let dir = tempfile::tempdir().unwrap();
201    let root = dir.path().join("not_mounted");
202
203    let result = LocalStorage::new(&root);
204
205    assert!(
206        matches!(result, Err(StorageError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound)
207    );
208    assert!(!root.exists());
209}
210
211#[tokio::test]
212async fn writes_never_recreate_a_vanished_root() {
213    let dir = tempfile::tempdir().unwrap();
214    let root = dir.path().join("mounted");
215    std::fs::create_dir(&root).unwrap();
216    let storage = LocalStorage::new(&root).unwrap();
217
218    // E.g. the storage is unmounted while the CDA runs.
219    std::fs::remove_dir(&root).unwrap();
220
221    assert!(matches!(
222        storage.begin_transaction(),
223        Err(StorageError::Io(_))
224    ));
225    assert!(matches!(
226        storage
227            .get_or_create_collection(&CollectionName::DiagnosticDatabase)
228            .await,
229        Err(StorageError::Io(_))
230    ));
231    assert!(!root.exists());
232
233    // A failed start must not leave the storage busy.
234    std::fs::create_dir(&root).unwrap();
235    storage.begin_transaction().unwrap();
236}
237
238#[tokio::test]
239async fn case_insensitive_keys() {
240    let (storage, _dir) = create_test_storage();
241    let name = CollectionName::DiagnosticDatabase;
242    let collection = storage.get_or_create_collection(&name).await.unwrap();
243
244    let mut tx = storage.begin_transaction().unwrap();
245    let mut data: &[u8] = b"case test";
246    collection
247        .write(&mut tx, "FooBar", &mut data)
248        .await
249        .unwrap();
250    tx.commit().await.unwrap();
251
252    // Reading with different cases should find the same entry.
253    let handle = collection.read("foobar").await.unwrap();
254    assert_eq!(handle.data_size().unwrap(), 9);
255
256    let handle = collection.read("FOOBAR").await.unwrap();
257    assert_eq!(handle.data_size().unwrap(), 9);
258}
259
260// Collection management
261#[tokio::test]
262async fn get_nonexistent_collection_returns_not_found() {
263    let (storage, _dir) = create_test_storage();
264    let name = CollectionName::Custom("nonexistent".to_string());
265    let result = storage.get_collection(&name).await;
266    assert!(matches!(result, Err(StorageError::CollectionNotFound(_))));
267}
268
269#[tokio::test]
270async fn create_and_delete_collection() {
271    let (storage, _dir) = create_test_storage();
272    let name = CollectionName::Custom("temp_collection".to_string());
273
274    let mut tx = storage.begin_transaction().unwrap();
275    let collection = storage.create_collection(&mut tx, &name).await.unwrap();
276
277    // Write data into the new collection (before commit, the dir doesn't exist yet for reads
278    // but the write goes to staging).
279    let mut data: &[u8] = b"in new collection";
280    collection.write(&mut tx, "item", &mut data).await.unwrap();
281    tx.commit().await.unwrap();
282
283    // Should be readable now. Drop the handle before the next transaction to avoid deadlock.
284    let collection = storage.get_collection(&name).await.unwrap();
285    {
286        let handle = collection.read("item").await.unwrap();
287        assert_eq!(handle.data_size().unwrap(), 17);
288    }
289
290    // Delete the collection.
291    let mut tx = storage.begin_transaction().unwrap();
292    storage.delete_collection(&mut tx, &name).await.unwrap();
293    tx.commit().await.unwrap();
294
295    let result = storage.get_collection(&name).await;
296    assert!(matches!(result, Err(StorageError::CollectionNotFound(_))));
297}
298
299#[tokio::test]
300async fn copy_collection() {
301    let (storage, _dir) = create_test_storage();
302    let source = CollectionName::DiagnosticDatabase;
303    let dest = CollectionName::DiagnosticDatabaseBackup;
304
305    // Write some data to source.
306    let source_col = storage.get_or_create_collection(&source).await.unwrap();
307    let mut tx = storage.begin_transaction().unwrap();
308    let mut d1: &[u8] = b"alpha";
309    let mut d2: &[u8] = b"beta";
310    source_col.write(&mut tx, "a", &mut d1).await.unwrap();
311    source_col.write(&mut tx, "b", &mut d2).await.unwrap();
312    tx.commit().await.unwrap();
313
314    // Copy source to dest.
315    let mut tx = storage.begin_transaction().unwrap();
316    storage
317        .copy_collection(&mut tx, &source, &dest)
318        .await
319        .unwrap();
320    tx.commit().await.unwrap();
321
322    // Verify dest has the same data.
323    let dest_col = storage.get_collection(&dest).await.unwrap();
324    let handle_a = dest_col.read("a").await.unwrap();
325    let mut buf_a = vec![0u8; 5];
326    handle_a.read_at(0, &mut buf_a).unwrap();
327    assert_eq!(&buf_a, b"alpha");
328
329    let handle_b = dest_col.read("b").await.unwrap();
330    let mut buf_b = vec![0u8; 4];
331    handle_b.read_at(0, &mut buf_b).unwrap();
332    assert_eq!(&buf_b, b"beta");
333}
334
335#[tokio::test]
336async fn copy_collection_replaces_existing_dest() {
337    let (storage, _dir) = create_test_storage();
338    let source = CollectionName::DiagnosticDatabase;
339    let dest = CollectionName::DiagnosticDatabaseBackup;
340
341    // Write data to source.
342    let source_col = storage.get_or_create_collection(&source).await.unwrap();
343    let mut tx = storage.begin_transaction().unwrap();
344    let mut d1: &[u8] = b"alpha";
345    source_col.write(&mut tx, "a", &mut d1).await.unwrap();
346    tx.commit().await.unwrap();
347
348    // Write different data to dest (pre-existing content that should be replaced).
349    let dest_col = storage.get_or_create_collection(&dest).await.unwrap();
350    let mut tx = storage.begin_transaction().unwrap();
351    let mut d_old: &[u8] = b"old_stuff";
352    let mut d_extra: &[u8] = b"extra_file";
353    dest_col.write(&mut tx, "a", &mut d_old).await.unwrap();
354    dest_col
355        .write(&mut tx, "extra", &mut d_extra)
356        .await
357        .unwrap();
358    tx.commit().await.unwrap();
359
360    // Copy source -> dest. Dest should be fully replaced.
361    let mut tx = storage.begin_transaction().unwrap();
362    storage
363        .copy_collection(&mut tx, &source, &dest)
364        .await
365        .unwrap();
366    tx.commit().await.unwrap();
367
368    // Verify dest has exactly source's contents (key "a" with "alpha"), not the old data.
369    let dest_col = storage.get_collection(&dest).await.unwrap();
370    let handle_a = dest_col.read("a").await.unwrap();
371    let mut buf = vec![0u8; 5];
372    handle_a.read_at(0, &mut buf).unwrap();
373    assert_eq!(&buf, b"alpha");
374
375    // The "extra" key that was only in dest should be gone (replaced, not merged).
376    let result = dest_col.read("extra").await;
377    assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
378}
379
380// Metadata, list, len
381#[tokio::test]
382async fn metadata_returns_correct_size() {
383    let (storage, _dir) = create_test_storage();
384    let name = CollectionName::DiagnosticDatabase;
385    let collection = storage.get_or_create_collection(&name).await.unwrap();
386
387    let mut tx = storage.begin_transaction().unwrap();
388    let mut data: &[u8] = b"twelve chars";
389    collection.write(&mut tx, "sized", &mut data).await.unwrap();
390    tx.commit().await.unwrap();
391
392    let meta = collection.metadata("sized").await.unwrap();
393    assert_eq!(meta.name, "sized");
394    assert_eq!(meta.data_size, 12);
395}
396
397#[tokio::test]
398async fn list_and_len_reflect_committed_state() {
399    let (storage, _dir) = create_test_storage();
400    let name = CollectionName::DiagnosticDatabase;
401    let collection = storage.get_or_create_collection(&name).await.unwrap();
402
403    assert!(collection.is_empty().await.unwrap());
404    assert_eq!(collection.len().await.unwrap(), 0);
405
406    let mut tx = storage.begin_transaction().unwrap();
407    let mut d1: &[u8] = b"x";
408    let mut d2: &[u8] = b"y";
409    let mut d3: &[u8] = b"z";
410    collection.write(&mut tx, "one", &mut d1).await.unwrap();
411    collection.write(&mut tx, "two", &mut d2).await.unwrap();
412    collection.write(&mut tx, "three", &mut d3).await.unwrap();
413    tx.commit().await.unwrap();
414
415    assert_eq!(collection.len().await.unwrap(), 3);
416    assert!(!collection.is_empty().await.unwrap());
417
418    let mut keys = collection.list().await.unwrap();
419    keys.sort();
420    assert_eq!(keys, vec!["one", "three", "two"]);
421}
422
423// Random access data
424#[tokio::test]
425async fn random_access_read_at_offset() {
426    let (storage, _dir) = create_test_storage();
427    let name = CollectionName::DiagnosticDatabase;
428    let collection = storage.get_or_create_collection(&name).await.unwrap();
429
430    let mut tx = storage.begin_transaction().unwrap();
431    let mut data: &[u8] = b"0123456789ABCDEF";
432    collection.write(&mut tx, "hex", &mut data).await.unwrap();
433    tx.commit().await.unwrap();
434
435    let handle = collection.read("hex").await.unwrap();
436    assert_eq!(handle.data_size().unwrap(), 16);
437
438    // Read from offset 10.
439    let mut buf = vec![0u8; 6];
440    let n = handle.read_at(10, &mut buf).unwrap();
441    assert_eq!(n, 6);
442    assert_eq!(&buf, b"ABCDEF");
443
444    // Read from offset 14..only 2 bytes remaining.
445    let mut buf = vec![0u8; 6];
446    let n = handle.read_at(14, &mut buf).unwrap();
447    assert_eq!(n, 2);
448    assert_eq!(buf.get(..2).unwrap(), b"EF");
449}
450
451#[tokio::test]
452async fn overwrite_existing_key() {
453    let (storage, _dir) = create_test_storage();
454    let name = CollectionName::DiagnosticDatabase;
455    let collection = storage.get_or_create_collection(&name).await.unwrap();
456
457    // Write initial value.
458    let mut tx = storage.begin_transaction().unwrap();
459    let mut data: &[u8] = b"version 1";
460    collection
461        .write(&mut tx, "config", &mut data)
462        .await
463        .unwrap();
464    tx.commit().await.unwrap();
465
466    // Overwrite with new value.
467    let mut tx = storage.begin_transaction().unwrap();
468    let mut data: &[u8] = b"version 2";
469    collection
470        .write(&mut tx, "config", &mut data)
471        .await
472        .unwrap();
473    tx.commit().await.unwrap();
474
475    let handle = collection.read("config").await.unwrap();
476    let mut buf = vec![0u8; 9];
477    handle.read_at(0, &mut buf).unwrap();
478    assert_eq!(&buf, b"version 2");
479}
480
481/// Verifies the WAL on-disk format at the byte level: appends a mix of `CreateCollection`,
482/// `Write`, and `Delete` operations, then reads them back and checks that each operation is
483/// decoded correctly, in order, with its checksum intact and `truncated` reported as `false`.
484/// This underpins the crash-recovery guarantees, which depend on being able to faithfully replay
485/// exactly the operations that were durably recorded before a crash.
[docs]486/// [[ test~storage-wal-checksum-round-trip, WAL entries round-trip through checksum-verified encode/decode, test ]]
487// WAL checksum round-trip
488#[tokio::test]
489async fn wal_round_trip_with_checksum_verification() {
490    let dir = tempfile::tempdir().unwrap();
491    let wal_path = dir.path().join("test.wal");
492    cda_storage::wal::create_wal(&wal_path).unwrap();
493
494    let ops = vec![
495        cda_interfaces::storage_api::Operation::CreateCollection {
496            name: CollectionName::Custom("col_a".to_string()),
497        },
498        cda_interfaces::storage_api::Operation::Write {
499            collection: CollectionName::DiagnosticDatabase,
500            key: "my_key".to_string(),
501            staged_path: "/tmp/fake.tmp".to_string(),
502        },
503        cda_interfaces::storage_api::Operation::Delete {
504            collection: CollectionName::DiagnosticDatabase,
505            key: "old_key".to_string(),
506        },
507    ];
508
509    for op in &ops {
510        cda_storage::wal::append_operation(&wal_path, op).unwrap();
511    }
512
513    let wal_data = std::fs::read(&wal_path).unwrap();
514    let wal_result = cda_storage::wal::read_wal(&wal_data).unwrap();
515    assert!(!wal_result.truncated);
516    assert_eq!(wal_result.operations.len(), 3);
517
518    // Verify the operations match using .get() to satisfy clippy::indexing_slicing.
519    let read_op_0 = wal_result
520        .operations
521        .first()
522        .expect("Expected 3 operations");
523    assert!(matches!(
524        read_op_0,
525        cda_interfaces::storage_api::Operation::CreateCollection { name }
526        if name.as_str() == "col_a"
527    ));
528    let read_op_1 = wal_result.operations.get(1).expect("Expected 3 operations");
529    assert!(matches!(
530        read_op_1,
531        cda_interfaces::storage_api::Operation::Write { key, .. }
532        if key == "my_key"
533    ));
534    let read_op_2 = wal_result.operations.get(2).expect("Expected 3 operations");
535    assert!(matches!(
536        read_op_2,
537        cda_interfaces::storage_api::Operation::Delete { key, .. }
538        if key == "old_key"
539    ));
540}
541
542/// Writes two valid WAL entries, then truncates the file mid-way through the second entry to
543/// simulate a torn write. The reader must return exactly the first (fully valid) operation and
544/// report `truncated: true`, rather than erroring out or fabricating data for the incomplete
545/// entry. This is the detection mechanism that lets recovery safely distinguish "crash mid-write
546/// to the WAL itself" from genuine filesystem corruption.
[docs]547/// [[ test~storage-wal-truncation-detection, WAL reader detects and stops at a truncated entry, test ]]
548#[tokio::test]
549async fn wal_stops_at_truncated_entry() {
550    let dir = tempfile::tempdir().unwrap();
551    let wal_path = dir.path().join("test.wal");
552    cda_storage::wal::create_wal(&wal_path).unwrap();
553
554    // Write two valid entries.
555    cda_storage::wal::append_operation(
556        &wal_path,
557        &cda_interfaces::storage_api::Operation::CreateCollection {
558            name: CollectionName::Custom("first".to_string()),
559        },
560    )
561    .unwrap();
562    cda_storage::wal::append_operation(
563        &wal_path,
564        &cda_interfaces::storage_api::Operation::CreateCollection {
565            name: CollectionName::Custom("second".to_string()),
566        },
567    )
568    .unwrap();
569
570    // Truncate the file to corrupt the second entry (keep first + partial second).
571    let data = std::fs::read(&wal_path).unwrap();
572    // Write only 80% of the data to truncate the second entry.
573    let truncated_len = data.len() * 4 / 5;
574    std::fs::write(&wal_path, data.get(..truncated_len).unwrap()).unwrap();
575
576    let wal_data = std::fs::read(&wal_path).unwrap();
577    let wal_result = cda_storage::wal::read_wal(&wal_data).unwrap();
578    assert!(wal_result.truncated);
579    // Should only have the first valid entry.
580    assert_eq!(wal_result.operations.len(), 1);
581    let read_op_0 = wal_result.operations.first().expect("Expected 1 operation");
582    assert!(matches!(
583        read_op_0,
584        cda_interfaces::storage_api::Operation::CreateCollection { name }
585        if name.as_str() == "first"
586    ));
587}
588
589/// Regression test for a bug where `list()` returned a file's on-disk (possibly mixed-case)
590/// name verbatim, while `metadata()`/`read()`/`file_path()` normalize keys to lowercase before
591/// resolving them to a path. On a case-sensitive filesystem this mismatch caused every
592/// mixed-case file to appear in `list()` but then fail with `KeyNotFound` as soon as any
593/// per-key lookup (`metadata`, `read`, `file_path`) was attempted - exactly the "key vanished
594/// during iteration" symptom, even with no concurrent writer at all.
595///
596/// Simulates a file that ended up on disk with a mixed-case name outside of `Collection::write`
597/// (e.g. placed there during initial provisioning), then verifies that `list()`, `metadata()`,
598/// and `read()` are all consistent with each other.
599#[tokio::test]
600async fn list_normalizes_mixed_case_filenames_to_match_metadata_and_read() {
601    let (storage, dir) = create_test_storage();
602    let name = CollectionName::DiagnosticDatabase;
603
604    // Create the collection directory and place a mixed-case file directly on disk, bypassing
605    // `Collection::write` (which would have normalized the name to lowercase).
606    let collection_dir = dir.path().join("collections").join(name.as_str());
607    std::fs::create_dir_all(&collection_dir).unwrap();
608    std::fs::write(
609        collection_dir.join("FLXC1000_06.18.13.mdd"),
610        b"mixed case data",
611    )
612    .unwrap();
613
614    let collection = storage.get_or_create_collection(&name).await.unwrap();
615
616    // `list()` must return the normalized (lowercase) key, matching what `metadata`/`read`/
617    // `file_path` expect.
618    let keys = collection.list().await.unwrap();
619    assert_eq!(keys, vec!["flxc1000_06.18.13.mdd".to_string()]);
620
621    // Every key returned by `list()` must be resolvable via `metadata()` without a
622    // `KeyNotFound` error.
623    for key in &keys {
624        let meta = collection.metadata(key).await.unwrap();
625        assert_eq!(meta.data_size, "mixed case data".len() as u64);
626    }
627
628    // Every key returned by `list()` must also be resolvable via `read()`.
629    let Some(key) = keys.first() else {
630        panic!("expected one key");
631    };
632    let handle = collection.read(key).await.unwrap();
633    let mut buf = vec![0u8; "mixed case data".len()];
634    handle.read_at(0, &mut buf).unwrap();
635    assert_eq!(&buf, b"mixed case data");
636}