Skip to main content

plexus_auth_core/tenant/
storage.rs

1//! `Tenanted<S>`, `Scoped<'a, S>`, and the sealed `TenantScopedStore` marker
2//! trait — the structural foundation for tenant-scoped storage access.
3//!
4//! Per AUTHZ-0 principle 1 ("trust is structural, not procedural"), an
5//! activation must not be able to hold a bare storage handle and call its
6//! query API directly. The wrapper introduced here is the load-bearing
7//! enforcement: an activation receives a [`Tenanted<S>`], the inner store
8//! is unreachable, and the only way to call any storage method is through
9//! a tenant-tagged [`Scoped<'a, S>`] borrow obtained by
10//! [`Tenanted::scoped`].
11//!
12//! See `plans/AUTHZ/AUTHZ-DATA-1-WRAPPER.md` for the contract and
13//! `plans/AUTHZ/AUTHZ-DATA-S01-output.md` §3 for the design rationale
14//! (wrapper-plus-trait, option a with elements of b and c).
15//!
16//! # Sealing summary
17//!
18//! | Protection                       | Mechanism                                                |
19//! |----------------------------------|----------------------------------------------------------|
20//! | Inner store unreachable          | `Tenanted::inner` is module-private                      |
21//! | No fabrication of `Tenanted`     | `Tenanted::new_sealed` is `pub(crate)`                   |
22//! | Marker trait cannot be implemented externally | `TenantScopedStore: seal::SealedStore` (private super) |
23//! | `TenantBoundary` proof unforgeable | `TenantBoundary::new_sealed` is `pub(crate)`           |
24//! | No accidental `Default`          | Not derived on any sealed type                           |
25//! | No leaky `Deserialize`           | Not derived on any sealed type                           |
26//!
27//! # The three structural compile-fails (per ticket §"Failing examples")
28//!
29//! ## 1. Reaching for the inner store directly
30//!
31//! ```compile_fail
32//! use plexus_auth_core::tenant::storage::Tenanted;
33//! fn leak<S>(t: &Tenanted<S>)
34//! where
35//!     S: plexus_auth_core::TenantScopedStore,
36//! {
37//!     let _ = &t.inner;
38//! }
39//! ```
40//!
41//! Diagnostic: `field 'inner' of struct 'Tenanted' is private` (E0616).
42//!
43//! ## 2. Constructing a `Tenant` from a literal inside an activation
44//!
45//! ```compile_fail
46//! use plexus_auth_core::Tenant;
47//! let _ = Tenant::try_new("victim-tenant");
48//! ```
49//!
50//! Diagnostic: `associated function 'try_new' is private` (E0624).
51//!
52//! ## 3. Implementing `TenantScopedStore` for a sibling-crate type without
53//!    the sealed super-trait
54//!
55//! ```compile_fail
56//! struct MyStore;
57//! impl plexus_auth_core::TenantScopedStore for MyStore {
58//!     type Error = std::io::Error;
59//! }
60//! ```
61//!
62//! Diagnostic: `the trait bound 'MyStore: SealedStore' is not satisfied`
63//! (E0277), pointing to the private `seal::SealedStore` super-trait.
64//!
65//! # Canonical happy path (passing doc test)
66//!
67//! The doc test below demonstrates the end-to-end pattern using the
68//! framework-supplied [`reference::InMemoryKvStore`] (a pre-sealed
69//! reference store kept in this crate so the canonical pattern is
70//! exercise-able from a doc test). In a real activation, the type
71//! satisfying [`TenantScopedStore`] is the activation's own storage
72//! handle, the impl is generated by `AUTHZ-DATA-2-MACRO`, and domain
73//! methods are added by a trait implemented on
74//! [`Scoped<'_, MyStore>`] from inside the activation crate (Rust's
75//! orphan rule forbids inherent impls on a foreign type — see the
76//! run-notes for the design implication).
77//!
78//! ```
79//! use plexus_auth_core::tenant::storage::{
80//!     reference::InMemoryKvStore, Scoped, TenantScopedStore, Tenanted,
81//! };
82//! use plexus_auth_core::Tenant;
83//!
84//! // 1. The framework hands the activation a `Tenanted<S>`.
85//! //    Here we mint one via the framework-blessed reference store.
86//! let store = InMemoryKvStore::new();
87//! store.put_for_doc_test("acme", "widget-1", b"sprocket".to_vec());
88//! store.put_for_doc_test("acme", "widget-2", b"flange".to_vec());
89//! store.put_for_doc_test("beta", "widget-1", b"do-not-leak".to_vec());
90//! let tenanted: Tenanted<InMemoryKvStore> =
91//!     plexus_auth_core::tenant::storage::__doctest_blessed_tenanted(store);
92//!
93//! // 2. The framework hands the activation a `&Tenant` extension.
94//! let tenant: Tenant =
95//!     plexus_auth_core::tenant::storage::__doctest_blessed_tenant("acme");
96//!
97//! // 3. The activation calls `.scoped(tenant)` and invokes domain
98//! //    methods on the borrow. For the reference store, those methods
99//! //    are pre-defined on `Scoped<'_, InMemoryKvStore>`.
100//! let scoped: Scoped<'_, InMemoryKvStore> = tenanted.scoped(&tenant);
101//! let names = scoped.list_keys();
102//! assert_eq!(names.len(), 2);
103//! assert!(names.contains(&"widget-1".to_string()));
104//! assert!(names.contains(&"widget-2".to_string()));
105//! ```
106
107use crate::tenant::types::Tenant;
108
109// ---------------------------------------------------------------------------
110// Sealing module — private super-trait pattern.
111//
112// `SealedStore` lives in a `pub(crate)` module. The trait itself is `pub`
113// inside the module so framework-internal types can implement it via the
114// macro `seal_store_impl!`, but the path `seal::SealedStore` is
115// `pub(crate)` overall — third-party crates cannot name it and therefore
116// cannot satisfy the super-trait bound on `TenantScopedStore`.
117//
118// AUTHZ-DATA-1-WRAPPER ticket §"`TenantScopedStore` marker trait", row
119// "Sealing", names this private re-export pattern.
120// ---------------------------------------------------------------------------
121pub(crate) mod seal {
122    /// Private super-trait that seals [`super::TenantScopedStore`].
123    ///
124    /// Reachable only from inside `plexus-auth-core`. Third-party crates
125    /// cannot name this path and therefore cannot satisfy the
126    /// super-trait bound on `TenantScopedStore`.
127    pub trait SealedStore {}
128}
129
130/// Crate-private macro that emits an empty `SealedStore` impl for a type.
131///
132/// Friend modules inside `plexus-auth-core` use this when introducing a
133/// new `TenantScopedStore` candidate type. Outside crates cannot use it
134/// because [`seal::SealedStore`] is unreachable.
135///
136/// Macros that are referenced via `$crate::tenant::storage::seal::...`
137/// do not need an explicit `use seal_store_impl` import inside
138/// sub-modules of this crate; rustc resolves the macro by name at the
139/// invocation site within the same crate.
140macro_rules! seal_store_impl {
141    ($t:ty) => {
142        impl $crate::tenant::storage::seal::SealedStore for $t {}
143    };
144}
145
146// ---------------------------------------------------------------------------
147// TenantBoundary — zero-sized witness that a tenant boundary was crossed.
148// ---------------------------------------------------------------------------
149
150/// Zero-sized witness that a tenant boundary was crossed structurally.
151///
152/// A `TenantBoundary` value exists if and only if it was minted by
153/// `plexus-auth-core` — its constructor is `pub(crate)`. The framework
154/// hands a `TenantBoundary` into every [`Tenanted<S>`] at construction;
155/// the value rides along on every [`Scoped<'a, S>`] obtained from that
156/// wrapper. Downstream audit / observability layers can accept a
157/// `TenantBoundary` argument to attest "this code path is structurally
158/// downstream of a tenant scoping" without re-deriving the fact.
159///
160/// # Sealing
161///
162/// - **No fabrication.** `new_sealed` is `pub(crate)`.
163/// - **No backdoor.** No public constructor, no public field, no
164///   `From`/`Into`, no `Default`.
165/// - **Copy-able.** `Copy` is derived so a `Scoped::boundary()` accessor
166///   can return a value without invalidating the parent borrow's
167///   witness.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub struct TenantBoundary {
170    /// Module-private marker field. Prevents struct-literal fabrication
171    /// from outside `plexus-auth-core` even if a future change widens
172    /// the constructor visibility by accident.
173    _seal: (),
174}
175
176impl TenantBoundary {
177    /// Mint a `TenantBoundary`. Crate-private — only the framework's
178    /// tenant-scoping path constructs one.
179    pub(crate) const fn new_sealed() -> Self {
180        Self { _seal: () }
181    }
182}
183
184// ---------------------------------------------------------------------------
185// TenantScopedStore — the public marker trait.
186// ---------------------------------------------------------------------------
187
188/// Sealed capability marker for types that can be wrapped in a
189/// [`Tenanted<S>`].
190///
191/// Implementing this trait is a structural declaration: "this storage
192/// handle's domain methods should be defined on [`Scoped<'_, Self>`], not
193/// on `Self` directly." The framework wraps the handle in [`Tenanted<S>`]
194/// at activation construction; the activation reaches the methods only
195/// through [`Tenanted::scoped`].
196///
197/// # Bounds
198///
199/// - `Send + Sync + 'static`: the wrapper crosses async boundaries and
200///   is shared across concurrent requests.
201/// - `seal::SealedStore`: the private super-trait that seals this trait.
202///   Third-party crates cannot name `seal::SealedStore` and therefore
203///   cannot implement `TenantScopedStore`.
204///
205/// # Associated `Error`
206///
207/// The framework standardizes on a single error type per store so that
208/// activation domain methods on [`Scoped<'a, S>`] can name a single
209/// failure type. `Error: std::error::Error + Send + Sync + 'static`
210/// keeps it compatible with `anyhow::Error`, `tracing` formatting, and
211/// the dispatch layer's audit envelope.
212///
213/// # No required methods
214///
215/// This trait carries no methods; it is a capability marker. Activation
216/// authors will, in `AUTHZ-DATA-2-MACRO` and per-activation tickets,
217/// define a domain trait inside their own crate and implement that trait
218/// on [`Scoped<'a, MyStore>`]. (Rust's orphan rule for inherent impls
219/// forbids `impl Scoped<'_, MyStore>` from outside this crate; the
220/// activation pattern uses a trait + impl, generated by the framework
221/// macro.)
222pub trait TenantScopedStore: seal::SealedStore + Send + Sync + 'static {
223    /// The error type returned by domain methods defined on
224    /// [`Scoped<'a, Self>`].
225    type Error: std::error::Error + Send + Sync + 'static;
226}
227
228// ---------------------------------------------------------------------------
229// Tenanted<S> — the wrapper.
230// ---------------------------------------------------------------------------
231
232/// A tenant-scoped wrapper around a storage handle.
233///
234/// `Tenanted<S>` is the only legal carrier for a storage handle inside an
235/// activation. The inner store is module-private; the constructor is
236/// crate-private; the only public access path is [`scoped`](Self::scoped),
237/// which requires a `&Tenant`.
238///
239/// # Construction
240///
241/// Activation code never constructs a `Tenanted<S>` directly. The
242/// framework's hub-builder layer (a follow-up ticket) accepts a bare `S`
243/// and hands a `Tenanted<S>` to the activation at startup. Inside
244/// `plexus-auth-core`, the crate-private `new_sealed` is the only
245/// constructor.
246///
247/// # Sealing
248///
249/// - **No fabrication.** Constructor is `pub(crate)`.
250/// - **No inner reach.** The `inner` field is module-private; activation
251///   code that writes `self.store.inner.method()` fails to compile.
252/// - **No `Default`.** Not derived; a default-constructed wrapper would
253///   widen the access boundary silently.
254/// - **No `Deserialize`.** Not derived; the wrapper is not transport.
255pub struct Tenanted<S: TenantScopedStore> {
256    /// The wrapped storage handle. Module-private — activation code in
257    /// any other module cannot name this field, so
258    /// `self.store.inner.foo()` is a compile error.
259    inner: S,
260    /// Structural witness that a tenant boundary exists at this site.
261    boundary: TenantBoundary,
262}
263
264impl<S: TenantScopedStore> Tenanted<S> {
265    /// Mint a `Tenanted<S>` from a bare storage handle.
266    ///
267    /// Crate-private — only `plexus-auth-core` (and the framework
268    /// hub-builder layer that lives a few crates downstream) constructs
269    /// this. Activation code receives a `Tenanted<S>` from the
270    /// framework; it does not construct one.
271    pub(crate) fn new_sealed(inner: S) -> Self {
272        Self {
273            inner,
274            boundary: TenantBoundary::new_sealed(),
275        }
276    }
277
278    /// Bind the wrapper to a tenant, returning a [`Scoped<'a, S>`]
279    /// borrow.
280    ///
281    /// The returned borrow's lifetime is the shorter of the wrapper's
282    /// borrow and the tenant's borrow. The activation's domain methods,
283    /// defined on `Scoped<'_, S>` via a trait (because Rust's orphan
284    /// rule forbids inherent impls on a foreign type), read both
285    /// `self.store()` and `self.tenant()` to bind the active tenant
286    /// into every query.
287    pub fn scoped<'a>(&'a self, tenant: &'a Tenant) -> Scoped<'a, S> {
288        Scoped {
289            inner: &self.inner,
290            tenant,
291            boundary: self.boundary,
292        }
293    }
294}
295
296impl<S: TenantScopedStore + std::fmt::Debug> std::fmt::Debug for Tenanted<S> {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        // Intentionally elide the inner store: the type is sealed, and
299        // Debug output that exposed inner state would be a leak surface
300        // for audit logs that pretty-print extension values.
301        f.debug_struct("Tenanted")
302            .field("inner", &"<sealed>")
303            .finish()
304    }
305}
306
307// ---------------------------------------------------------------------------
308// Scoped<'a, S> — the tenant-tagged borrow.
309// ---------------------------------------------------------------------------
310
311/// A tenant-tagged borrow of a storage handle.
312///
313/// `Scoped<'a, S>` is produced by [`Tenanted::scoped`]. It bundles a
314/// borrow of the inner store with a borrow of the active tenant. The
315/// activation defines its domain methods via a trait implemented on
316/// `Scoped<'a, MyStore>` (Rust's orphan rule forbids inherent impls on a
317/// foreign type from another crate; the AUTHZ-DATA-2-MACRO layer
318/// generates the trait + impl).
319///
320/// # Lifetime
321///
322/// The single `'a` lifetime is the shorter of the parent
323/// [`Tenanted<S>`]'s borrow and the supplied `&Tenant`. Both must
324/// outlive the `Scoped`. This is what makes "fabricated tenant" and
325/// "mismatched tenants" structural failures: the lifetime threads
326/// through every call site.
327///
328/// # Public surface
329///
330/// The framework supplies only [`store`](Self::store),
331/// [`tenant`](Self::tenant), and [`boundary`](Self::boundary). The
332/// activation's own domain methods live in a trait implementation on
333/// `Scoped<'_, MyStore>` inside the activation's own crate.
334///
335/// # Sealing
336///
337/// - **No fabrication.** Constructor is module-private; the only path
338///   is [`Tenanted::scoped`].
339/// - **No struct-literal.** Fields are module-private.
340/// - **No `Default`.** Not derived.
341/// - **No `Deserialize`.** Not derived; the borrow is not transport.
342pub struct Scoped<'a, S: TenantScopedStore> {
343    /// Borrow of the inner store. Module-private — outside crates can
344    /// only reach it through [`store`](Self::store), which the
345    /// activation reads inside its own domain trait impl on `Scoped`.
346    inner: &'a S,
347    /// Borrow of the active tenant. Module-private; reached via
348    /// [`tenant`](Self::tenant).
349    tenant: &'a Tenant,
350    /// Witness that this `Scoped` is structurally downstream of a
351    /// [`TenantBoundary`].
352    boundary: TenantBoundary,
353}
354
355impl<'a, S: TenantScopedStore> Scoped<'a, S> {
356    /// Borrow the underlying storage handle.
357    ///
358    /// The activation's domain trait impls on `Scoped` call this to
359    /// reach the store's bare API, paired with [`tenant`](Self::tenant)
360    /// to bind the active tenant in queries. There is no path to the
361    /// bare store that does not require having produced a `Scoped`,
362    /// which itself requires a `&Tenant`.
363    pub fn store(&self) -> &'a S {
364        self.inner
365    }
366
367    /// Borrow the active tenant.
368    ///
369    /// Activation domain methods read this to bind the tenant in every
370    /// SQL bind, key prefix, namespace, etc.
371    pub fn tenant(&self) -> &'a Tenant {
372        self.tenant
373    }
374
375    /// Return the structural [`TenantBoundary`] witness for this scope.
376    ///
377    /// Useful for downstream audit/attestation code that wants a
378    /// type-level proof that a tenant boundary was crossed.
379    pub fn boundary(&self) -> TenantBoundary {
380        self.boundary
381    }
382}
383
384impl<'a, S: TenantScopedStore> std::fmt::Debug for Scoped<'a, S> {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        // Same rationale as Tenanted's Debug: don't pretty-print the
387        // inner store. The tenant is printable (already public via
388        // `Tenant: Display`); it's safe to surface.
389        f.debug_struct("Scoped")
390            .field("tenant", &self.tenant.as_str())
391            .field("store", &"<sealed>")
392            .finish()
393    }
394}
395
396// ---------------------------------------------------------------------------
397// Reference store — `InMemoryKvStore` (the doc-test vehicle).
398//
399// A pre-sealed example `TenantScopedStore` is required because:
400//
401// 1. Rust's orphan rule (E0116) forbids `impl Scoped<'_, MyStore>` as an
402//    inherent impl from any crate that does not own `Scoped`. Activation
403//    code uses a trait + impl pattern (generated by AUTHZ-DATA-2-MACRO);
404//    that pattern is also what doc tests must follow.
405// 2. The seal on `TenantScopedStore` is strict: no crate outside
406//    `plexus-auth-core` can satisfy `seal::SealedStore`. So the
407//    canonical happy-path doc test cannot define its own example store
408//    inline. The reference store lives inside this crate.
409//
410// `InMemoryKvStore` is a useful generic K/V example in its own right.
411// Per the run-notes, it is the minimal type addition needed to give the
412// ticket's acceptance criterion 6 a passing doc test.
413// ---------------------------------------------------------------------------
414
415pub mod reference {
416    //! Reference [`TenantScopedStore`] implementations.
417    //!
418    //! These types are pre-sealed inside `plexus-auth-core` so that the
419    //! canonical wrapper pattern can be demonstrated without requiring
420    //! AUTHZ-DATA-2-* tickets to land first. They are also useful in
421    //! their own right for in-memory tests and small dev installs.
422    //!
423    //! See `plans/AUTHZ/AUTHZ-DATA-1-WRAPPER-RUN-NOTES.md` for why a
424    //! reference store is required to satisfy the doc-test acceptance
425    //! criterion under Rust's orphan rule.
426    use super::{Scoped, TenantScopedStore};
427    use crate::tenant::types::Tenant;
428    use std::collections::BTreeMap;
429    use std::sync::Mutex;
430
431    /// A small thread-safe in-memory key/value store, partitioned by
432    /// [`Tenant`].
433    ///
434    /// The store is intentionally minimal: it serves the canonical
435    /// doc-test demonstration of the wrapper pattern. Real backends
436    /// (SQL, Redis, vector DBs, etc.) define their own
437    /// [`TenantScopedStore`] type inside their own crate; the activation
438    /// macro generates the trait + impl on `Scoped<'_, ThatStore>`.
439    #[derive(Debug, Default)]
440    pub struct InMemoryKvStore {
441        /// Per-tenant map of key → value. `Mutex` because the store is
442        /// shared across the dispatch (`Send + Sync` is required by
443        /// `TenantScopedStore`).
444        inner: Mutex<BTreeMap<String, BTreeMap<String, Vec<u8>>>>,
445    }
446
447    impl InMemoryKvStore {
448        /// Construct an empty store.
449        pub fn new() -> Self {
450            Self::default()
451        }
452
453        /// Pre-populate the store for a doc test or unit test.
454        ///
455        /// Marked with the `_for_doc_test` suffix because it bypasses
456        /// the tenant-scoping ceremony: it directly inserts under a
457        /// caller-supplied tenant string. Real production code uses
458        /// only the `put` method on [`Scoped<'_, Self>`](super::Scoped),
459        /// which goes through `Scoped` and binds the active tenant.
460        pub fn put_for_doc_test(&self, tenant: &str, key: &str, value: Vec<u8>) {
461            let mut g = self.inner.lock().unwrap();
462            g.entry(tenant.to_string())
463                .or_default()
464                .insert(key.to_string(), value);
465        }
466    }
467
468    seal_store_impl!(InMemoryKvStore);
469    impl TenantScopedStore for InMemoryKvStore {
470        type Error = InMemoryKvError;
471    }
472
473    /// Error type for [`InMemoryKvStore`] operations.
474    ///
475    /// The reference store is in-memory and infallible in practice; the
476    /// error type exists to satisfy the
477    /// [`TenantScopedStore::Error`] associated type bound. The variant
478    /// is intentionally empty so
479    /// rustc's exhaustiveness checks remain useful.
480    #[derive(Debug)]
481    pub enum InMemoryKvError {
482        /// Reserved for future variants. The reference store never
483        /// produces an error today.
484        #[doc(hidden)]
485        __NonExhaustive,
486    }
487
488    impl std::fmt::Display for InMemoryKvError {
489        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490            match self {
491                Self::__NonExhaustive => f.write_str("<unreachable>"),
492            }
493        }
494    }
495
496    impl std::error::Error for InMemoryKvError {}
497
498    // Domain methods on `Scoped<'a, InMemoryKvStore>`. These live inside
499    // `plexus-auth-core` because Rust's orphan rule for inherent impls
500    // requires the impl to live in the crate that owns either `Scoped`
501    // or the second generic parameter — and we own both. (Activation
502    // crates can satisfy the orphan rule with a trait + impl pattern
503    // since they own at least one of the trait or `MyStore`.)
504    impl<'a> Scoped<'a, InMemoryKvStore> {
505        /// Insert a value under the active tenant's namespace.
506        pub fn put(&self, key: &str, value: Vec<u8>) {
507            let mut g = self.store().inner.lock().unwrap();
508            g.entry(self.tenant().as_str().to_string())
509                .or_default()
510                .insert(key.to_string(), value);
511        }
512
513        /// Look up a value under the active tenant's namespace.
514        pub fn get(&self, key: &str) -> Option<Vec<u8>> {
515            let g = self.store().inner.lock().unwrap();
516            g.get(self.tenant().as_str())
517                .and_then(|m| m.get(key).cloned())
518        }
519
520        /// List all keys in the active tenant's namespace.
521        pub fn list_keys(&self) -> Vec<String> {
522            let g = self.store().inner.lock().unwrap();
523            g.get(self.tenant().as_str())
524                .map(|m| m.keys().cloned().collect())
525                .unwrap_or_default()
526        }
527    }
528
529    // Make `Tenant` reachable inside the doc tests on this submodule.
530    #[allow(dead_code)]
531    fn _force_tenant_in_scope(_t: &Tenant) {}
532}
533
534// ---------------------------------------------------------------------------
535// Doc-test helpers (hidden — not part of the public API).
536//
537// These are `pub` because doc tests compile as external crates; they
538// must be reachable. They are `#[doc(hidden)]` because they are NOT the
539// framework's normal construction path — they exist solely so the
540// canonical happy-path doc test can compile and run.
541//
542// The structural seal is unchanged: `TenantScopedStore` is still
543// implementable only from inside this crate, so these helpers can wrap
544// only types that are themselves sealed inside `plexus-auth-core`. A
545// third-party crate gains nothing from calling them.
546// ---------------------------------------------------------------------------
547
548/// Doc-test helper: wrap a pre-sealed store in a `Tenanted<S>`.
549///
550/// **Not part of the framework's public construction path.** The
551/// hub-builder API in a follow-up ticket is the supported way for an
552/// activation to receive a `Tenanted<S>`.
553#[doc(hidden)]
554pub fn __doctest_blessed_tenanted<S: TenantScopedStore>(inner: S) -> Tenanted<S> {
555    Tenanted::new_sealed(inner)
556}
557
558/// Doc-test helper: mint a `Tenant` from a string literal.
559///
560/// **Not part of the framework's public construction path.** Real
561/// callers obtain a `Tenant` only via [`crate::TenantResolver`].
562#[doc(hidden)]
563pub fn __doctest_blessed_tenant(s: &str) -> Tenant {
564    Tenant::try_new(s).expect("doc-test tenant identifier should be valid")
565}
566
567// ---------------------------------------------------------------------------
568// Unit tests.
569// ---------------------------------------------------------------------------
570
571#[cfg(test)]
572mod tests {
573    use super::reference::InMemoryKvStore;
574    use super::*;
575
576    // A second in-crate `TenantScopedStore` implementor for unit tests,
577    // distinct from the reference store, to confirm the seal works for
578    // arbitrary friend types.
579    #[derive(Debug)]
580    struct TestStore {
581        rows: Vec<(String, i64)>,
582    }
583
584    seal_store_impl!(TestStore);
585    impl TenantScopedStore for TestStore {
586        type Error = std::io::Error;
587    }
588
589    impl<'a> Scoped<'a, TestStore> {
590        fn count_for_tenant(&self) -> i64 {
591            let tid = self.tenant().as_str();
592            self.store()
593                .rows
594                .iter()
595                .filter(|(t, _)| t == tid)
596                .map(|(_, n)| n)
597                .sum()
598        }
599    }
600
601    fn fixture_store() -> TestStore {
602        TestStore {
603            rows: vec![
604                ("acme".into(), 3),
605                ("acme".into(), 5),
606                ("beta".into(), 11),
607            ],
608        }
609    }
610
611    #[test]
612    fn tenanted_wraps_store_via_crate_private_constructor() {
613        let _ = Tenanted::new_sealed(fixture_store());
614    }
615
616    #[test]
617    fn scoped_borrow_threads_tenant_to_domain_method() {
618        let store = fixture_store();
619        let tenanted = Tenanted::new_sealed(store);
620        let tenant = Tenant::try_new("acme").unwrap();
621
622        let scoped = tenanted.scoped(&tenant);
623        assert_eq!(scoped.count_for_tenant(), 8);
624    }
625
626    #[test]
627    fn scoped_isolates_by_tenant() {
628        let store = fixture_store();
629        let tenanted = Tenanted::new_sealed(store);
630
631        let acme = Tenant::try_new("acme").unwrap();
632        let beta = Tenant::try_new("beta").unwrap();
633
634        assert_eq!(tenanted.scoped(&acme).count_for_tenant(), 8);
635        assert_eq!(tenanted.scoped(&beta).count_for_tenant(), 11);
636    }
637
638    #[test]
639    fn scoped_returns_supplied_tenant() {
640        let store = fixture_store();
641        let tenanted = Tenanted::new_sealed(store);
642        let tenant = Tenant::try_new("acme").unwrap();
643
644        let scoped = tenanted.scoped(&tenant);
645        assert_eq!(scoped.tenant().as_str(), "acme");
646    }
647
648    #[test]
649    fn scoped_carries_tenant_boundary_witness() {
650        let store = fixture_store();
651        let tenanted = Tenanted::new_sealed(store);
652        let tenant = Tenant::try_new("acme").unwrap();
653
654        let scoped = tenanted.scoped(&tenant);
655        let b1: TenantBoundary = scoped.boundary();
656        let b2: TenantBoundary = scoped.boundary();
657        assert_eq!(b1, b2);
658    }
659
660    #[test]
661    fn tenant_boundary_is_copy_and_eq() {
662        let a = TenantBoundary::new_sealed();
663        let b = a; // Copy
664        assert_eq!(a, b);
665    }
666
667    #[test]
668    fn tenanted_debug_elides_inner_store() {
669        let store = fixture_store();
670        let tenanted = Tenanted::new_sealed(store);
671        let rendered = format!("{tenanted:?}");
672        assert!(rendered.contains("Tenanted"));
673        assert!(rendered.contains("<sealed>"));
674        // The inner row data must NOT appear.
675        assert!(!rendered.contains("acme"));
676        assert!(!rendered.contains("beta"));
677    }
678
679    #[test]
680    fn scoped_debug_elides_inner_store_but_surfaces_tenant() {
681        let store = fixture_store();
682        let tenanted = Tenanted::new_sealed(store);
683        let tenant = Tenant::try_new("acme").unwrap();
684        let scoped = tenanted.scoped(&tenant);
685        let rendered = format!("{scoped:?}");
686        assert!(rendered.contains("Scoped"));
687        assert!(rendered.contains("<sealed>"));
688        // The tenant identifier is safe to surface (Tenant: Display).
689        assert!(rendered.contains("acme"));
690        // The inner row data must NOT appear.
691        assert!(!rendered.contains("beta"));
692    }
693
694    #[test]
695    fn scoped_lifetime_bounds_to_shorter_of_wrapper_and_tenant() {
696        // Compile-only test: both references must outlive `'a`.
697        // If either source goes out of scope, the borrow is invalid.
698        let store = fixture_store();
699        let tenanted = Tenanted::new_sealed(store);
700        let tenant = Tenant::try_new("acme").unwrap();
701        {
702            let scoped: Scoped<'_, TestStore> = tenanted.scoped(&tenant);
703            let _ = scoped.tenant();
704        }
705    }
706
707    #[test]
708    fn store_associated_error_is_named_per_impl() {
709        // Compile-only assertion that the associated error type is
710        // reachable via the trait.
711        fn _accepts_err<S: TenantScopedStore>(_e: S::Error) {}
712        let _io = std::io::Error::other("test");
713        _accepts_err::<TestStore>(_io);
714    }
715
716    #[test]
717    fn reference_store_round_trip_isolates_tenants() {
718        let store = InMemoryKvStore::new();
719        let tenanted = Tenanted::new_sealed(store);
720
721        let acme = Tenant::try_new("acme").unwrap();
722        let beta = Tenant::try_new("beta").unwrap();
723
724        tenanted.scoped(&acme).put("k", b"acme-value".to_vec());
725        tenanted.scoped(&beta).put("k", b"beta-value".to_vec());
726
727        assert_eq!(
728            tenanted.scoped(&acme).get("k"),
729            Some(b"acme-value".to_vec())
730        );
731        assert_eq!(
732            tenanted.scoped(&beta).get("k"),
733            Some(b"beta-value".to_vec())
734        );
735
736        // The acme tenant cannot read beta's value: `get("k")` returns
737        // only the value under its own namespace.
738        assert_ne!(
739            tenanted.scoped(&acme).get("k"),
740            Some(b"beta-value".to_vec())
741        );
742    }
743
744    #[test]
745    fn reference_store_list_keys_is_tenant_partitioned() {
746        let store = InMemoryKvStore::new();
747        store.put_for_doc_test("acme", "k1", b"v1".to_vec());
748        store.put_for_doc_test("acme", "k2", b"v2".to_vec());
749        store.put_for_doc_test("beta", "k3", b"v3".to_vec());
750        let tenanted = Tenanted::new_sealed(store);
751
752        let acme = Tenant::try_new("acme").unwrap();
753        let beta = Tenant::try_new("beta").unwrap();
754
755        let mut acme_keys = tenanted.scoped(&acme).list_keys();
756        acme_keys.sort();
757        assert_eq!(acme_keys, vec!["k1".to_string(), "k2".to_string()]);
758
759        assert_eq!(tenanted.scoped(&beta).list_keys(), vec!["k3".to_string()]);
760    }
761}