Skip to main content

coven_storage/cloud/
test_utils.rs

1//! In-process CloudHome implementation for tests. Records every write keyed
2//! by cloud_key so tests can read back exactly what landed, and serves reads
3//! from the same map — enough to simulate two devices sharing a cloud bucket.
4//!
5//! Available under `#[cfg(test)]` in this crate itself and to the crates above
6//! it that enable the `test-utils` feature.
7
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::sync::Mutex;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15
16use super::{
17    BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudFileReadError, CloudHome,
18    CloudHomeError, CloudObjectVersion, CloudVersionedObject, ConditionalWriteOutcome,
19    ExactSlotStorage, PartSink,
20};
21use coven_protocol::objects::ObjectSlot;
22
23#[derive(Clone)]
24struct AppendPause {
25    call: usize,
26    reached: Arc<tokio::sync::Notify>,
27    release: Arc<tokio::sync::Notify>,
28}
29
30struct ExactStreamReadGuard {
31    inflight: Arc<AtomicUsize>,
32}
33
34struct InflightGuard {
35    inflight: Arc<AtomicUsize>,
36}
37
38#[derive(Clone)]
39struct ProbePause {
40    reached: Arc<tokio::sync::Notify>,
41    release: Arc<tokio::sync::Notify>,
42}
43
44#[derive(Clone)]
45struct MemoryObject {
46    bytes: Vec<u8>,
47    version: u64,
48}
49
50struct MemoryObjects {
51    values: HashMap<String, MemoryObject>,
52    next_version: u64,
53}
54
55impl MemoryObjects {
56    fn new() -> Self {
57        Self {
58            values: HashMap::new(),
59            next_version: 0,
60        }
61    }
62
63    fn insert(&mut self, key: String, bytes: Vec<u8>) -> Option<MemoryObject> {
64        self.next_version = self
65            .next_version
66            .checked_add(1)
67            .expect("in-memory cloud object version overflow");
68        self.values.insert(
69            key,
70            MemoryObject {
71                bytes,
72                version: self.next_version,
73            },
74        )
75    }
76
77    fn bytes(&self, key: &str) -> Option<Vec<u8>> {
78        self.values.get(key).map(|object| object.bytes.clone())
79    }
80}
81
82impl Drop for ExactStreamReadGuard {
83    fn drop(&mut self) {
84        self.inflight.fetch_sub(1, Ordering::SeqCst);
85    }
86}
87
88impl Drop for InflightGuard {
89    fn drop(&mut self) {
90        self.inflight.fetch_sub(1, Ordering::SeqCst);
91    }
92}
93
94/// In-memory CloudHome backed by a HashMap. `Clone` shares one backing store, so
95/// clones act as separate devices reading and writing the same cloud bucket, and
96/// a test can keep its own handle for direct at-rest assertions while each device
97/// owns an exact cloud-home clone.
98///
99/// Beyond the happy path it carries fault-injection knobs
100/// ([`arm_write_failures`](Self::arm_write_failures),
101/// [`fail_next_range_reads`](Self::fail_next_range_reads),
102/// [`remove`](Self::remove)) so a host test can drive upload-failure,
103/// read-retry, and missing-blob paths without a bespoke `CloudHome` impl. The
104/// arming state is shared across clones, like the backing store.
105#[derive(Clone)]
106pub struct InMemoryCloudHome {
107    provider_binding: coven_protocol::objects::ResolvedProviderBinding,
108    writes: Arc<Mutex<MemoryObjects>>,
109    exact_slot_allocations: Arc<AtomicUsize>,
110    exact_slot_allocation_delay_millis: Arc<AtomicU64>,
111    exact_slot_allocation_inflight: Arc<AtomicUsize>,
112    exact_slot_allocation_max_inflight: Arc<AtomicUsize>,
113    deletes: Arc<Mutex<Vec<String>>>,
114    fail_writes: Arc<AtomicBool>,
115    fail_next_range_reads: Arc<AtomicUsize>,
116    fail_next_exact_stream_reads: Arc<AtomicUsize>,
117    sort_listings: Arc<AtomicBool>,
118    exact_create_count: Arc<AtomicUsize>,
119    exact_creates: Arc<Mutex<Vec<ObjectSlot>>>,
120    fail_exact_create_before: Arc<AtomicUsize>,
121    fail_exact_create_after: Arc<AtomicUsize>,
122    lose_next_conditional_replace_response: Arc<AtomicBool>,
123    exact_create_pause: Arc<Mutex<Option<AppendPause>>>,
124    probe_pause: Arc<Mutex<Option<ProbePause>>>,
125    probe_failure: Arc<Mutex<Option<coven_protocol::objects::StorageBackendFailure>>>,
126    exact_full_read_count: Arc<AtomicUsize>,
127    exact_full_read_delay_millis: Arc<AtomicU64>,
128    exact_list_count: Arc<AtomicUsize>,
129    exact_listed_prefixes: Arc<Mutex<Vec<String>>>,
130    exact_full_read_inflight: Arc<AtomicUsize>,
131    exact_full_read_max_inflight: Arc<AtomicUsize>,
132    exact_stream_read_count: Arc<AtomicUsize>,
133    exact_reads: Arc<Mutex<Vec<ObjectSlot>>>,
134    exact_stream_read_inflight: Arc<AtomicUsize>,
135    exact_stream_read_max_inflight: Arc<AtomicUsize>,
136    exact_stream_read_barrier: Arc<Mutex<Option<Arc<tokio::sync::Barrier>>>>,
137    exact_stream_read_chunk_bytes: Arc<AtomicUsize>,
138    exact_stream_read_chunk_delay_millis: Arc<AtomicU64>,
139    exact_delete_count: Arc<AtomicUsize>,
140    /// Every ranged exact read this home has served, as `(start, end)` stored
141    /// offsets. What a test counts to say a read cost the bytes it asked for and
142    /// no more — a full read is counted separately, by `exact_full_read_count`
143    /// and `exact_stream_read_count`, so reintroducing a whole-object fetch
144    /// shows up as a full read rather than hiding inside the range total.
145    exact_range_reads: Arc<Mutex<Vec<(u64, u64)>>>,
146    fail_exact_delete_on: Arc<AtomicUsize>,
147    fail_exact_delete_of: Arc<Mutex<Option<TargetedDeleteFailure>>>,
148}
149
150/// Fail the `countdown`-th delete of an object whose key is in `keys`, counting
151/// only those deletes. Set by [`InMemoryCloudHome::fail_nth_exact_delete_of`].
152struct TargetedDeleteFailure {
153    keys: std::collections::HashSet<String>,
154    countdown: usize,
155    failure: TargetedDeleteFailureKind,
156}
157
158/// What the armed delete returns: a transport fault a later attempt may clear,
159/// or a provider refusal no attempt can.
160enum TargetedDeleteFailureKind {
161    Transport,
162    Permanent,
163}
164
165impl InMemoryCloudHome {
166    pub fn new() -> Self {
167        Self {
168            provider_binding: coven_protocol::objects::ResolvedProviderBinding {
169                store: coven_protocol::objects::StoreProviderBinding::S3 {
170                    endpoint: coven_protocol::objects::S3EndpointBinding::Custom {
171                        origin: "https://in-memory.invalid".to_string(),
172                    },
173                    region: "test".to_string(),
174                    bucket: "in-memory".to_string(),
175                    key_prefix: None,
176                },
177                device: coven_protocol::objects::ProviderDeviceBinding {
178                    principal: coven_protocol::objects::ProviderPrincipalId::CustomS3Credential {
179                        access_key_id_hash: coven_protocol::store_commit::ObjectHash::digest(
180                            b"coven.s3-access-key-id.v1\0in-memory",
181                        ),
182                    },
183                },
184            },
185            writes: Arc::new(Mutex::new(MemoryObjects::new())),
186            exact_slot_allocations: Arc::new(AtomicUsize::new(0)),
187            exact_slot_allocation_delay_millis: Arc::new(AtomicU64::new(0)),
188            exact_slot_allocation_inflight: Arc::new(AtomicUsize::new(0)),
189            exact_slot_allocation_max_inflight: Arc::new(AtomicUsize::new(0)),
190            deletes: Arc::new(Mutex::new(Vec::new())),
191            fail_writes: Arc::new(AtomicBool::new(false)),
192            fail_next_range_reads: Arc::new(AtomicUsize::new(0)),
193            fail_next_exact_stream_reads: Arc::new(AtomicUsize::new(0)),
194            sort_listings: Arc::new(AtomicBool::new(false)),
195            exact_create_count: Arc::new(AtomicUsize::new(0)),
196            exact_creates: Arc::new(Mutex::new(Vec::new())),
197            fail_exact_create_before: Arc::new(AtomicUsize::new(0)),
198            fail_exact_create_after: Arc::new(AtomicUsize::new(0)),
199            lose_next_conditional_replace_response: Arc::new(AtomicBool::new(false)),
200            exact_create_pause: Arc::new(Mutex::new(None)),
201            probe_pause: Arc::new(Mutex::new(None)),
202            probe_failure: Arc::new(Mutex::new(None)),
203            exact_full_read_count: Arc::new(AtomicUsize::new(0)),
204            exact_list_count: Arc::new(AtomicUsize::new(0)),
205            exact_listed_prefixes: Arc::new(Mutex::new(Vec::new())),
206            exact_full_read_delay_millis: Arc::new(AtomicU64::new(0)),
207            exact_full_read_inflight: Arc::new(AtomicUsize::new(0)),
208            exact_full_read_max_inflight: Arc::new(AtomicUsize::new(0)),
209            exact_stream_read_count: Arc::new(AtomicUsize::new(0)),
210            exact_reads: Arc::new(Mutex::new(Vec::new())),
211            exact_stream_read_inflight: Arc::new(AtomicUsize::new(0)),
212            exact_stream_read_max_inflight: Arc::new(AtomicUsize::new(0)),
213            exact_stream_read_barrier: Arc::new(Mutex::new(None)),
214            exact_stream_read_chunk_bytes: Arc::new(AtomicUsize::new(0)),
215            exact_stream_read_chunk_delay_millis: Arc::new(AtomicU64::new(0)),
216            exact_delete_count: Arc::new(AtomicUsize::new(0)),
217            exact_range_reads: Arc::new(Mutex::new(Vec::new())),
218            fail_exact_delete_on: Arc::new(AtomicUsize::new(0)),
219            fail_exact_delete_of: Arc::new(Mutex::new(None)),
220        }
221    }
222
223    pub fn with_provider_binding(
224        mut self,
225        binding: coven_protocol::objects::ResolvedProviderBinding,
226    ) -> Self {
227        binding
228            .validate()
229            .expect("in-memory provider binding must be valid");
230        self.provider_binding = binding;
231        self
232    }
233
234    /// Return `list` results in sorted key order instead of the backing map's
235    /// arbitrary order. A real bucket LIST has no defined order, so the pull's
236    /// cross-device apply order is arbitrary; a test that needs a fixed order (to
237    /// reproduce an order-dependent bug deterministically) arms this and picks the
238    /// order through its device ids.
239    pub fn sort_listings(&self) {
240        self.sort_listings.store(true, Ordering::SeqCst);
241    }
242
243    /// Arm every subsequent write (`put_object` and `open_multipart`) to fail
244    /// with a retryable transport error. A test can let a home's setup writes
245    /// land and then arm this before driving the path whose uploads must fail;
246    /// it stays armed for the store's lifetime.
247    pub fn arm_write_failures(&self) {
248        self.fail_writes.store(true, Ordering::SeqCst);
249    }
250
251    /// Let writes through again, so a test can drive the resume that follows a
252    /// failed one rather than only the failure.
253    pub fn clear_write_failures(&self) {
254        self.fail_writes.store(false, Ordering::SeqCst);
255    }
256
257    /// Make the next `n` `read_range` calls fail with a retryable transport
258    /// error before any serves bytes, to exercise a caller's read-retry path.
259    /// Each failed call consumes one; once `n` are spent, ranges serve
260    /// normally.
261    pub fn fail_next_range_reads(&self, n: usize) {
262        self.fail_next_range_reads.store(n, Ordering::SeqCst);
263    }
264
265    /// Make the next `n` exact streaming reads fail before creating their local
266    /// destination. This drives retry of operations such as snapshot bootstrap
267    /// without changing the durable object stored in the shared test home.
268    pub fn fail_next_exact_stream_reads(&self, n: usize) {
269        self.fail_next_exact_stream_reads.store(n, Ordering::SeqCst);
270    }
271
272    /// Serve exact streaming reads in chunks separated by `delay`. This lets a
273    /// test drive the real streaming progress path instead of calling an
274    /// observer directly.
275    pub fn stream_exact_reads_in_chunks(&self, chunk_bytes: usize, delay: std::time::Duration) {
276        assert!(chunk_bytes > 0, "stream chunk size must be nonzero");
277        self.exact_stream_read_chunk_bytes
278            .store(chunk_bytes, Ordering::SeqCst);
279        self.exact_stream_read_chunk_delay_millis.store(
280            u64::try_from(delay.as_millis()).expect("test stream delay fits u64 milliseconds"),
281            Ordering::SeqCst,
282        );
283    }
284
285    /// Reset the exact-create counter and fail before the selected call stores bytes.
286    pub fn fail_exact_create_before_call(&self, call: usize) {
287        assert!(call > 0, "create call numbers are 1-based");
288        self.exact_create_count.store(0, Ordering::SeqCst);
289        self.fail_exact_create_before.store(call, Ordering::SeqCst);
290    }
291
292    /// Reset the exact-create counter and lose the response after the selected create.
293    pub fn fail_exact_create_after_call(&self, call: usize) {
294        assert!(call > 0, "create call numbers are 1-based");
295        self.exact_create_count.store(0, Ordering::SeqCst);
296        self.fail_exact_create_after.store(call, Ordering::SeqCst);
297    }
298
299    /// Commit the next conditional replacement but return a transport error,
300    /// reproducing a response lost after the provider accepted the write.
301    pub fn lose_next_conditional_replace_response(&self) {
302        self.lose_next_conditional_replace_response
303            .store(true, Ordering::SeqCst);
304    }
305
306    /// Pause after the selected exact create is physically visible.
307    pub fn pause_after_exact_create_call(
308        &self,
309        call: usize,
310    ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
311        assert!(call > 0, "create call numbers are 1-based");
312        self.exact_create_count.store(0, Ordering::SeqCst);
313        let reached = Arc::new(tokio::sync::Notify::new());
314        let release = Arc::new(tokio::sync::Notify::new());
315        *self.exact_create_pause.lock().unwrap() = Some(AppendPause {
316            call,
317            reached: reached.clone(),
318            release: release.clone(),
319        });
320        (reached, release)
321    }
322
323    /// Pause the next reachability probe after it starts and before it succeeds.
324    pub fn pause_next_probe(&self) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
325        let reached = Arc::new(tokio::sync::Notify::new());
326        let release = Arc::new(tokio::sync::Notify::new());
327        *self.probe_pause.lock().unwrap() = Some(ProbePause {
328            reached: reached.clone(),
329            release: release.clone(),
330        });
331        (reached, release)
332    }
333
334    pub fn fail_next_probe_with(&self, failure: coven_protocol::objects::StorageBackendFailure) {
335        *self.probe_failure.lock().unwrap() = Some(failure);
336    }
337
338    pub fn exact_create_count(&self) -> usize {
339        self.exact_create_count.load(Ordering::SeqCst)
340    }
341
342    pub fn delay_exact_slot_allocations(&self, delay: std::time::Duration) {
343        self.exact_slot_allocation_delay_millis.store(
344            u64::try_from(delay.as_millis()).expect("test allocation delay fits u64 milliseconds"),
345            Ordering::SeqCst,
346        );
347        self.exact_slot_allocation_inflight
348            .store(0, Ordering::SeqCst);
349        self.exact_slot_allocation_max_inflight
350            .store(0, Ordering::SeqCst);
351    }
352
353    pub fn exact_slot_allocation_max_inflight(&self) -> usize {
354        self.exact_slot_allocation_max_inflight
355            .load(Ordering::SeqCst)
356    }
357
358    pub fn exact_creates(&self) -> Vec<ObjectSlot> {
359        self.exact_creates.lock().unwrap().clone()
360    }
361
362    pub fn clear_exact_creates(&self) {
363        self.exact_creates.lock().unwrap().clear();
364    }
365
366    pub fn exact_full_read_count(&self) -> usize {
367        self.exact_full_read_count.load(Ordering::SeqCst)
368    }
369
370    pub fn exact_list_count(&self) -> usize {
371        self.exact_list_count.load(Ordering::SeqCst)
372    }
373
374    pub fn exact_listed_prefixes(&self) -> Vec<String> {
375        self.exact_listed_prefixes.lock().unwrap().clone()
376    }
377
378    pub fn clear_exact_listings(&self) {
379        self.exact_list_count.store(0, Ordering::SeqCst);
380        self.exact_listed_prefixes.lock().unwrap().clear();
381    }
382
383    /// Delay every whole-object exact read and measure their concurrency.
384    pub fn delay_exact_full_reads(&self, delay: std::time::Duration) {
385        self.exact_full_read_delay_millis.store(
386            u64::try_from(delay.as_millis()).expect("test read delay fits u64 milliseconds"),
387            Ordering::SeqCst,
388        );
389        self.exact_full_read_inflight.store(0, Ordering::SeqCst);
390        self.exact_full_read_max_inflight.store(0, Ordering::SeqCst);
391    }
392
393    pub fn exact_full_read_max_inflight(&self) -> usize {
394        self.exact_full_read_max_inflight.load(Ordering::SeqCst)
395    }
396
397    /// Every ranged exact read served so far, as `(start, end)` stored offsets.
398    pub fn exact_range_reads(&self) -> Vec<(u64, u64)> {
399        self.exact_range_reads.lock().unwrap().clone()
400    }
401
402    /// Total stored bytes ranged reads have transferred.
403    pub fn exact_range_read_bytes(&self) -> u64 {
404        self.exact_range_reads
405            .lock()
406            .unwrap()
407            .iter()
408            .map(|(start, end)| end - start)
409            .sum()
410    }
411
412    pub fn clear_exact_range_reads(&self) {
413        self.exact_range_reads.lock().unwrap().clear();
414    }
415
416    pub fn exact_stream_read_count(&self) -> usize {
417        self.exact_stream_read_count.load(Ordering::SeqCst)
418    }
419
420    pub fn exact_reads(&self) -> Vec<ObjectSlot> {
421        self.exact_reads.lock().unwrap().clone()
422    }
423
424    pub fn clear_exact_reads(&self) {
425        self.exact_reads.lock().unwrap().clear();
426    }
427
428    pub fn arm_exact_stream_read_concurrency_probe(&self, width: usize) {
429        assert!(width > 0, "exact stream read probe width must be positive");
430        self.exact_stream_read_inflight.store(0, Ordering::SeqCst);
431        self.exact_stream_read_max_inflight
432            .store(0, Ordering::SeqCst);
433        *self.exact_stream_read_barrier.lock().unwrap() =
434            Some(Arc::new(tokio::sync::Barrier::new(width)));
435    }
436
437    pub fn exact_stream_read_max_inflight(&self) -> usize {
438        self.exact_stream_read_max_inflight.load(Ordering::SeqCst)
439    }
440
441    pub fn exact_delete_count(&self) -> usize {
442        self.exact_delete_count.load(Ordering::SeqCst)
443    }
444
445    pub fn fail_exact_delete_on_call(&self, call: usize) {
446        assert!(call > 0, "exact-delete call numbers are 1-based");
447        self.exact_delete_count.store(0, Ordering::SeqCst);
448        self.fail_exact_delete_on.store(call, Ordering::SeqCst);
449    }
450
451    /// Fail the `nth` (1-based) delete of an object among `slots`, counting only
452    /// deletes of those objects, then disarm. Unlike `fail_exact_delete_on_call`,
453    /// which counts every exact delete (probes, candidate cleanup), this counts
454    /// only the identities that matter, so "fail the 2nd package delete" lands
455    /// deterministically however many unrelated deletes interleave and whatever
456    /// order the two package deletes arrive in.
457    pub fn fail_nth_exact_delete_of(&self, slots: &[&ObjectSlot], nth: usize) {
458        self.arm_targeted_delete_failure(slots, nth, TargetedDeleteFailureKind::Transport);
459    }
460
461    /// Fail the `nth` (1-based) delete of an object among `slots` with a
462    /// configuration error — a provider refusal retrying cannot clear — then
463    /// disarm. The permanent counterpart to [`Self::fail_nth_exact_delete_of`],
464    /// and what drives the deterministic-failure paths that treat an operation
465    /// as decided rather than as worth another cycle.
466    pub fn fail_nth_exact_delete_of_permanently(&self, slots: &[&ObjectSlot], nth: usize) {
467        self.arm_targeted_delete_failure(slots, nth, TargetedDeleteFailureKind::Permanent);
468    }
469
470    fn arm_targeted_delete_failure(
471        &self,
472        slots: &[&ObjectSlot],
473        nth: usize,
474        failure: TargetedDeleteFailureKind,
475    ) {
476        assert!(nth > 0, "targeted delete ordinals are 1-based");
477        let keys = slots
478            .iter()
479            .map(|slot| Self::exact_storage_key(slot).expect("test exact slot is valid"))
480            .collect();
481        *self.fail_exact_delete_of.lock().unwrap() = Some(TargetedDeleteFailure {
482            keys,
483            countdown: nth,
484            failure,
485        });
486    }
487
488    /// Drop `key`'s bytes out of band — as if the object vanished from the
489    /// bucket on its own, without a `delete` (which `deletes_seen` would
490    /// record). Drives missing-blob read failures.
491    pub fn remove(&self, key: &str) {
492        self.writes.lock().unwrap().values.remove(key);
493    }
494
495    /// Snapshot of every key currently in the cloud. Useful for assertions
496    /// that don't want to hold the lock across an await.
497    pub fn keys(&self) -> Vec<String> {
498        self.writes.lock().unwrap().values.keys().cloned().collect()
499    }
500
501    /// Snapshot of the bytes at `key`, or `None` if absent. Cloned so the
502    /// caller can hold the result across `await` points without retaining
503    /// the internal lock.
504    pub fn get(&self, key: &str) -> Option<Vec<u8>> {
505        self.writes.lock().unwrap().bytes(key)
506    }
507
508    /// Number of objects stored. Cheap snapshot.
509    pub fn len(&self) -> usize {
510        self.writes.lock().unwrap().values.len()
511    }
512
513    /// Returns true if the store is empty.
514    pub fn is_empty(&self) -> bool {
515        self.writes.lock().unwrap().values.is_empty()
516    }
517
518    /// Snapshot of every delete that's been requested, in arrival order.
519    pub fn deletes_seen(&self) -> Vec<String> {
520        self.deletes.lock().unwrap().clone()
521    }
522
523    /// Insert caller-selected bytes at one exact logical slot.
524    pub fn insert_exact_object(&self, logical_key: &str, bytes: Vec<u8>) -> ObjectSlot {
525        let slot = ObjectSlot::logical(logical_key.to_string())
526            .map_err(CloudHomeError::from)
527            .expect("test logical key is non-empty");
528        self.writes
529            .lock()
530            .unwrap()
531            .insert(logical_key.to_string(), bytes);
532        slot
533    }
534
535    /// Snapshot the bytes stored at one exact slot, or `None` if absent.
536    pub fn stored_exact_bytes(&self, slot: &ObjectSlot) -> Option<Vec<u8>> {
537        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
538        self.writes.lock().unwrap().bytes(&key)
539    }
540
541    /// Whether one exact protocol object is currently stored.
542    pub fn contains_exact_object(&self, object: &coven_protocol::objects::ExactObjectRef) -> bool {
543        let key = Self::exact_storage_key(object.slot()).expect("test exact slot is valid");
544        self.writes.lock().unwrap().values.contains_key(&key)
545    }
546
547    /// Re-insert bytes at one exact slot, restoring an object dropped by
548    /// [`remove_exact_object`](Self::remove_exact_object).
549    pub fn restore_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
550        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
551        self.writes.lock().unwrap().insert(key, bytes);
552    }
553
554    /// Remove one exact object without recording a protocol delete.
555    pub fn remove_exact_object(&self, slot: &ObjectSlot) {
556        let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
557        self.writes.lock().unwrap().values.remove(&key);
558    }
559
560    /// The bytes currently stored at one exact slot, without counting a read.
561    pub fn stored_exact_object(&self, slot: &ObjectSlot) -> Vec<u8> {
562        self.writes
563            .lock()
564            .unwrap()
565            .bytes(&Self::exact_storage_key(slot).expect("test exact slot is valid"))
566            .expect("exact slot exists")
567    }
568
569    /// Replace bytes at one exact slot without changing its locator.
570    pub fn replace_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
571        let previous = self.writes.lock().unwrap().insert(
572            Self::exact_storage_key(slot).expect("test exact slot is valid"),
573            bytes,
574        );
575        assert!(previous.is_some(), "exact slot exists");
576    }
577
578    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
579        if self.fail_writes.load(Ordering::SeqCst) {
580            return Err(CloudHomeError::Transport(
581                "InMemoryCloudHome: armed write failure".into(),
582            ));
583        }
584        self.writes.lock().unwrap().insert(key.to_string(), data);
585        Ok(())
586    }
587
588    async fn open_multipart<'a>(
589        &'a self,
590        key: &str,
591        _total_len: u64,
592    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
593        // Gate multipart too, so `arm_write_failures` fails a write whatever its
594        // size — `write_blob` routes blobs above `multipart_threshold` here.
595        if self.fail_writes.load(Ordering::SeqCst) {
596            return Err(CloudHomeError::Transport(
597                "InMemoryCloudHome: armed write failure".into(),
598            ));
599        }
600        Ok(Box::new(InMemoryPartSink {
601            writes: self.writes.clone(),
602            key: key.to_string(),
603            buf: Vec::new(),
604        }))
605    }
606
607    fn multipart_threshold(&self) -> u64 {
608        // A small threshold so tests exercise the multipart driver path; the part
609        // size matches so a multi-part blob ticks progress several times.
610        super::PROGRESS_CHUNK_SIZE as u64
611    }
612    fn validate_exact_slot(slot: &ObjectSlot) -> Result<(), CloudHomeError> {
613        slot.validate()?;
614        Ok(())
615    }
616
617    fn exact_storage_key(slot: &ObjectSlot) -> Result<String, CloudHomeError> {
618        Self::validate_exact_slot(slot)?;
619        Ok(match slot.physical() {
620            coven_protocol::objects::PhysicalObjectLocator::LogicalKey => {
621                slot.logical_key().to_string()
622            }
623            coven_protocol::objects::PhysicalObjectLocator::Opaque(provider_id) => {
624                format!("{}#exact#{provider_id}", slot.logical_key())
625            }
626        })
627    }
628
629    /// The slot [`exact_storage_key`](Self::exact_storage_key) built this
630    /// bucket key from, so a listing recovers the locator the writer allocated
631    /// the same way a real provider's listing does.
632    fn exact_slot_from_storage_key(key: &str) -> Result<ObjectSlot, CloudHomeError> {
633        match key.split_once("#exact#") {
634            Some((logical_key, provider_id)) => {
635                ObjectSlot::opaque(logical_key.to_string(), provider_id.to_string())
636            }
637            None => ObjectSlot::logical(key.to_string()),
638        }
639        .map_err(CloudHomeError::from)
640    }
641
642    async fn create_at_slot(
643        &self,
644        upload: &super::ExactUpload<'_>,
645        control: &super::UploadControl,
646    ) -> Result<super::ExactCreateOutcome, CloudHomeError> {
647        if self.fail_writes.load(Ordering::SeqCst) {
648            return Err(CloudHomeError::Transport(
649                "InMemoryCloudHome: armed write failure".into(),
650            ));
651        }
652        let slot = upload.object().slot();
653        self.exact_creates.lock().unwrap().push(slot.clone());
654        let key = Self::exact_storage_key(slot)?;
655        let call = self.exact_create_count.fetch_add(1, Ordering::SeqCst) + 1;
656        if self.fail_exact_create_before.load(Ordering::SeqCst) == call {
657            self.fail_exact_create_before.store(0, Ordering::SeqCst);
658            return Err(CloudHomeError::Transport(format!(
659                "InMemoryCloudHome: forced failure before exact create call {call}"
660            )));
661        }
662        let bytes = upload.body().await?.collect().await?;
663        control.report(bytes.len() as u64);
664        {
665            let mut writes = self.writes.lock().unwrap();
666            if let Some(existing) = writes.values.get(&key) {
667                return if upload.object().verify(&existing.bytes).is_ok() {
668                    Ok(super::ExactCreateOutcome::AlreadyPresent)
669                } else {
670                    Err(CloudHomeError::SlotCollision(key))
671                };
672            }
673            writes.insert(key.clone(), bytes);
674        }
675        let pause = self
676            .exact_create_pause
677            .lock()
678            .unwrap()
679            .clone()
680            .filter(|pause| pause.call == call);
681        if let Some(pause) = pause {
682            pause.reached.notify_one();
683            pause.release.notified().await;
684            self.exact_create_pause.lock().unwrap().take();
685        }
686        if self.fail_exact_create_after.load(Ordering::SeqCst) == call {
687            self.fail_exact_create_after.store(0, Ordering::SeqCst);
688            let stored_matches = self
689                .writes
690                .lock()
691                .unwrap()
692                .values
693                .get(&key)
694                .is_some_and(|stored| upload.object().verify(&stored.bytes).is_ok());
695            if !stored_matches {
696                return Err(CloudHomeError::Transport(format!(
697                    "InMemoryCloudHome: forced failure after exact create call {call}"
698                )));
699            }
700        }
701        let stored_matches = self
702            .writes
703            .lock()
704            .unwrap()
705            .values
706            .get(&key)
707            .is_some_and(|stored| upload.object().verify(&stored.bytes).is_ok());
708        if !stored_matches {
709            return Err(CloudHomeError::SlotCollision(key));
710        }
711        Ok(super::ExactCreateOutcome::Created)
712    }
713
714    async fn read_exact(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
715        self.exact_full_read_count.fetch_add(1, Ordering::SeqCst);
716        let inflight = self.exact_full_read_inflight.fetch_add(1, Ordering::SeqCst) + 1;
717        self.exact_full_read_max_inflight
718            .fetch_max(inflight, Ordering::SeqCst);
719        let _guard = InflightGuard {
720            inflight: self.exact_full_read_inflight.clone(),
721        };
722        let delay = self.exact_full_read_delay_millis.load(Ordering::SeqCst);
723        if delay > 0 {
724            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
725        }
726        let key = Self::exact_storage_key(slot)?;
727        self.exact_reads.lock().unwrap().push(slot.clone());
728        self.writes
729            .lock()
730            .unwrap()
731            .bytes(&key)
732            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))
733    }
734
735    async fn read_exact_to_file(
736        &self,
737        slot: &ObjectSlot,
738        destination: &std::path::Path,
739        progress: super::DownloadProgress,
740    ) -> Result<(), CloudFileReadError> {
741        if self
742            .fail_next_exact_stream_reads
743            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
744            .is_ok()
745        {
746            return Err(CloudHomeError::Transport(
747                "InMemoryCloudHome: armed exact stream-read failure".into(),
748            )
749            .into());
750        }
751        self.exact_stream_read_count.fetch_add(1, Ordering::SeqCst);
752        self.exact_reads.lock().unwrap().push(slot.clone());
753        let inflight = self
754            .exact_stream_read_inflight
755            .fetch_add(1, Ordering::SeqCst)
756            + 1;
757        self.exact_stream_read_max_inflight
758            .fetch_max(inflight, Ordering::SeqCst);
759        let _guard = ExactStreamReadGuard {
760            inflight: self.exact_stream_read_inflight.clone(),
761        };
762        let barrier = self.exact_stream_read_barrier.lock().unwrap().clone();
763        if let Some(barrier) = barrier {
764            barrier.wait().await;
765        }
766        let key = Self::exact_storage_key(slot)?;
767        let bytes = self
768            .writes
769            .lock()
770            .unwrap()
771            .bytes(&key)
772            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
773        let chunk_bytes = self.exact_stream_read_chunk_bytes.load(Ordering::SeqCst);
774        let chunk_delay = std::time::Duration::from_millis(
775            self.exact_stream_read_chunk_delay_millis
776                .load(Ordering::SeqCst),
777        );
778        let chunks = if chunk_bytes == 0 {
779            vec![bytes::Bytes::from(bytes)]
780        } else {
781            bytes
782                .chunks(chunk_bytes)
783                .map(bytes::Bytes::copy_from_slice)
784                .collect::<Vec<_>>()
785        };
786        let stream = futures_util::StreamExt::then(
787            futures_util::stream::iter(chunks),
788            move |chunk| async move {
789                if !chunk_delay.is_zero() {
790                    tokio::time::sleep(chunk_delay).await;
791                }
792                Ok(chunk)
793            },
794        );
795        super::write_cloud_object_stream(destination, Box::pin(stream), progress).await?;
796        Ok(())
797    }
798
799    async fn delete_exact(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
800        let key = Self::exact_storage_key(slot)?;
801        let call = self.exact_delete_count.fetch_add(1, Ordering::SeqCst) + 1;
802        if self.fail_exact_delete_on.load(Ordering::SeqCst) == call {
803            self.fail_exact_delete_on.store(0, Ordering::SeqCst);
804            return Err(CloudHomeError::Transport(format!(
805                "InMemoryCloudHome: forced exact delete failure on call {call}"
806            )));
807        }
808        {
809            let mut targeted = self.fail_exact_delete_of.lock().unwrap();
810            if let Some(failure) = targeted.as_mut() {
811                if failure.keys.contains(&key) {
812                    failure.countdown -= 1;
813                    if failure.countdown == 0 {
814                        let error = match failure.failure {
815                            TargetedDeleteFailureKind::Transport => CloudHomeError::Transport(
816                                format!("InMemoryCloudHome: forced exact delete failure of {key}"),
817                            ),
818                            TargetedDeleteFailureKind::Permanent => CloudHomeError::Configuration(
819                                format!("InMemoryCloudHome: refused to delete {key}"),
820                            ),
821                        };
822                        *targeted = None;
823                        return Err(error);
824                    }
825                }
826            }
827        }
828        self.writes.lock().unwrap().values.remove(&key);
829        self.deletes.lock().unwrap().push(key);
830        Ok(())
831    }
832    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
833        self.writes
834            .lock()
835            .unwrap()
836            .bytes(key)
837            .ok_or_else(|| CloudHomeError::NotFound(key.to_string()))
838    }
839
840    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
841        // An armed range read fails before touching the store. `checked_sub`
842        // returns `None` at zero, so `fetch_update` only succeeds (and errors)
843        // while the countdown is positive.
844        if self
845            .fail_next_range_reads
846            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
847            .is_ok()
848        {
849            return Err(CloudHomeError::Transport(
850                "InMemoryCloudHome: armed range-read failure".into(),
851            ));
852        }
853        let data = self.read(key).await?;
854        let s = start as usize;
855        let e = (end as usize).min(data.len());
856        if s > data.len() {
857            return Err(CloudHomeError::NotFound(format!("range past end of {key}")));
858        }
859        Ok(data[s..e].to_vec())
860    }
861
862    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
863        let mut keys: Vec<String> = self
864            .writes
865            .lock()
866            .unwrap()
867            .values
868            .keys()
869            .filter(|k| k.starts_with(prefix))
870            .cloned()
871            .collect();
872        if self.sort_listings.load(Ordering::SeqCst) {
873            keys.sort();
874        }
875        Ok(keys)
876    }
877
878    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
879        self.writes.lock().unwrap().values.remove(key);
880        self.deletes.lock().unwrap().push(key.to_string());
881        Ok(())
882    }
883
884    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
885        Ok(self.writes.lock().unwrap().values.contains_key(key))
886    }
887
888    async fn set_access(
889        &self,
890        desired: super::CloudAccessState,
891    ) -> Result<super::CloudAccessOutcome, CloudHomeError> {
892        Ok(match desired {
893            super::CloudAccessState::Present { .. } => {
894                super::CloudAccessOutcome::Present(super::CloudHomeJoinInfo::S3 {
895                    bucket: "in-memory".to_string(),
896                    region: "test".to_string(),
897                    endpoint: Some("https://in-memory.invalid".to_string()),
898                    access_key: "in-memory".to_string(),
899                    secret_key: "in-memory".to_string(),
900                    key_prefix: None,
901                })
902            }
903            super::CloudAccessState::Absent { .. } => {
904                super::CloudAccessOutcome::Absent(super::RevokeOutcome::Unsupported)
905            }
906        })
907    }
908}
909
910impl Default for InMemoryCloudHome {
911    fn default() -> Self {
912        Self::new()
913    }
914}
915
916/// A [`PartSink`] for the in-memory backend: accumulate the streamed parts in
917/// order and store the assembled object on `finish`, so a multipart upload
918/// round-trips exactly like a single `put_object`.
919struct InMemoryPartSink {
920    writes: Arc<Mutex<MemoryObjects>>,
921    key: String,
922    buf: Vec<u8>,
923}
924
925#[async_trait]
926impl PartSink for InMemoryPartSink {
927    fn part_size(&self) -> usize {
928        super::PROGRESS_CHUNK_SIZE
929    }
930
931    async fn send_part(
932        &mut self,
933        part: Bytes,
934        _offset: u64,
935        _is_last: bool,
936        _control: &super::UploadControl,
937    ) -> Result<(), CloudHomeError> {
938        self.buf.extend_from_slice(&part);
939        Ok(())
940    }
941
942    async fn abort(&mut self) -> Result<(), CloudHomeError> {
943        Ok(())
944    }
945
946    async fn finish(self: Box<Self>) -> Result<(), CloudHomeError> {
947        self.writes.lock().unwrap().insert(self.key, self.buf);
948        Ok(())
949    }
950}
951
952#[async_trait]
953impl CloudHome for InMemoryCloudHome {
954    async fn probe(&self) -> Result<(), CloudHomeError> {
955        let pause = self.probe_pause.lock().unwrap().take();
956        if let Some(pause) = pause {
957            pause.reached.notify_one();
958            pause.release.notified().await;
959        }
960        if let Some(kind) = self.probe_failure.lock().unwrap().take() {
961            return Err(CloudHomeError::backend(
962                kind,
963                "probe in-memory cloud home",
964                std::io::Error::other("injected provider probe failure"),
965            ));
966        }
967        Ok(())
968    }
969
970    async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
971        InMemoryCloudHome::put_object(self, key, data).await
972    }
973
974    async fn open_multipart<'a>(
975        &'a self,
976        key: &str,
977        total_len: u64,
978    ) -> Result<BoxPartSink<'a>, CloudHomeError> {
979        InMemoryCloudHome::open_multipart(self, key, total_len).await
980    }
981
982    fn multipart_threshold(&self) -> u64 {
983        InMemoryCloudHome::multipart_threshold(self)
984    }
985
986    async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
987        InMemoryCloudHome::read(self, key).await
988    }
989
990    async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
991        InMemoryCloudHome::read_range(self, key, start, end).await
992    }
993
994    async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
995        InMemoryCloudHome::list(self, prefix).await
996    }
997
998    async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
999        InMemoryCloudHome::delete(self, key).await
1000    }
1001
1002    async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
1003        InMemoryCloudHome::exists(self, key).await
1004    }
1005
1006    async fn set_access(
1007        &self,
1008        desired: CloudAccessState,
1009    ) -> Result<CloudAccessOutcome, CloudHomeError> {
1010        InMemoryCloudHome::set_access(self, desired).await
1011    }
1012}
1013
1014#[async_trait]
1015impl ExactSlotStorage for InMemoryCloudHome {
1016    async fn provider_binding(
1017        &self,
1018    ) -> Result<coven_protocol::objects::ResolvedProviderBinding, CloudHomeError> {
1019        Ok(self.provider_binding.clone())
1020    }
1021
1022    async fn list_slots(&self, prefix: &str) -> Result<Vec<ObjectSlot>, CloudHomeError> {
1023        self.exact_list_count.fetch_add(1, Ordering::SeqCst);
1024        self.exact_listed_prefixes
1025            .lock()
1026            .unwrap()
1027            .push(prefix.to_string());
1028        let mut keys: Vec<String> = self
1029            .writes
1030            .lock()
1031            .unwrap()
1032            .values
1033            .keys()
1034            .filter(|key| key.starts_with(prefix))
1035            .cloned()
1036            .collect();
1037        keys.sort();
1038        keys.iter()
1039            .map(|key| Self::exact_slot_from_storage_key(key))
1040            .collect()
1041    }
1042
1043    async fn allocate_slot(&self, logical_key: &str) -> Result<ObjectSlot, CloudHomeError> {
1044        let inflight = self
1045            .exact_slot_allocation_inflight
1046            .fetch_add(1, Ordering::SeqCst)
1047            + 1;
1048        self.exact_slot_allocation_max_inflight
1049            .fetch_max(inflight, Ordering::SeqCst);
1050        let _guard = InflightGuard {
1051            inflight: self.exact_slot_allocation_inflight.clone(),
1052        };
1053        let delay = self
1054            .exact_slot_allocation_delay_millis
1055            .load(Ordering::SeqCst);
1056        if delay > 0 {
1057            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
1058        }
1059        match &self.provider_binding.store {
1060            coven_protocol::objects::StoreProviderBinding::GoogleDrive { .. } => {
1061                let allocation = self.exact_slot_allocations.fetch_add(1, Ordering::SeqCst) + 1;
1062                ObjectSlot::opaque(logical_key.to_string(), format!("in-memory-{allocation}"))
1063                    .map_err(CloudHomeError::from)
1064            }
1065            coven_protocol::objects::StoreProviderBinding::S3 { .. }
1066            | coven_protocol::objects::StoreProviderBinding::Dropbox { .. }
1067            | coven_protocol::objects::StoreProviderBinding::OneDrive { .. }
1068            | coven_protocol::objects::StoreProviderBinding::CloudKit { .. } => {
1069                ObjectSlot::logical(logical_key.to_string()).map_err(CloudHomeError::from)
1070            }
1071        }
1072    }
1073
1074    async fn create_at(
1075        &self,
1076        upload: &super::ExactUpload<'_>,
1077        control: &super::UploadControl,
1078    ) -> Result<super::ExactCreateOutcome, CloudHomeError> {
1079        InMemoryCloudHome::create_at_slot(self, upload, control).await
1080    }
1081
1082    async fn read_at(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
1083        InMemoryCloudHome::read_exact(self, slot).await
1084    }
1085
1086    async fn read_versioned_at(
1087        &self,
1088        slot: &ObjectSlot,
1089    ) -> Result<CloudVersionedObject, CloudHomeError> {
1090        let key = Self::exact_storage_key(slot)?;
1091        let writes = self.writes.lock().unwrap();
1092        let object = writes
1093            .values
1094            .get(&key)
1095            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
1096        Ok(CloudVersionedObject {
1097            bytes: object.bytes.clone(),
1098            version: CloudObjectVersion::from_provider(object.version.to_string())?,
1099        })
1100    }
1101
1102    async fn replace_at_if_version(
1103        &self,
1104        slot: &ObjectSlot,
1105        expected: &CloudObjectVersion,
1106        bytes: Vec<u8>,
1107    ) -> Result<ConditionalWriteOutcome, CloudHomeError> {
1108        if self.fail_writes.load(Ordering::SeqCst) {
1109            return Err(CloudHomeError::Transport(
1110                "InMemoryCloudHome: armed write failure".into(),
1111            ));
1112        }
1113        let key = Self::exact_storage_key(slot)?;
1114        let mut writes = self.writes.lock().unwrap();
1115        let Some(current) = writes.values.get(&key) else {
1116            return Err(CloudHomeError::NotFound(slot.logical_key().to_string()));
1117        };
1118        if current.version.to_string() != expected.as_provider() {
1119            return Ok(ConditionalWriteOutcome::VersionChanged);
1120        }
1121        writes.insert(key, bytes);
1122        let version = writes
1123            .values
1124            .get(&Self::exact_storage_key(slot)?)
1125            .expect("conditional replacement inserted the record")
1126            .version;
1127        if self
1128            .lose_next_conditional_replace_response
1129            .swap(false, Ordering::SeqCst)
1130        {
1131            return Err(CloudHomeError::Transport(
1132                "InMemoryCloudHome: conditional replacement response lost".to_string(),
1133            ));
1134        }
1135        Ok(ConditionalWriteOutcome::Replaced(
1136            CloudObjectVersion::from_provider(version.to_string())?,
1137        ))
1138    }
1139
1140    async fn read_range_at(
1141        &self,
1142        slot: &ObjectSlot,
1143        start: u64,
1144        end: u64,
1145    ) -> Result<Vec<u8>, CloudHomeError> {
1146        // Served straight out of the bucket rather than through `read_exact`, so
1147        // the full-read counter keeps meaning "something fetched a whole object"
1148        // and a ranged read never inflates it.
1149        let key = Self::exact_storage_key(slot)?;
1150        let bytes = self
1151            .writes
1152            .lock()
1153            .unwrap()
1154            .bytes(&key)
1155            .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
1156        // A range past the object's end is refused, not clamped: a short answer
1157        // to a range request is the provider ignoring it, which a caller must
1158        // see rather than splice.
1159        let window = bytes
1160            .get(start as usize..end as usize)
1161            .ok_or_else(|| {
1162                CloudHomeError::NotFound(format!(
1163                    "range {start}..{end} past the {} bytes of {}",
1164                    bytes.len(),
1165                    slot.logical_key()
1166                ))
1167            })?
1168            .to_vec();
1169        self.exact_range_reads.lock().unwrap().push((start, end));
1170        Ok(window)
1171    }
1172
1173    async fn read_at_to_file(
1174        &self,
1175        slot: &ObjectSlot,
1176        destination: &std::path::Path,
1177        progress: super::DownloadProgress,
1178    ) -> Result<(), CloudFileReadError> {
1179        InMemoryCloudHome::read_exact_to_file(self, slot, destination, progress).await
1180    }
1181
1182    async fn delete_at(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
1183        InMemoryCloudHome::delete_exact(self, slot).await
1184    }
1185}
1186
1187#[cfg(test)]
1188#[path = "test_utils_tests.rs"]
1189mod tests;
1190
1191/// An [`InMemoryCloudHome`] bound to a fixed Google Drive provider, for the
1192/// tests that need a cloud home but do not care which provider it claims to
1193/// be.
1194#[cfg(any(test, feature = "test-utils"))]
1195pub fn test_cloud_home() -> std::sync::Arc<InMemoryCloudHome> {
1196    test_cloud_home_with_binding(coven_protocol::objects::ResolvedProviderBinding {
1197        store: coven_protocol::objects::StoreProviderBinding::GoogleDrive {
1198            corpus: coven_protocol::objects::GoogleDriveCorpus::SharedDrive {
1199                drive_id: "test-drive".to_string(),
1200                folder_id: "test-folder".to_string(),
1201            },
1202        },
1203        device: coven_protocol::objects::ProviderDeviceBinding {
1204            principal: coven_protocol::objects::ProviderPrincipalId::GoogleDrive {
1205                permission_id: "test-permission".to_string(),
1206            },
1207        },
1208    })
1209}
1210
1211#[cfg(any(test, feature = "test-utils"))]
1212pub fn test_cloud_home_with_binding(
1213    binding: coven_protocol::objects::ResolvedProviderBinding,
1214) -> std::sync::Arc<InMemoryCloudHome> {
1215    std::sync::Arc::new(InMemoryCloudHome::new().with_provider_binding(binding))
1216}