Skip to main content

vcon_server/
tenants.rs

1//! The registry of tenants: each one's id, name, retention period and
2//! wrapped tenant key, kept in `server.sqlite` under a catalog key derived
3//! from the master key, with the admin audit log beside them. Each tenant's
4//! vCons live in `tenants/<id>/`, in a Store of their own.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex, MutexGuard};
9
10use time::OffsetDateTime;
11
12use crate::audit::{self, Action, Actor, Entry};
13#[cfg(test)]
14use crate::backend::local::LocalConn;
15use crate::backend::local::{LocalRegistry, LocalTenantSpace};
16use crate::backend::{CommitError, ObjectPlace, Registry, RegistryTx, TenantRow, TenantSpace};
17use crate::clock::{Clock, from_nanos, nanos};
18use crate::envelope::{self, Context, Key, WrapLimit};
19use crate::export::Metadata;
20use crate::fs::Fs;
21use crate::ids::TenantId;
22use crate::retention::Retention;
23use crate::store::{OpenError, Store};
24
25mod backup;
26mod examine;
27mod keys;
28mod rekey;
29
30pub use backup::{Backup, BackupError};
31pub use examine::{KeyOrder, RegistryExamination, RotationBacklog, TenantExamination, Unexamined};
32pub use keys::{KeyInfo, NewKey};
33pub(crate) use rekey::MasterStep;
34pub use rekey::rekey_master;
35#[cfg(test)]
36pub(crate) use tests::{Fixture, fixture};
37
38/// A tenant's metadata column as read: NULL is `none`, and a level this
39/// build does not know is damage.
40fn stored_metadata(text: Option<String>) -> Result<Metadata, TenantError> {
41    match text {
42        None => Ok(Metadata::None),
43        Some(text) => Metadata::parse(&text).ok_or(TenantError::Io),
44    }
45}
46
47/// A tenant's retention column as read: None is the server default. A
48/// period that does not read back is damage, not the server default.
49fn stored_retention(text: Option<String>) -> Result<Option<Retention>, TenantError> {
50    text.map(|text| Retention::parse(&text).ok_or(TenantError::Io))
51        .transpose()
52}
53
54/// Why a Tenants call was refused or failed.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TenantError {
57    /// No tenant has the id.
58    NotFound,
59    /// Another tenant has the name.
60    NameTaken,
61    /// The name breaks the naming rule (`good_name`).
62    BadName,
63    /// `Removal::Empty` was asked of a tenant that keeps versions.
64    HoldsVcons,
65    /// `Removal::Force` was asked of a tenant with a uuid under a legal
66    /// hold.
67    UnderHold,
68    /// The master key is at its wrap limit and must be rotated.
69    LimitReached,
70    /// The audit record could not be written, so nothing was done.
71    AuditFailed,
72    /// The tenant is being removed.
73    Closing,
74    /// Only the command line may do this: make an operator key.
75    CliOnly,
76    /// Scopes a key of its kind cannot hold.
77    BadScopes,
78    /// server.sqlite was made under another master key.
79    WrongKey,
80    Io,
81}
82
83/// Why the registry did not open: the key it was made under, or the step
84/// that failed, on which file, with the error underneath as its source, so
85/// a failure says its cause.
86#[derive(Debug)]
87#[non_exhaustive]
88pub enum RegistryError {
89    /// server.sqlite was made under another master key.
90    WrongKey,
91    /// `step` failed on `path`.
92    Failed {
93        /// What was being done.
94        step: OpenStep,
95        /// The file or directory it was done to.
96        path: PathBuf,
97        /// The error underneath.
98        source: Box<dyn std::error::Error + Send + Sync>,
99    },
100    /// Start-up recovery failed.
101    Recovery(TenantError),
102}
103
104/// A step of opening the registry.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum OpenStep {
108    /// Deriving server.sqlite's key from the master key.
109    CatalogKey,
110    /// Opening, or making, server.sqlite.
111    OpenCatalog,
112    /// Making the tenants/ directory.
113    MakeTenantsDir,
114    /// Syncing the data directory.
115    SyncDataDir,
116}
117
118impl RegistryError {
119    pub(crate) fn failed(
120        step: OpenStep,
121        path: PathBuf,
122        source: impl std::error::Error + Send + Sync + 'static,
123    ) -> RegistryError {
124        RegistryError::Failed {
125            step,
126            path,
127            source: Box::new(source),
128        }
129    }
130}
131
132impl std::fmt::Display for RegistryError {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            RegistryError::WrongKey => {
136                f.write_str("the master key is not the one server.sqlite was made under")
137            }
138            RegistryError::Failed { step, path, .. } => {
139                let doing = match step {
140                    OpenStep::CatalogKey => "the catalog key for",
141                    OpenStep::OpenCatalog => "opening",
142                    OpenStep::MakeTenantsDir => "making",
143                    OpenStep::SyncDataDir => "syncing",
144                };
145                write!(f, "{doing} {} failed", path.display())
146            }
147            RegistryError::Recovery(e) => write!(f, "start-up recovery failed: {e:?}"),
148        }
149    }
150}
151
152impl std::error::Error for RegistryError {
153    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
154        match self {
155            RegistryError::Failed { source, .. } => Some(source.as_ref()),
156            RegistryError::WrongKey | RegistryError::Recovery(_) => None,
157        }
158    }
159}
160
161/// `error` and each error under it, as their Displays joined by ": ", for
162/// a report that gives the whole cause.
163pub(crate) fn with_causes(error: &dyn std::error::Error) -> String {
164    let mut text = error.to_string();
165    let mut under = error.source();
166    while let Some(cause) = under {
167        text.push_str(": ");
168        text.push_str(&cause.to_string());
169        under = cause.source();
170    }
171    text
172}
173
174/// Every caller but the two that report it sees what TenantError can say.
175impl From<RegistryError> for TenantError {
176    fn from(e: RegistryError) -> TenantError {
177        match e {
178            RegistryError::WrongKey => TenantError::WrongKey,
179            RegistryError::Failed { .. } => TenantError::Io,
180            RegistryError::Recovery(e) => e,
181        }
182    }
183}
184
185/// Why a tenant's Store could not be had: the registry refused, its
186/// catalog does not open under its key, or this process may not use one of
187/// its files. Only `check` and `serve`'s start-up check tell these apart;
188/// every other caller sees `TenantError` (`From` below).
189#[derive(Debug)]
190pub enum OpenFailure {
191    Refused(TenantError),
192    WrongKey,
193    Access {
194        path: PathBuf,
195        error: std::io::Error,
196    },
197}
198
199impl From<OpenFailure> for TenantError {
200    fn from(f: OpenFailure) -> TenantError {
201        match f {
202            OpenFailure::Refused(e) => e,
203            OpenFailure::WrongKey | OpenFailure::Access { .. } => TenantError::Io,
204        }
205    }
206}
207
208impl From<TenantError> for OpenFailure {
209    fn from(e: TenantError) -> OpenFailure {
210        OpenFailure::Refused(e)
211    }
212}
213
214impl From<OpenError> for OpenFailure {
215    fn from(e: OpenError) -> OpenFailure {
216        match e {
217            OpenError::WrongKey => OpenFailure::WrongKey,
218            OpenError::Access { path, error } => OpenFailure::Access { path, error },
219            OpenError::Store(_) => OpenFailure::Refused(TenantError::Io),
220        }
221    }
222}
223
224/// A tenant as `list` shows it.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct TenantInfo {
227    pub id: TenantId,
228    pub name: String,
229    /// The tenant's own retention period, if it set one.
230    pub retention: Option<Retention>,
231    /// Which clear columns its Parquet files carry.
232    pub metadata: Metadata,
233    pub created_at: OffsetDateTime,
234}
235
236impl TenantInfo {
237    /// The tenant as the admin API and the command line show it: id, name,
238    /// its own retention period, null for the server default, and its
239    /// metadata level.
240    pub fn to_json(&self) -> serde_json::Value {
241        serde_json::json!({
242            "id": self.id.to_string(),
243            "name": self.name,
244            "retention": self.retention.map(|r| r.to_string()),
245            "metadata": self.metadata.as_str(),
246        })
247    }
248
249    /// How long the tenant's vCons are kept: its own period, else the
250    /// server's `default`.
251    pub fn kept_for(&self, default: Retention) -> Retention {
252        kept_for(self.retention, default)
253    }
254}
255
256/// How long a tenant's vCons are kept: its own period `own`, else the
257/// server's `default`. The one rule the sweep, `check` and TenantInfo use.
258pub(crate) fn kept_for(own: Option<Retention>, default: Retention) -> Retention {
259    own.unwrap_or(default)
260}
261
262/// How far `remove` may go.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum Removal {
265    /// Only a tenant that keeps no version.
266    Empty,
267    /// Its vCons too, unless a uuid is under a legal hold.
268    Force,
269    /// Even held vCons; only from the command line, with the server stopped.
270    ForceOverridingHolds,
271}
272
273/// A tenant's Store as the registry holds it.
274enum Slot {
275    Open(Arc<Store>),
276    /// Being removed: no new handle is handed out.
277    Closing,
278}
279
280/// The registry of tenants.
281pub struct Tenants {
282    /// Where each tenant's catalog and objects are kept.
283    space: Box<dyn TenantSpace>,
284    master: Key,
285    registry: Box<dyn Registry>,
286    /// Each tenant's Store, once a request has opened it.
287    stores: Mutex<HashMap<TenantId, Slot>>,
288    /// Opened beside a server: its Stores are opened without recovery.
289    beside: bool,
290    fs: Arc<dyn Fs>,
291    clock: Arc<dyn Clock>,
292    #[cfg(test)]
293    hooks: tests::Hooks,
294}
295
296impl Tenants {
297    /// Opens `<dir>/server.sqlite` under the master key's catalog key,
298    /// making it if new, and removes any tenant directory a crash left
299    /// without its row.
300    pub fn open(
301        dir: &Path,
302        master: Key,
303        fs: Arc<dyn Fs>,
304        clock: Arc<dyn Clock>,
305    ) -> Result<Tenants, RegistryError> {
306        Tenants::open_in(dir, master, fs, clock, ObjectPlace::Local)
307    }
308
309    /// Opens the registry as `open` does, with each tenant's objects kept
310    /// in `objects`.
311    pub(crate) fn open_in(
312        dir: &Path,
313        master: Key,
314        fs: Arc<dyn Fs>,
315        clock: Arc<dyn Clock>,
316        objects: ObjectPlace,
317    ) -> Result<Tenants, RegistryError> {
318        let registry = Tenants::opened(dir, master, fs, clock, false, objects)?;
319        registry.recover().map_err(RegistryError::Recovery)?;
320        Ok(registry)
321    }
322
323    /// Opens the registry as a command running beside a server does:
324    /// without start-up recovery, here or in any Store it opens, which
325    /// would take the server's creates and writes in progress for a
326    /// crash's leftovers.
327    pub fn open_beside(
328        dir: &Path,
329        master: Key,
330        fs: Arc<dyn Fs>,
331        clock: Arc<dyn Clock>,
332    ) -> Result<Tenants, RegistryError> {
333        Tenants::opened(dir, master, fs, clock, true, ObjectPlace::Local)
334    }
335
336    /// Opens the registry as `open_beside` does, with each tenant's objects
337    /// kept in `objects`.
338    pub(crate) fn open_beside_in(
339        dir: &Path,
340        master: Key,
341        fs: Arc<dyn Fs>,
342        clock: Arc<dyn Clock>,
343        objects: ObjectPlace,
344    ) -> Result<Tenants, RegistryError> {
345        Tenants::opened(dir, master, fs, clock, true, objects)
346    }
347
348    fn opened(
349        dir: &Path,
350        master: Key,
351        fs: Arc<dyn Fs>,
352        clock: Arc<dyn Clock>,
353        beside: bool,
354        objects: ObjectPlace,
355    ) -> Result<Tenants, RegistryError> {
356        let catalog_key = envelope::catalog_key(&master).map_err(|e| {
357            RegistryError::failed(OpenStep::CatalogKey, dir.join("server.sqlite"), e)
358        })?;
359        let registry = LocalRegistry::open(dir, catalog_key)?;
360        let space = LocalTenantSpace::open(dir, fs.clone(), objects)?;
361        Ok(Tenants::with_backends(
362            Box::new(registry),
363            Box::new(space),
364            master,
365            fs,
366            clock,
367            beside,
368        ))
369    }
370
371    /// The registry over the given backends, opened as `open_beside` opens
372    /// one when `beside`, else without its start-up recovery yet.
373    pub(crate) fn with_backends(
374        registry: Box<dyn Registry>,
375        space: Box<dyn TenantSpace>,
376        master: Key,
377        fs: Arc<dyn Fs>,
378        clock: Arc<dyn Clock>,
379        beside: bool,
380    ) -> Tenants {
381        Tenants {
382            space,
383            master,
384            registry,
385            stores: Mutex::new(HashMap::new()),
386            beside,
387            fs,
388            clock,
389            #[cfg(test)]
390            hooks: tests::Hooks::default(),
391        }
392    }
393
394    /// Removes each tenant directory with no row: a create cut off before
395    /// its commit. A name that is not a tenant id is not ours and is left
396    /// alone. A removal a crash undoes is made again at the next start, so
397    /// none is synced.
398    fn recover(&self) -> Result<(), TenantError> {
399        for id in self.space.present()? {
400            if self.registry.snapshot()?.tenant(id)?.is_none() {
401                self.space.remove(id)?;
402            }
403        }
404        Ok(())
405    }
406
407    /// Makes a tenant: a new tenant key wrapped under the master key, the
408    /// tenant's directory and catalog, and then its row. A create that
409    /// fails after making its directory removes it if it can; any directory
410    /// left without a row, by a crash or a failed removal, goes at the next
411    /// start.
412    pub fn create(
413        &self,
414        who: Actor,
415        name: &str,
416        retention: Option<Retention>,
417    ) -> Result<TenantId, TenantError> {
418        let id = TenantId::generate();
419        let entry = |status, target: Option<TenantId>| Entry {
420            actor: who,
421            action: Action::TenantCreate,
422            target: target.map(|t| t.to_string()),
423            version: None,
424            status,
425        };
426        let master_id = self.master.id();
427        {
428            let mut tx = self.registry.immediate()?;
429            let refused = if !good_name(name) {
430                Some((422, TenantError::BadName))
431            } else if tx.name_taken(name)? {
432                Some((409, TenantError::NameTaken))
433            } else if envelope::wrap_limit(tx.master_wraps(master_id)?) == WrapLimit::Refuse {
434                Some((503, TenantError::LimitReached))
435            } else {
436                None
437            };
438            if let Some((status, error)) = refused {
439                self.record(&mut tx, &entry(status, None))?;
440                commit(tx)?;
441                return Err(error);
442            }
443            // The wrap is counted before it is made, so every wrap is
444            // counted once whether this create wins or loses.
445            tx.add_master_wrap(master_id, nanos(self.clock.now()))?;
446            commit(tx)?;
447        }
448
449        let tenant_key = Key::generate().map_err(|_| TenantError::Io)?;
450        let key_id = tenant_key.id();
451        let wrapped = envelope::wrap(&self.master, &Context::Tenant { tenant: id }, &tenant_key);
452        // The directory and its catalog are made before the row, and made
453        // durable: the directory's entry in `tenants/` by syncing it. Any
454        // step that fails once the directory may exist removes it.
455        let unknown = std::cell::Cell::new(false);
456        let made = (|| {
457            self.space
458                .make(id, tenant_key, self.clock.clone())
459                .map_err(|_| TenantError::Io)?;
460            #[cfg(test)]
461            {
462                let hook = self.hooks.between.lock().expect("hook").take();
463                if let Some(hook) = hook {
464                    hook();
465                }
466            }
467
468            let mut tx = self.registry.immediate()?;
469            if tx.name_taken(name)? {
470                // A refusal's record commits.
471                self.record(&mut tx, &entry(409, None))?;
472                commit(tx)?;
473                return Err(TenantError::NameTaken);
474            }
475            let now = nanos(self.clock.now());
476            tx.insert_tenant(&TenantRow {
477                id,
478                name: name.to_owned(),
479                retention: retention.map(|r| r.to_string()),
480                metadata: None,
481                created_at: now,
482                tenant_key_id: key_id,
483                wrapped: wrapped.clone(),
484                pending: None,
485                key_since: now,
486            })?;
487            self.record(&mut tx, &entry(201, Some(id)))?;
488            tx.commit().map_err(|e| {
489                unknown.set(e == CommitError::Unknown);
490                TenantError::Io
491            })?;
492            Ok(id)
493        })();
494        // After a commit whose outcome is unknown the directory stays: the
495        // row may be kept, and a row without its directory is a tenant
496        // that cannot open, while a directory without a row is removed at
497        // the next start.
498        if made.is_err() && !unknown.get() {
499            let _ = self.space.remove(id);
500        }
501        made
502    }
503
504    /// Every tenant, by name.
505    pub fn list(&self, who: Actor) -> Result<Vec<TenantInfo>, TenantError> {
506        let mut tx = self.registry.immediate()?;
507        let tenants = tx
508            .tenants()?
509            .into_iter()
510            .map(|row| {
511                Ok(TenantInfo {
512                    id: row.id,
513                    name: row.name,
514                    retention: stored_retention(row.retention)?,
515                    metadata: stored_metadata(row.metadata)?,
516                    created_at: from_nanos(row.created_at).ok_or(TenantError::Io)?,
517                })
518            })
519            .collect::<Result<Vec<_>, TenantError>>()?;
520        let entry = Entry {
521            actor: who,
522            action: Action::TenantList,
523            target: None,
524            version: None,
525            status: 200,
526        };
527        self.record(&mut tx, &entry)?;
528        commit(tx)?;
529        Ok(tenants)
530    }
531
532    /// Sets the tenant's retention period, or with None returns it to the
533    /// server default.
534    pub fn set_retention(
535        &self,
536        who: Actor,
537        id: TenantId,
538        retention: Option<Retention>,
539    ) -> Result<(), TenantError> {
540        let entry = |action, status| Entry {
541            actor: who,
542            action,
543            target: Some(id.to_string()),
544            version: None,
545            status,
546        };
547        let closing = matches!(self.stores().get(&id), Some(Slot::Closing));
548        {
549            let mut tx = self.registry.immediate()?;
550            let text = retention.map(|r| r.to_string());
551            let refused = if closing {
552                Some((503, TenantError::Closing))
553            } else if !tx.set_retention(id, text.as_deref())? {
554                Some((404, TenantError::NotFound))
555            } else {
556                None
557            };
558            let status = refused.map_or(200, |(status, _)| status);
559            self.record(&mut tx, &entry(Action::TenantRetention, status))?;
560            commit(tx)?;
561            if let Some((_, error)) = refused {
562                return Err(error);
563            }
564        }
565        self.copy_to_tenant(id, &entry(Action::Retention, 200))
566    }
567
568    /// Sets which clear columns the tenant's Parquet files carry.
569    pub fn set_metadata(
570        &self,
571        who: Actor,
572        id: TenantId,
573        level: Metadata,
574    ) -> Result<(), TenantError> {
575        let entry = |action, status| Entry {
576            actor: who,
577            action,
578            target: Some(id.to_string()),
579            version: None,
580            status,
581        };
582        let closing = matches!(self.stores().get(&id), Some(Slot::Closing));
583        {
584            let mut tx = self.registry.immediate()?;
585            let text = (level != Metadata::None).then(|| level.as_str());
586            let refused = if closing {
587                Some((503, TenantError::Closing))
588            } else if !tx.set_metadata(id, text)? {
589                Some((404, TenantError::NotFound))
590            } else {
591                None
592            };
593            let status = refused.map_or(200, |(status, _)| status);
594            self.record(&mut tx, &entry(Action::TenantMetadata, status))?;
595            commit(tx)?;
596            if let Some((_, error)) = refused {
597                return Err(error);
598            }
599        }
600        self.copy_to_tenant(id, &entry(Action::Metadata, 200))
601    }
602
603    /// The tenant's metadata level.
604    pub fn metadata(&self, id: TenantId) -> Result<Metadata, TenantError> {
605        let row = self.registry.snapshot()?.tenant(id)?;
606        stored_metadata(row.ok_or(TenantError::NotFound)?.metadata)
607    }
608
609    /// Writes the copy of an admin record that a tenant's own log keeps
610    /// too. The change it records has already committed with its admin
611    /// record, so a crash or failure here can lose only the copy; the
612    /// caller then answers that the audit failed, though the change
613    /// stands.
614    fn copy_to_tenant(&self, tenant: TenantId, entry: &Entry) -> Result<(), TenantError> {
615        let store = self.store(tenant).map_err(|_| TenantError::AuditFailed)?;
616        store.log(entry).map_err(|_| TenantError::AuditFailed)
617    }
618
619    /// Removes the tenant: its row and wrapped key, then its directory.
620    ///
621    /// The tenant is drained before it is looked at, so a write that lands
622    /// meanwhile counts: an `Empty` removal is then refused rather than
623    /// losing it. A refused or failed removal leaves the tenant served
624    /// again.
625    pub fn remove(&self, who: Actor, id: TenantId, removal: Removal) -> Result<(), TenantError> {
626        let entry = |status| Entry {
627            actor: who,
628            action: Action::TenantRemove,
629            target: Some(id.to_string()),
630            version: None,
631            status,
632        };
633        let drained = match self.drain(id) {
634            Ok(drained) => drained,
635            Err(error) => {
636                self.record_alone(&entry(503))?;
637                return Err(error);
638            }
639        };
640        let result = self.remove_drained(id, removal, drained, entry);
641        if result.is_err() {
642            self.stores().remove(&id);
643        }
644        result
645    }
646
647    fn remove_drained(
648        &self,
649        id: TenantId,
650        removal: Removal,
651        drained: Option<Store>,
652        entry: impl Fn(u16) -> Entry,
653    ) -> Result<(), TenantError> {
654        let store = match drained {
655            Some(store) => store,
656            None => match self.open_store(id).map_err(TenantError::from) {
657                Err(TenantError::NotFound) => {
658                    self.record_alone(&entry(404))?;
659                    return Err(TenantError::NotFound);
660                }
661                other => other?,
662            },
663        };
664        let counts = store.counts().map_err(io)?;
665        let refused = match removal {
666            Removal::Empty if counts.versions > 0 => Some(TenantError::HoldsVcons),
667            Removal::Force if counts.held > 0 => Some(TenantError::UnderHold),
668            _ => None,
669        };
670        if let Some(error) = refused {
671            self.record_alone(&entry(409))?;
672            return Err(error);
673        }
674        {
675            let mut tx = self.registry.immediate()?;
676            tx.delete_tenant(id, nanos(self.clock.now()))?;
677            self.record(&mut tx, &entry(204))?;
678            commit(tx)?;
679        }
680        // The removal stands with its commit. The Store is closed before
681        // its directory goes; a directory left here, by a crash or a
682        // failed removal, goes at the next start.
683        drop(store);
684        let _ = self.space.remove(id);
685        self.stores().remove(&id);
686        Ok(())
687    }
688
689    /// The tenant's Store, shared by every request to it.
690    pub fn store(&self, id: TenantId) -> Result<Arc<Store>, TenantError> {
691        self.store_or_why(id).map_err(TenantError::from)
692    }
693
694    /// `store`, saying why a Store that did not open did not.
695    pub fn store_or_why(&self, id: TenantId) -> Result<Arc<Store>, OpenFailure> {
696        // The Store is opened under this lock, so it is opened once: a
697        // second open's recovery could remove a write's temporary file.
698        let mut stores = self.stores();
699        match stores.get(&id) {
700            Some(Slot::Open(store)) => return Ok(store.clone()),
701            Some(Slot::Closing) => return Err(TenantError::Closing.into()),
702            None => {}
703        }
704        let store = Arc::new(self.open_store(id)?);
705        stores.insert(id, Slot::Open(store.clone()));
706        Ok(store)
707    }
708
709    /// Opens the tenant's Store under the key its row holds. Its callers
710    /// make sure no other Store of the tenant is open: `store` holds the
711    /// lock, and `remove` has drained the tenant.
712    fn open_store(&self, id: TenantId) -> Result<Store, OpenFailure> {
713        let wrapped = self
714            .registry
715            .snapshot()?
716            .tenant(id)?
717            .ok_or(TenantError::NotFound)?
718            .wrapped;
719        let tenant_key = envelope::unwrap(&self.master, &Context::Tenant { tenant: id }, &wrapped)
720            .map_err(io)?;
721        #[cfg(test)]
722        {
723            let hook = self.hooks.opening.lock().expect("hook").take();
724            if let Some(hook) = hook {
725                hook();
726            }
727        }
728        self.space
729            .open(id, tenant_key, !self.beside, self.clock.clone())
730            .map_err(OpenFailure::from)
731    }
732
733    /// Marks the tenant closing, so no new handle is handed out, and waits
734    /// until every handle given out before is dropped. Handles are plain
735    /// `Arc`s, so the wait watches the count; requests are short and
736    /// removal is rare. A tenant already closing is refused, so two
737    /// removals never both hold its Store.
738    ///
739    /// Returns the tenant's Store if a request had opened it, now held
740    /// only here.
741    fn drain(&self, id: TenantId) -> Result<Option<Store>, TenantError> {
742        let slot = {
743            let mut stores = self.stores();
744            if matches!(stores.get(&id), Some(Slot::Closing)) {
745                return Err(TenantError::Closing);
746            }
747            stores.insert(id, Slot::Closing)
748        };
749        let Some(Slot::Open(mut store)) = slot else {
750            return Ok(None);
751        };
752        loop {
753            match Arc::try_unwrap(store) {
754                Ok(store) => return Ok(Some(store)),
755                Err(shared) => {
756                    store = shared;
757                    std::thread::sleep(std::time::Duration::from_millis(1));
758                }
759            }
760        }
761    }
762
763    fn stores(&self) -> MutexGuard<'_, HashMap<TenantId, Slot>> {
764        self.stores
765            .lock()
766            .expect("no thread panicked holding the stores")
767    }
768
769    /// Makes every audit insert on server.sqlite fail, for tests.
770    #[cfg(test)]
771    pub(crate) fn break_audit(&self) {
772        self.conn()
773            .execute_batch(
774                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
775                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
776            )
777            .expect("trigger");
778    }
779
780    /// server.sqlite's connection, for tests that damage it.
781    #[cfg(test)]
782    pub(crate) fn conn_for_test(&self) -> MutexGuard<'_, LocalConn> {
783        self.conn()
784    }
785
786    /// The id of the tenant's current key, for tests.
787    #[cfg(test)]
788    pub(crate) fn tenant_key_id_for_test(&self, id: TenantId) -> Vec<u8> {
789        let row = self.registry.snapshot().expect("reads").tenant(id);
790        let row = row.expect("reads").expect("the tenant's row");
791        row.tenant_key_id.bytes().to_vec()
792    }
793
794    /// The admin log as `export` writes it, for tests.
795    #[cfg(test)]
796    pub(crate) fn admin_records(&self) -> Vec<serde_json::Value> {
797        crate::audit::testing::exported(&self.conn(), None)
798    }
799
800    /// Marks the tenant closing, as a removal does, for tests that need a
801    /// closing tenant without removing it.
802    #[cfg(test)]
803    pub(crate) fn close(&self, id: TenantId) {
804        self.drain(id).expect("not closing yet");
805    }
806
807    /// A tenant's own retention period, None when it uses the server
808    /// default, read afresh so a change counts from the next call. It is
809    /// read for the server's own use, such as a delete's purge, so nothing
810    /// is recorded.
811    pub fn retention(&self, id: TenantId) -> Result<Option<Retention>, TenantError> {
812        let row = self.registry.snapshot()?.tenant(id)?;
813        stored_retention(row.ok_or(TenantError::NotFound)?.retention)
814    }
815
816    /// Every tenant's id, read for the server's own use, such as the purge
817    /// sweep, so nothing is recorded.
818    pub fn ids(&self) -> Result<Vec<TenantId>, TenantError> {
819        Ok(self
820            .registry
821            .snapshot()?
822            .tenants()?
823            .into_iter()
824            .map(|row| row.id)
825            .collect())
826    }
827
828    /// Writes the admin log after `since` as JSON lines, oldest first.
829    /// Not recorded: reading the log changes nothing.
830    pub fn export_admin(
831        &self,
832        since: Option<i64>,
833        out: &mut dyn std::io::Write,
834    ) -> Result<(), audit::AuditError> {
835        self.registry.export(since.unwrap_or(0), out)
836    }
837
838    /// A page of the admin log as JSON lines, the lines `export_admin`
839    /// writes for the same records. The read is recorded as audit_read in
840    /// the admin log, in the same transaction, after the page is read.
841    pub fn audit_page(
842        &self,
843        who: Actor,
844        query: &audit::AuditQuery,
845    ) -> Result<Vec<u8>, TenantError> {
846        let mut tx = self.registry.immediate()?;
847        let mut out = Vec::new();
848        let read = tx.audit_lines(query.after, Some(query.limit), &mut out);
849        self.record(
850            &mut tx,
851            &Entry {
852                actor: who,
853                action: Action::AuditRead,
854                target: None,
855                version: None,
856                status: if read.is_ok() { 200 } else { 500 },
857            },
858        )?;
859        commit(tx)?;
860        read.map(|()| out).map_err(io)
861    }
862
863    /// Whether server.sqlite answers a query, for the health route.
864    pub fn check(&self) -> Result<(), TenantError> {
865        self.registry.snapshot()?.tenants().map(|_| ())
866    }
867
868    /// Records an event in the admin log, in a transaction of its own.
869    pub(crate) fn record_alone(&self, entry: &Entry) -> Result<(), TenantError> {
870        let mut tx = self.registry.immediate()?;
871        self.record(&mut tx, entry)?;
872        commit(tx)
873    }
874
875    /// Counts an event in the admin log, in a transaction of its own.
876    pub(crate) fn count_admin(&self, entry: &Entry) -> Result<(), TenantError> {
877        let mut tx = self.registry.immediate()?;
878        tx.count(entry, self.clock.now())
879            .map_err(|_| TenantError::AuditFailed)?;
880        commit(tx)
881    }
882
883    fn record(&self, tx: &mut RTx, entry: &Entry) -> Result<(), TenantError> {
884        tx.record(entry, self.clock.now())
885            .map_err(|_| TenantError::AuditFailed)
886    }
887
888    /// The local registry's connection, for tests.
889    #[cfg(test)]
890    fn conn(&self) -> MutexGuard<'_, LocalConn> {
891        self.registry.local_conn()
892    }
893}
894
895/// A tenant's name is a label for people: 1 to 128 characters, none of
896/// them a control character, and no white space at either end, so two
897/// names that look alike are alike.
898fn good_name(name: &str) -> bool {
899    (1..=128).contains(&name.chars().count())
900        && !name.chars().any(char::is_control)
901        && name.trim() == name
902}
903
904/// A registry transaction.
905pub(crate) type RTx<'a> = Box<dyn RegistryTx + 'a>;
906
907pub(crate) fn commit(tx: RTx) -> Result<(), TenantError> {
908    tx.commit().map_err(io)
909}
910
911pub(crate) fn io<E>(_: E) -> TenantError {
912    TenantError::Io
913}
914
915#[cfg(test)]
916mod tests {
917    use std::ffi::OsString;
918
919    use rusqlite::{Connection, OptionalExtension, params};
920
921    use serde_json::Value;
922    use time::macros::datetime;
923
924    use super::*;
925    use crate::audit::testing::exported;
926    use crate::catalog;
927    use crate::catalog::testing::columns_taking_null;
928    use crate::clock::ManualClock;
929    use crate::fs::{CrashFs, Op};
930    use crate::ids::VersionId;
931    use crate::store::testing::vcon;
932    use crate::store::{Caller, Created, StoreError};
933
934    const MARKER: &str = "plain-record-marker-8d21e4";
935    const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
936
937    fn caller() -> Caller {
938        Caller {
939            key: Key::generate().expect("key").id(),
940        }
941    }
942
943    /// What a backup runs at each try, after it copies server.sqlite.
944    pub(super) type RegistryHook = Box<dyn FnMut(&Tenants) + Send>;
945
946    #[derive(Default)]
947    pub(super) struct Hooks {
948        /// Runs once, between a create's directory and catalog and the
949        /// transaction that commits its row.
950        pub(super) between: Mutex<Option<Box<dyn FnOnce() + Send>>>,
951        /// Runs once, as `store` opens a tenant's Store.
952        pub(super) opening: Mutex<Option<Box<dyn FnOnce() + Send>>>,
953        /// Stops a key rotation, as a failure, after this step.
954        pub(crate) rekey_stop: Mutex<Option<super::rekey::RekeyStep>>,
955        /// Runs at each try of a backup, after server.sqlite is copied and
956        /// before the tenants it lists are.
957        pub(super) registry_copied: Mutex<Option<RegistryHook>>,
958    }
959
960    /// A master key kept wrapped, so a test can have it more than once.
961    struct Master {
962        holder: Key,
963        wrapped: Vec<u8>,
964    }
965
966    const CTX: Context = Context::Tenant {
967        tenant: TenantId([0; 16]),
968    };
969
970    impl Master {
971        fn new() -> Master {
972            let holder = Key::generate().expect("key");
973            let wrapped = envelope::wrap(&holder, &CTX, &Key::generate().expect("key"));
974            Master { holder, wrapped }
975        }
976
977        fn key(&self) -> Key {
978            envelope::unwrap(&self.holder, &CTX, &self.wrapped).expect("unwraps")
979        }
980    }
981
982    pub(crate) struct Fixture {
983        pub(crate) dir: tempfile::TempDir,
984        pub(crate) fs: Arc<CrashFs>,
985        pub(crate) clock: Arc<ManualClock>,
986        master: Master,
987        pub(crate) tenants: Tenants,
988    }
989
990    impl Fixture {
991        fn open(&self) -> Result<Tenants, RegistryError> {
992            Tenants::open(
993                self.dir.path(),
994                self.master.key(),
995                self.fs.clone(),
996                self.clock.clone(),
997            )
998        }
999
1000        /// A crash, then a start: what was not synced is lost, and the
1001        /// registry is opened again.
1002        fn restart(&mut self) {
1003            self.fs.crash();
1004            let tenants = self.open().expect("opens again");
1005            drop(std::mem::replace(&mut self.tenants, tenants));
1006        }
1007
1008        pub(crate) fn conn(&self) -> MutexGuard<'_, Connection> {
1009            self.tenants.conn()
1010        }
1011
1012        /// The master key the registry was made under.
1013        pub(crate) fn master_key(&self) -> Key {
1014            self.master.key()
1015        }
1016
1017        pub(crate) fn tenants_dir(&self) -> PathBuf {
1018            self.dir.path().join("tenants")
1019        }
1020
1021        /// The tenant directories, as the filesystem shows them now.
1022        fn dirs(&self) -> Vec<OsString> {
1023            self.fs.list(&self.tenants_dir()).expect("list tenants")
1024        }
1025
1026        fn count(&self, sql: &str) -> i64 {
1027            self.conn().query_row(sql, [], |r| r.get(0)).expect(sql)
1028        }
1029
1030        pub(crate) fn master_wraps(&self) -> i64 {
1031            self.count("SELECT coalesce(sum(wraps), 0) FROM master_key_use")
1032        }
1033
1034        fn admin_log(&self) -> Vec<Value> {
1035            exported(&self.conn(), None)
1036        }
1037
1038        /// The tenant's key, unwrapped from its row.
1039        pub(crate) fn tenant_key(&self, id: TenantId) -> Key {
1040            let wrapped: Vec<u8> = self
1041                .conn()
1042                .query_row(
1043                    "SELECT wrapped_tenant_key FROM tenant WHERE id = ?1",
1044                    [id.0],
1045                    |r| r.get(0),
1046                )
1047                .expect("a row");
1048            envelope::unwrap(
1049                &self.master.key(),
1050                &Context::Tenant { tenant: id },
1051                &wrapped,
1052            )
1053            .expect("unwraps under the master key")
1054        }
1055
1056        /// The tenant's Store, opened as the registry would open it.
1057        pub(crate) fn store(&self, id: TenantId) -> Store {
1058            Store::open(
1059                &self.tenants_dir().join(id.to_string()),
1060                id,
1061                self.tenant_key(id),
1062                self.fs.clone(),
1063                self.clock.clone(),
1064            )
1065            .expect("the store opens")
1066        }
1067
1068        fn has_row(&self, id: TenantId) -> bool {
1069            self.conn()
1070                .query_row("SELECT 1 FROM tenant WHERE id = ?1", [id.0], |_| Ok(()))
1071                .optional()
1072                .expect("query")
1073                .is_some()
1074        }
1075
1076        /// A second connection to server.sqlite, as another writer.
1077        fn other(&self) -> Connection {
1078            let catalog_key = envelope::catalog_key(&self.master.key()).expect("catalog key");
1079            catalog::open(&self.dir.path().join("server.sqlite"), &catalog_key, "")
1080                .expect("second connection")
1081        }
1082    }
1083
1084    pub(crate) fn fixture() -> Fixture {
1085        let dir = tempfile::tempdir().expect("temp dir");
1086        let fs = Arc::new(CrashFs::new(dir.path()));
1087        let clock = Arc::new(ManualClock::at(datetime!(2026-10-23 12:00 UTC)));
1088        let master = Master::new();
1089        let tenants =
1090            Tenants::open(dir.path(), master.key(), fs.clone(), clock.clone()).expect("opens");
1091        Fixture {
1092            dir,
1093            fs,
1094            clock,
1095            master,
1096            tenants,
1097        }
1098    }
1099
1100    #[test]
1101    fn server_sqlite_is_a_catalog_under_the_master_keys_catalog_key() {
1102        let f = fixture();
1103        let conn = f.other();
1104        let tables: Vec<String> = conn
1105            .prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
1106            .expect("prepare")
1107            .query_map([], |r| r.get(0))
1108            .expect("query")
1109            .collect::<Result<_, _>>()
1110            .expect("names");
1111        assert_eq!(
1112            tables,
1113            [
1114                "api_key",
1115                "audit",
1116                "master_key_use",
1117                "sqlite_sequence",
1118                "tenant"
1119            ]
1120        );
1121    }
1122
1123    #[test]
1124    fn an_admin_audit_page_is_export_s_lines_and_records_its_read_after_them() {
1125        let f = fixture();
1126        for name in ["acme", "beta"] {
1127            f.tenants.create(Actor::Cli, name, None).expect("made");
1128        }
1129        let mut before = Vec::new();
1130        f.tenants.export_admin(None, &mut before).expect("export");
1131        let who = Actor::Key(Key::generate().expect("key").id());
1132        let query = |after, limit| audit::AuditQuery { after, limit };
1133        assert_eq!(
1134            f.tenants.audit_page(who, &query(0, 1000)),
1135            Ok(before.clone())
1136        );
1137        let read = exported(&f.conn(), None);
1138        let last = read.last().expect("records");
1139        assert_eq!(last["action"], "audit_read");
1140        assert_eq!(last["status"], 200);
1141        let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
1142        assert_eq!(
1143            f.tenants.audit_page(who, &query(1, 1)),
1144            Ok(lines[1].to_vec())
1145        );
1146        f.conn()
1147            .execute(
1148                "INSERT INTO audit (at, actor, action, status, count)
1149                 VALUES ('2026-10-23T12:00:00Z', 'cli', 'read', 70000, 1)",
1150                [],
1151            )
1152            .expect("damaged");
1153        assert_eq!(
1154            f.tenants.audit_page(who, &query(0, 1000)),
1155            Err(TenantError::Io)
1156        );
1157        let last = exported_last(&f);
1158        assert_eq!(last["action"], "audit_read");
1159        assert_eq!(last["status"], 500);
1160        f.tenants.break_audit();
1161        assert_eq!(
1162            f.tenants.audit_page(who, &query(0, 1000)),
1163            Err(TenantError::AuditFailed)
1164        );
1165    }
1166
1167    /// The admin log's last record, read past a damaged one.
1168    fn exported_last(f: &Fixture) -> Value {
1169        let conn = f.conn();
1170        let (action, status): (String, i64) = conn
1171            .query_row(
1172                "SELECT action, status FROM audit ORDER BY seq DESC LIMIT 1",
1173                [],
1174                |r| Ok((r.get(0)?, r.get(1)?)),
1175            )
1176            .expect("a record");
1177        serde_json::json!({"action": action, "status": status})
1178    }
1179
1180    /// What a registry that does not open says: everything a failure
1181    /// report shows of the error.
1182    fn said(dir: &Path) -> String {
1183        let error = Tenants::open(
1184            dir,
1185            Key::generate().expect("key"),
1186            Arc::new(crate::fs::OsFs),
1187            Arc::new(ManualClock::at(datetime!(2026-10-23 12:00 UTC))),
1188        )
1189        .err()
1190        .expect("it does not open");
1191        format!("{error:?}")
1192    }
1193
1194    /// A server.sqlite that cannot be opened names the file, the step and
1195    /// SQLite's error, not only "Io".
1196    #[test]
1197    fn a_server_sqlite_that_cannot_be_opened_names_the_file_and_the_cause() {
1198        let dir = tempfile::tempdir().expect("temp dir");
1199        std::fs::create_dir(dir.path().join("server.sqlite")).expect("a directory in its place");
1200        let said = said(dir.path());
1201        assert!(said.contains("server.sqlite"), "the file is named: {said}");
1202        assert!(said.contains("OpenCatalog"), "the step is named: {said}");
1203        assert!(
1204            said.contains("unable to open database file"),
1205            "SQLite's own error is kept: {said}"
1206        );
1207    }
1208
1209    /// A tenants/ that cannot be made names the directory and the system's
1210    /// error.
1211    #[test]
1212    fn a_tenants_directory_that_cannot_be_made_names_it_and_the_cause() {
1213        let dir = tempfile::tempdir().expect("temp dir");
1214        std::fs::write(dir.path().join("tenants"), b"").expect("a file in its place");
1215        let said = said(dir.path());
1216        assert!(said.contains("tenants"), "the directory is named: {said}");
1217        assert!(said.contains("MakeTenantsDir"), "the step is named: {said}");
1218        assert!(
1219            said.contains("File exists"),
1220            "the system's error is kept: {said}"
1221        );
1222    }
1223
1224    #[test]
1225    fn a_wrong_master_key_does_not_open_server_sqlite() {
1226        let f = fixture();
1227        let wrong = Tenants::open(
1228            f.dir.path(),
1229            Master::new().key(),
1230            f.fs.clone(),
1231            f.clock.clone(),
1232        );
1233        assert!(
1234            matches!(wrong, Err(RegistryError::WrongKey)),
1235            "a wrong key is told apart from every other failure: {:?}",
1236            wrong.err()
1237        );
1238        assert!(f.open().is_ok(), "the right key still opens it");
1239    }
1240
1241    #[test]
1242    fn only_the_optional_columns_may_be_empty() {
1243        let f = fixture();
1244        let conn = f.conn();
1245        let tenant = [
1246            ("id", "x'11'"),
1247            ("name", "'a'"),
1248            ("retention", "'7y'"),
1249            ("created_at", "1"),
1250            ("tenant_key_id", "x'22'"),
1251            ("wrapped_tenant_key", "x'33'"),
1252            ("pending_wrapped_tenant_key", "x'44'"),
1253            ("key_since", "1"),
1254        ];
1255        let key_use = [("master_key_id", "x'55'"), ("wraps", "1"), ("since", "1")];
1256        let api_key = [
1257            ("id", "x'66'"),
1258            ("tenant", "x'11'"),
1259            ("token_hash", "x'77'"),
1260            ("scopes", "'read'"),
1261            ("label", "'a'"),
1262            ("created_at", "1"),
1263            ("revoked_at", "2"),
1264        ];
1265        let mut may_be_empty = columns_taking_null(&conn, "tenant", &tenant);
1266        may_be_empty.extend(columns_taking_null(&conn, "master_key_use", &key_use));
1267        may_be_empty.extend(columns_taking_null(&conn, "api_key", &api_key));
1268        assert_eq!(
1269            may_be_empty,
1270            [
1271                "tenant.retention",
1272                "tenant.pending_wrapped_tenant_key",
1273                "api_key.tenant",
1274                "api_key.revoked_at"
1275            ]
1276        );
1277    }
1278
1279    #[test]
1280    fn a_name_belongs_to_one_tenant() {
1281        let f = fixture();
1282        let conn = f.conn();
1283        let add = |id: u8, name: &str| {
1284            conn.execute(
1285                "INSERT INTO tenant (id, name, created_at, tenant_key_id,
1286                     wrapped_tenant_key, key_since)
1287                 VALUES (?1, ?2, 1, x'22', x'33', 1)",
1288                params![[id; 16], name],
1289            )
1290        };
1291        add(1, "a").expect("the first tenant");
1292        add(2, "b").expect("another name");
1293        assert!(add(3, "a").is_err(), "a second tenant named a");
1294    }
1295
1296    /// Run while the connection is open, so the commits are still in the
1297    /// WAL.
1298    #[test]
1299    fn no_plain_record_reaches_server_sqlite_or_its_wal() {
1300        let f = fixture();
1301        f.tenants
1302            .create(Actor::Cli, MARKER, Some(Retention::Years(7)))
1303            .expect("a tenant");
1304        let wal = std::fs::metadata(f.dir.path().join("server.sqlite-wal")).expect("a WAL");
1305        assert!(wal.len() > 0, "the commits are in the WAL");
1306        for entry in std::fs::read_dir(f.dir.path()).expect("list") {
1307            let file = entry.expect("entry").path();
1308            if !file.is_file() {
1309                continue;
1310            }
1311            let bytes = std::fs::read(&file).expect("read");
1312            assert!(
1313                !bytes.windows(MARKER.len()).any(|w| w == MARKER.as_bytes()),
1314                "plain record in {}",
1315                file.display()
1316            );
1317        }
1318    }
1319
1320    #[test]
1321    fn a_new_registrys_tenants_directory_survives_a_crash() {
1322        let f = fixture();
1323        f.fs.crash();
1324        assert_eq!(
1325            f.fs.list(f.dir.path()).expect("list"),
1326            [OsString::from("tenants")]
1327        );
1328    }
1329
1330    /// A tenant row's columns, besides its id and wrapped key.
1331    struct Row {
1332        name: String,
1333        retention: Option<String>,
1334        created_at: i64,
1335        key_id: Vec<u8>,
1336        pending: Option<Vec<u8>>,
1337        key_since: i64,
1338    }
1339
1340    #[test]
1341    fn a_tenant_is_made_with_its_key_directory_catalog_and_row() {
1342        let f = fixture();
1343        let id = f
1344            .tenants
1345            .create(Actor::Cli, "acme", Some(Retention::Years(7)))
1346            .expect("made");
1347        f.clock.set(datetime!(2026-10-23 13:00 UTC));
1348        let other = f.tenants.create(Actor::Cli, "lab", None).expect("another");
1349        assert_ne!(id, other, "a new id each time");
1350
1351        let Row {
1352            name,
1353            retention,
1354            created_at,
1355            key_id,
1356            pending,
1357            key_since,
1358        } = f
1359            .conn()
1360            .query_row(
1361                "SELECT name, retention, created_at, tenant_key_id,
1362                     pending_wrapped_tenant_key, key_since
1363                 FROM tenant WHERE id = ?1",
1364                [id.0],
1365                |r| {
1366                    Ok(Row {
1367                        name: r.get(0)?,
1368                        retention: r.get(1)?,
1369                        created_at: r.get(2)?,
1370                        key_id: r.get(3)?,
1371                        pending: r.get(4)?,
1372                        key_since: r.get(5)?,
1373                    })
1374                },
1375            )
1376            .expect("a row");
1377        let now = nanos(datetime!(2026-10-23 12:00 UTC));
1378        assert_eq!(name, "acme");
1379        assert_eq!(retention.as_deref(), Some("7y"));
1380        assert_eq!((created_at, key_since), (now, now));
1381        assert_eq!(pending, None);
1382        let key = f.tenant_key(id);
1383        assert_eq!(key_id, key.id().bytes());
1384        let lab_retention: Option<String> = f
1385            .conn()
1386            .query_row(
1387                "SELECT retention FROM tenant WHERE id = ?1",
1388                [other.0],
1389                |r| r.get(0),
1390            )
1391            .expect("a row");
1392        assert_eq!(lab_retention, None, "no period means the server default");
1393
1394        let mut dirs = vec![
1395            OsString::from(id.to_string()),
1396            OsString::from(other.to_string()),
1397        ];
1398        dirs.sort();
1399        assert_eq!(f.dirs(), dirs);
1400        drop(f.store(id));
1401        let catalog_key = envelope::catalog_key(&key).expect("catalog key");
1402        let tenant_dir = f.tenants_dir().join(id.to_string());
1403        assert!(
1404            catalog::open(&tenant_dir.join("catalog.sqlite"), &catalog_key, "").is_ok(),
1405            "the catalog is under the tenant key's catalog key"
1406        );
1407
1408        let (wraps, since): (i64, i64) = f
1409            .conn()
1410            .query_row(
1411                "SELECT wraps, since FROM master_key_use WHERE master_key_id = ?1",
1412                [f.master.key().id().bytes()],
1413                |r| Ok((r.get(0)?, r.get(1)?)),
1414            )
1415            .expect("the master key's use");
1416        assert_eq!((wraps, since), (2, now));
1417
1418        let log = f.admin_log();
1419        assert_eq!(log.len(), 2);
1420        assert_eq!(log[0]["actor"], "cli");
1421        assert_eq!(log[0]["action"], "tenant_create");
1422        assert_eq!(log[0]["target"], id.to_string());
1423        assert_eq!(log[0]["status"], 201);
1424        assert_eq!(log[0]["at"], "2026-10-23T12:00:00Z");
1425    }
1426
1427    #[test]
1428    fn a_taken_name_is_refused_before_anything_is_made() {
1429        let f = fixture();
1430        f.tenants.create(Actor::Cli, "acme", None).expect("made");
1431        let ops = f.fs.log().len();
1432        assert_eq!(
1433            f.tenants.create(Actor::Cli, "acme", None),
1434            Err(TenantError::NameTaken)
1435        );
1436        assert_eq!(f.fs.log().len(), ops, "nothing made");
1437        assert_eq!(f.master_wraps(), 1, "nothing wrapped");
1438        assert_eq!(f.count("SELECT count(*) FROM tenant"), 1);
1439        let log = f.admin_log();
1440        assert_eq!(log[1]["action"], "tenant_create");
1441        assert_eq!(log[1]["status"], 409);
1442        assert_eq!(log[1]["target"], Value::Null, "no tenant was made");
1443    }
1444
1445    #[test]
1446    fn a_name_is_1_to_128_characters_without_controls_or_outer_spaces() {
1447        let long = "é".repeat(128);
1448        for name in ["acme", "a", "Acme Corp", "Acme — Europe", long.as_str()] {
1449            assert!(good_name(name), "{name:?}");
1450        }
1451        let longer = "é".repeat(129);
1452        for name in [
1453            "",
1454            " acme",
1455            "acme ",
1456            "ac\nme",
1457            "ac\tme",
1458            "acme\u{7f}",
1459            "\u{85}acme",
1460            "acme\u{3000}",
1461            longer.as_str(),
1462        ] {
1463            assert!(!good_name(name), "{name:?}");
1464        }
1465    }
1466
1467    #[test]
1468    fn a_bad_name_is_refused_and_recorded_before_anything_is_made() {
1469        let f = fixture();
1470        let ops = f.fs.log().len();
1471        assert_eq!(
1472            f.tenants.create(Actor::Cli, " acme", None),
1473            Err(TenantError::BadName)
1474        );
1475        assert_eq!(f.fs.log().len(), ops, "nothing made");
1476        assert_eq!(f.master_wraps(), 0, "nothing wrapped");
1477        assert_eq!(f.count("SELECT count(*) FROM tenant"), 0);
1478        let log = f.admin_records("tenant_create");
1479        assert_eq!(log.len(), 1);
1480        assert_eq!(log[0]["status"], 422);
1481        assert_eq!(log[0]["target"], Value::Null, "no tenant was made");
1482    }
1483
1484    #[test]
1485    fn a_master_key_at_its_wrap_limit_makes_no_tenant() {
1486        let f = fixture();
1487        f.conn()
1488            .execute(
1489                "INSERT INTO master_key_use (master_key_id, wraps, since) VALUES (?1, ?2, 1)",
1490                params![f.master.key().id().bytes(), 1_i64 << 32],
1491            )
1492            .expect("at the limit");
1493        let ops = f.fs.log().len();
1494        assert_eq!(
1495            f.tenants.create(Actor::Cli, "acme", None),
1496            Err(TenantError::LimitReached)
1497        );
1498        assert_eq!(f.fs.log().len(), ops, "nothing made");
1499        assert_eq!(f.master_wraps(), 1_i64 << 32, "nothing counted");
1500        assert_eq!(f.count("SELECT count(*) FROM tenant"), 0);
1501        assert_eq!(f.admin_log()[0]["status"], 503);
1502    }
1503
1504    #[test]
1505    fn a_negative_wrap_count_is_damage_and_makes_no_tenant() {
1506        let f = fixture();
1507        f.conn()
1508            .execute(
1509                "INSERT INTO master_key_use (master_key_id, wraps, since) VALUES (?1, -1, 1)",
1510                [f.master.key().id().bytes()],
1511            )
1512            .expect("a damaged count");
1513        let ops = f.fs.log().len();
1514        assert_eq!(
1515            f.tenants.create(Actor::Cli, "acme", None),
1516            Err(TenantError::Io)
1517        );
1518        assert_eq!(f.fs.log().len(), ops, "nothing made");
1519    }
1520
1521    #[test]
1522    fn a_name_taken_while_the_directory_is_made_loses_and_leaves_nothing() {
1523        let f = fixture();
1524        let other = f.other();
1525        *f.tenants.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1526            other
1527                .execute(
1528                    "INSERT INTO tenant (id, name, created_at, tenant_key_id,
1529                         wrapped_tenant_key, key_since)
1530                     VALUES (x'01', 'acme', 1, x'02', x'03', 1)",
1531                    [],
1532                )
1533                .expect("another create wins the name");
1534        }));
1535        assert_eq!(
1536            f.tenants.create(Actor::Cli, "acme", None),
1537            Err(TenantError::NameTaken)
1538        );
1539        assert_eq!(f.dirs(), Vec::<OsString>::new(), "its directory is removed");
1540        assert_eq!(f.master_wraps(), 1, "its wrap is still counted");
1541        let log = f.admin_log();
1542        assert_eq!(log.len(), 1);
1543        assert_eq!(log[0]["status"], 409);
1544    }
1545
1546    #[test]
1547    fn a_create_whose_audit_record_fails_leaves_nothing() {
1548        let f = fixture();
1549        f.conn()
1550            .execute_batch(
1551                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
1552                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
1553            )
1554            .expect("trigger");
1555        assert_eq!(
1556            f.tenants.create(Actor::Cli, "acme", None),
1557            Err(TenantError::AuditFailed)
1558        );
1559        assert_eq!(f.count("SELECT count(*) FROM tenant"), 0);
1560        assert_eq!(f.dirs(), Vec::<OsString>::new(), "its directory is removed");
1561    }
1562
1563    #[test]
1564    fn a_create_whose_row_cannot_be_written_leaves_nothing() {
1565        let f = fixture();
1566        f.conn()
1567            .execute_batch(
1568                "CREATE TEMP TRIGGER no_row BEFORE INSERT ON tenant
1569                 BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
1570            )
1571            .expect("trigger");
1572        assert_eq!(
1573            f.tenants.create(Actor::Cli, "acme", None),
1574            Err(TenantError::Io)
1575        );
1576        assert_eq!(f.count("SELECT count(*) FROM tenant"), 0);
1577        assert_eq!(f.dirs(), Vec::<OsString>::new(), "its directory is removed");
1578    }
1579
1580    #[test]
1581    fn a_create_whose_commit_outcome_is_unknown_keeps_its_directory() {
1582        let f = fixture();
1583        // The wrap count commits first; the second commit, which adds the
1584        // row, is kept and then reported as failed, as a lost connection
1585        // reports a commit the database made.
1586        f.tenants.registry.fail_commit(2, CommitError::Unknown);
1587        assert_eq!(
1588            f.tenants.create(Actor::Cli, "acme", None),
1589            Err(TenantError::Io),
1590            "a_create_whose_commit_outcome_is_unknown_keeps_its_directory: f.tenants.create(Actor::Cli, \"acme\", None)"
1591        );
1592        assert_eq!(
1593            f.count("SELECT count(*) FROM tenant"),
1594            1,
1595            "a_create_whose_commit_outcome_is_unknown_keeps_its_directory: f.count(\"SELECT count(*) FROM tenant\")"
1596        );
1597        assert_eq!(
1598            f.dirs().len(),
1599            1,
1600            "the directory made for the kept row stays"
1601        );
1602        let id = f.tenants.ids().expect("ids")[0];
1603        let store = f.tenants.store(id).expect("the kept tenant opens");
1604        let (found, body) = vcon(UUID, "kept");
1605        store.create(&caller(), &found, &body).expect("stores");
1606    }
1607
1608    #[test]
1609    fn a_create_whose_commit_rolls_back_leaves_nothing() {
1610        let f = fixture();
1611        f.tenants.registry.fail_commit(2, CommitError::RolledBack);
1612        assert_eq!(
1613            f.tenants.create(Actor::Cli, "acme", None),
1614            Err(TenantError::Io),
1615            "a_create_whose_commit_rolls_back_leaves_nothing: f.tenants.create(Actor::Cli, \"acme\", None)"
1616        );
1617        assert_eq!(
1618            f.count("SELECT count(*) FROM tenant"),
1619            0,
1620            "a_create_whose_commit_rolls_back_leaves_nothing: f.count(\"SELECT count(*) FROM tenant\")"
1621        );
1622        assert_eq!(f.dirs(), Vec::<OsString>::new(), "its directory is removed");
1623    }
1624
1625    #[test]
1626    fn a_refusal_whose_audit_record_fails_is_an_audit_failure() {
1627        let f = fixture();
1628        f.tenants.create(Actor::Cli, "acme", None).expect("made");
1629        f.conn()
1630            .execute_batch(
1631                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
1632                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
1633            )
1634            .expect("trigger");
1635        assert_eq!(
1636            f.tenants.create(Actor::Cli, "acme", None),
1637            Err(TenantError::AuditFailed)
1638        );
1639    }
1640
1641    /// The steps of a create that the filesystem sees: the tenant's
1642    /// directory, its objects directory, the sync of the tenant's directory
1643    /// and the sync of `tenants/`. The row's commit follows them.
1644    const CREATE_STEPS: usize = 4;
1645
1646    #[test]
1647    fn a_create_writes_its_directories_in_order() {
1648        let f = fixture();
1649        let ops = f.fs.log().len();
1650        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1651        let dir = f.tenants_dir().join(id.to_string());
1652        assert_eq!(
1653            f.fs.log()[ops..],
1654            [
1655                Op::CreateDir(dir.clone()),
1656                Op::CreateDir(dir.join("objects")),
1657                Op::SyncDir(dir),
1658                Op::SyncDir(f.tenants_dir()),
1659            ]
1660        );
1661        assert_eq!(f.fs.log().len() - ops, CREATE_STEPS);
1662    }
1663
1664    #[test]
1665    fn a_create_cut_off_at_any_step_leaves_a_whole_tenant_or_none() {
1666        for step in 0..=CREATE_STEPS {
1667            let mut f = fixture();
1668            f.fs.fail_from(f.fs.log().len() + step);
1669            let result = f.tenants.create(Actor::Cli, "acme", None);
1670            assert_eq!(
1671                result.is_ok(),
1672                step == CREATE_STEPS,
1673                "step {step}: {result:?}"
1674            );
1675            f.restart();
1676            match result {
1677                Ok(id) => {
1678                    assert!(f.has_row(id), "step {step}");
1679                    assert_eq!(f.dirs(), [OsString::from(id.to_string())]);
1680                    let store = f.store(id);
1681                    drop(store);
1682                }
1683                Err(_) => {
1684                    assert_eq!(f.count("SELECT count(*) FROM tenant"), 0, "step {step}");
1685                    assert_eq!(f.dirs(), Vec::<OsString>::new(), "step {step}");
1686                }
1687            }
1688        }
1689    }
1690
1691    #[test]
1692    fn a_create_cut_off_before_its_commit_leaves_no_tenant() {
1693        let mut f = fixture();
1694        let other = f.other();
1695        let fs = f.fs.clone();
1696        *f.tenants.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1697            fs.fail_from(0);
1698            other
1699                .execute_batch(
1700                    "CREATE TRIGGER dead BEFORE INSERT ON audit
1701                     BEGIN SELECT RAISE(ABORT, 'the process has died'); END;",
1702                )
1703                .expect("trigger");
1704        }));
1705        assert!(f.tenants.create(Actor::Cli, "acme", None).is_err());
1706        assert_eq!(f.dirs().len(), 1, "the dead process removed nothing");
1707        f.conn()
1708            .execute_batch("DROP TRIGGER dead")
1709            .expect("drop trigger");
1710        f.restart();
1711        assert_eq!(f.count("SELECT count(*) FROM tenant"), 0);
1712        assert_eq!(f.dirs(), Vec::<OsString>::new(), "removed at start");
1713    }
1714
1715    #[test]
1716    fn opening_removes_only_tenant_directories_with_no_row() {
1717        let mut f = fixture();
1718        let kept = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1719        let orphan = TenantId::generate();
1720        let tenants = f.tenants_dir();
1721        for name in [orphan.to_string(), "notes".to_owned()] {
1722            f.fs.create_dir_all(&tenants.join(name))
1723                .expect("a directory");
1724        }
1725        f.fs.sync_dir(&tenants).expect("sync");
1726        f.restart();
1727        assert_eq!(
1728            f.dirs(),
1729            [OsString::from(kept.to_string()), OsString::from("notes")]
1730        );
1731        drop(f.store(kept));
1732    }
1733
1734    /// The server's registry opens each tenant's Store with recovery, so a
1735    /// crash's leftovers in it go once the tenant is next used.
1736    #[test]
1737    fn a_servers_registry_opens_each_store_with_recovery() {
1738        let f = fixture();
1739        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1740        let objects = f.tenants_dir().join(id.to_string()).join("objects");
1741        let leftover = format!("{}.tmp", VersionId([0x0e; 16]));
1742        f.fs.write_new(&objects.join(&leftover), b"x")
1743            .expect("write");
1744        let tenants = f.open().expect("opens again");
1745        drop(tenants.store(id).expect("its store"));
1746        assert!(
1747            !f.fs
1748                .list(&objects)
1749                .expect("list")
1750                .contains(&OsString::from(leftover)),
1751            "the leftover is removed"
1752        );
1753    }
1754
1755    /// A command beside a running server must not take a create or a
1756    /// write the server has in progress for a crash's leftovers, in the
1757    /// registry or in a tenant's store.
1758    #[test]
1759    fn opening_beside_a_server_removes_nothing() {
1760        let f = fixture();
1761        let kept = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1762        let orphan = TenantId::generate();
1763        let tenants = f.tenants_dir();
1764        f.fs.create_dir_all(&tenants.join(orphan.to_string()))
1765            .expect("a directory");
1766        f.fs.sync_dir(&tenants).expect("sync");
1767        let in_progress = tenants
1768            .join(kept.to_string())
1769            .join("objects")
1770            .join(format!("{}.tmp", VersionId([0x0e; 16])));
1771        f.fs.write_new(&in_progress, b"x").expect("write");
1772        let beside =
1773            Tenants::open_beside(f.dir.path(), f.master.key(), f.fs.clone(), f.clock.clone())
1774                .expect("opens beside");
1775        let mut expected = [
1776            OsString::from(kept.to_string()),
1777            OsString::from(orphan.to_string()),
1778        ];
1779        expected.sort();
1780        assert_eq!(f.dirs(), expected);
1781        drop(beside.store(kept).expect("its store"));
1782        assert!(
1783            f.fs.list(in_progress.parent().expect("objects"))
1784                .expect("list")
1785                .contains(&OsString::from(in_progress.file_name().expect("name"))),
1786            "the write in progress stays"
1787        );
1788    }
1789
1790    #[test]
1791    fn an_unknown_tenant_has_no_store() {
1792        let f = fixture();
1793        assert_eq!(
1794            f.tenants.store(TenantId::generate()).err(),
1795            Some(TenantError::NotFound)
1796        );
1797    }
1798
1799    #[test]
1800    fn every_request_to_a_tenant_shares_one_store() {
1801        let f = fixture();
1802        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1803        let a = f.tenants.store(id).expect("a store");
1804        let b = f.tenants.store(id).expect("the same store");
1805        assert!(Arc::ptr_eq(&a, &b));
1806        let (found, body) = vcon(UUID, "first");
1807        a.create(&caller(), &found, &body).expect("stored");
1808        assert_eq!(b.get(&caller(), found.uuid).expect("read").body, body);
1809    }
1810
1811    /// The first request opens the Store; one arriving meanwhile waits for
1812    /// it rather than opening a second.
1813    #[test]
1814    fn a_store_is_opened_once_even_by_requests_at_the_same_time() {
1815        let f = fixture();
1816        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1817        let (opening, is_opening) = std::sync::mpsc::channel();
1818        let (go, wait) = std::sync::mpsc::channel::<()>();
1819        *f.tenants.hooks.opening.lock().expect("hook") = Some(Box::new(move || {
1820            opening.send(()).expect("send");
1821            wait.recv_timeout(std::time::Duration::from_secs(30))
1822                .expect("go");
1823        }));
1824        // Everything the threads wait on is moved into the scope, so a
1825        // failed assertion drops it and the threads end instead of hanging.
1826        let tenants = &f.tenants;
1827        std::thread::scope(move |s| {
1828            let first = s.spawn(move || tenants.store(id).expect("the first"));
1829            is_opening
1830                .recv_timeout(std::time::Duration::from_secs(10))
1831                .expect("the first is opening");
1832            let (second_done, second) = std::sync::mpsc::channel();
1833            s.spawn(move || {
1834                second_done
1835                    .send(tenants.store(id).expect("the second"))
1836                    .expect("send");
1837            });
1838            assert!(
1839                second
1840                    .recv_timeout(std::time::Duration::from_millis(100))
1841                    .is_err(),
1842                "the second waits while the first opens"
1843            );
1844            go.send(()).expect("go");
1845            let first = first.join().expect("the first returns");
1846            let second = second
1847                .recv_timeout(std::time::Duration::from_secs(30))
1848                .expect("the second returns");
1849            assert!(Arc::ptr_eq(&first, &second));
1850        });
1851    }
1852
1853    #[test]
1854    fn a_tenant_key_that_does_not_unwrap_opens_no_store() {
1855        let f = fixture();
1856        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1857        f.conn()
1858            .execute(
1859                "UPDATE tenant SET wrapped_tenant_key = x'00' WHERE id = ?1",
1860                [id.0],
1861            )
1862            .expect("damage the wrapped key");
1863        assert_eq!(f.tenants.store(id).err(), Some(TenantError::Io));
1864    }
1865
1866    #[test]
1867    fn draining_refuses_new_requests_and_waits_for_those_in_flight() {
1868        let f = fixture();
1869        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1870        let (found, body) = vcon(UUID, "first");
1871        let held = f.tenants.store(id).expect("a request in flight");
1872        std::thread::scope(|s| {
1873            let (done, drained) = std::sync::mpsc::channel();
1874            let tenants = &f.tenants;
1875            s.spawn(move || {
1876                tenants.drain(id).expect("drains");
1877                done.send(()).expect("send");
1878            });
1879            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1880            while f.tenants.store(id).is_ok() {
1881                assert!(std::time::Instant::now() < deadline, "never closing");
1882                std::thread::yield_now();
1883            }
1884            assert_eq!(f.tenants.store(id).err(), Some(TenantError::Closing));
1885            assert!(
1886                drained
1887                    .recv_timeout(std::time::Duration::from_millis(100))
1888                    .is_err(),
1889                "the drain waits for the request in flight"
1890            );
1891            held.create(&caller(), &found, &body)
1892                .expect("the request in flight finishes");
1893            drop(held);
1894            drained
1895                .recv_timeout(std::time::Duration::from_secs(30))
1896                .expect("the drain ends once the last handle is dropped");
1897        });
1898        assert_eq!(f.tenants.store(id).err(), Some(TenantError::Closing));
1899    }
1900
1901    #[test]
1902    fn a_tenant_no_request_opened_drains_at_once() {
1903        let f = fixture();
1904        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1905        f.tenants.drain(id).expect("drains");
1906        assert_eq!(f.tenants.store(id).err(), Some(TenantError::Closing));
1907    }
1908
1909    impl Fixture {
1910        /// The tenant's own retention period, as its row holds it.
1911        fn own_retention(&self, id: TenantId) -> Option<String> {
1912            self.conn()
1913                .query_row("SELECT retention FROM tenant WHERE id = ?1", [id.0], |r| {
1914                    r.get(0)
1915                })
1916                .expect("a row")
1917        }
1918
1919        /// A second connection to the tenant's catalog.
1920        pub(crate) fn tenant_catalog(&self, id: TenantId) -> Connection {
1921            let catalog_key = envelope::catalog_key(&self.tenant_key(id)).expect("catalog key");
1922            catalog::open(
1923                &self
1924                    .tenants_dir()
1925                    .join(id.to_string())
1926                    .join("catalog.sqlite"),
1927                &catalog_key,
1928                "",
1929            )
1930            .expect("the tenant's catalog")
1931        }
1932
1933        /// The records with this action in the admin log.
1934        pub(crate) fn admin_records(&self, action: &str) -> Vec<Value> {
1935            self.admin_log()
1936                .into_iter()
1937                .filter(|r| r["action"] == action)
1938                .collect()
1939        }
1940    }
1941
1942    /// The rule the sweep and `check` both apply.
1943    #[test]
1944    fn retention_falls_back_to_the_server_default_when_unset() {
1945        let info = |retention| TenantInfo {
1946            id: TenantId([1; 16]),
1947            name: "acme".to_owned(),
1948            retention,
1949            metadata: Metadata::None,
1950            created_at: datetime!(2026-10-23 12:00 UTC),
1951        };
1952        assert_eq!(
1953            info(None).kept_for(Retention::Years(7)),
1954            Retention::Years(7)
1955        );
1956        assert_eq!(
1957            info(Some(Retention::Days(90))).kept_for(Retention::Years(7)),
1958            Retention::Days(90)
1959        );
1960    }
1961
1962    #[test]
1963    fn a_tenant_retention_is_read_afresh_and_not_recorded() {
1964        let f = fixture();
1965        let lab = f
1966            .tenants
1967            .create(Actor::Cli, "lab", Some(Retention::Months(18)))
1968            .expect("made");
1969        let acme = f.tenants.create(Actor::Cli, "acme", None).expect("made");
1970        let records = f.tenants.admin_records().len();
1971
1972        assert_eq!(f.tenants.retention(lab), Ok(Some(Retention::Months(18))));
1973        assert_eq!(f.tenants.retention(acme), Ok(None));
1974        f.tenants
1975            .set_retention(Actor::Cli, acme, Some(Retention::Days(0)))
1976            .expect("changed");
1977        assert_eq!(f.tenants.retention(acme), Ok(Some(Retention::Days(0))));
1978        assert_eq!(
1979            f.tenants.retention(TenantId([0xee; 16])),
1980            Err(TenantError::NotFound)
1981        );
1982        assert_eq!(
1983            f.tenants.admin_records().len(),
1984            records + 1,
1985            "only the retention change is recorded"
1986        );
1987        f.tenants
1988            .conn_for_test()
1989            .execute("UPDATE tenant SET retention = '7w' WHERE id = ?1", [lab.0])
1990            .expect("damaged");
1991        assert_eq!(
1992            f.tenants.retention(lab),
1993            Err(TenantError::Io),
1994            "a period that does not read back is damage"
1995        );
1996    }
1997
1998    #[test]
1999    fn ids_names_every_tenant_and_records_nothing() {
2000        let f = fixture();
2001        let a = f.tenants.create(Actor::Cli, "a", None).expect("made");
2002        let b = f.tenants.create(Actor::Cli, "b", None).expect("made");
2003        let records = f.tenants.admin_records().len();
2004
2005        let mut ids = f.tenants.ids().expect("ids");
2006        ids.sort_by_key(|id| id.0);
2007        let mut made = vec![a, b];
2008        made.sort_by_key(|id| id.0);
2009
2010        assert_eq!(ids, made);
2011        assert_eq!(f.tenants.admin_records().len(), records, "nothing recorded");
2012    }
2013
2014    #[test]
2015    fn list_shows_every_tenant_by_name_and_is_recorded() {
2016        let f = fixture();
2017        assert_eq!(f.tenants.list(Actor::Cli), Ok(Vec::new()));
2018        let lab = f
2019            .tenants
2020            .create(Actor::Cli, "lab", Some(Retention::Days(90)))
2021            .expect("made");
2022        f.clock.set(datetime!(2026-10-23 13:00 UTC));
2023        let acme = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2024        assert_eq!(
2025            f.tenants.list(Actor::Cli),
2026            Ok(vec![
2027                TenantInfo {
2028                    id: acme,
2029                    name: "acme".to_owned(),
2030                    retention: None,
2031                    metadata: Metadata::None,
2032                    created_at: datetime!(2026-10-23 13:00 UTC),
2033                },
2034                TenantInfo {
2035                    id: lab,
2036                    name: "lab".to_owned(),
2037                    retention: Some(Retention::Days(90)),
2038                    metadata: Metadata::None,
2039                    created_at: datetime!(2026-10-23 12:00 UTC),
2040                },
2041            ])
2042        );
2043        let lists = f.admin_records("tenant_list");
2044        assert_eq!(lists.len(), 2, "one record per list");
2045        for record in lists {
2046            assert_eq!(record["actor"], "cli");
2047            assert_eq!(record["status"], 200);
2048            assert_eq!(record["target"], Value::Null);
2049        }
2050    }
2051
2052    #[test]
2053    fn a_stored_period_that_does_not_read_back_is_damage() {
2054        let f = fixture();
2055        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2056        f.conn()
2057            .execute("UPDATE tenant SET retention = '7w' WHERE id = ?1", [id.0])
2058            .expect("damage the period");
2059        assert_eq!(f.tenants.list(Actor::Cli), Err(TenantError::Io));
2060    }
2061
2062    #[test]
2063    fn a_list_whose_record_fails_shows_nothing() {
2064        let f = fixture();
2065        f.tenants.create(Actor::Cli, "acme", None).expect("made");
2066        f.conn()
2067            .execute_batch(
2068                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
2069                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
2070            )
2071            .expect("trigger");
2072        assert_eq!(f.tenants.list(Actor::Cli), Err(TenantError::AuditFailed));
2073    }
2074
2075    #[test]
2076    fn a_retention_change_is_kept_and_recorded_in_both_logs() {
2077        let f = fixture();
2078        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2079        f.clock.set(datetime!(2026-10-23 13:00 UTC));
2080        f.tenants
2081            .set_retention(Actor::Cli, id, Some(Retention::Months(18)))
2082            .expect("set");
2083        assert_eq!(f.own_retention(id).as_deref(), Some("18m"));
2084        f.tenants
2085            .set_retention(Actor::Cli, id, None)
2086            .expect("back to the default");
2087        assert_eq!(f.own_retention(id), None);
2088
2089        let admin = f.admin_records("tenant_retention");
2090        assert_eq!(admin.len(), 2);
2091        for record in &admin {
2092            assert_eq!(record["actor"], "cli");
2093            assert_eq!(record["target"], id.to_string());
2094            assert_eq!(record["status"], 200);
2095            assert_eq!(record["at"], "2026-10-23T13:00:00Z");
2096        }
2097        let copies: Vec<Value> = exported(&f.tenant_catalog(id), None)
2098            .into_iter()
2099            .filter(|r| r["action"] == "retention")
2100            .collect();
2101        assert_eq!(copies.len(), 2, "a copy in the tenant's log for each");
2102        for record in &copies {
2103            assert_eq!(record["actor"], "cli");
2104            assert_eq!(record["target"], id.to_string());
2105            assert_eq!(record["status"], 200);
2106        }
2107    }
2108
2109    impl Fixture {
2110        /// The tenant's metadata level, as its row holds it.
2111        fn own_metadata(&self, id: TenantId) -> Option<String> {
2112            self.conn()
2113                .query_row("SELECT metadata FROM tenant WHERE id = ?1", [id.0], |r| {
2114                    r.get(0)
2115                })
2116                .expect("a row")
2117        }
2118    }
2119
2120    /// A tenant's level is `none` until set; a change is kept, shown in
2121    /// the list, and recorded in the admin log and the tenant's own.
2122    #[test]
2123    fn a_metadata_level_is_none_until_set_and_each_change_is_recorded() {
2124        let f = fixture();
2125        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2126        assert_eq!(
2127            f.tenants.metadata(id),
2128            Ok(Metadata::None),
2129            "a new tenant is at none"
2130        );
2131        assert_eq!(f.own_metadata(id), None, "NULL is none");
2132        f.clock.set(datetime!(2026-10-23 13:00 UTC));
2133        f.tenants
2134            .set_metadata(Actor::Cli, id, Metadata::Full)
2135            .expect("set");
2136        assert_eq!(f.tenants.metadata(id), Ok(Metadata::Full), "the level set");
2137        assert_eq!(
2138            f.own_metadata(id).as_deref(),
2139            Some("full"),
2140            "the row holds the level's name"
2141        );
2142        let listed = f.tenants.list(Actor::Cli).expect("lists");
2143        assert_eq!(listed[0].to_json()["metadata"], "full", "shown in the list");
2144        f.tenants
2145            .set_metadata(Actor::Cli, id, Metadata::None)
2146            .expect("back to none");
2147        assert_eq!(f.own_metadata(id), None, "none is NULL again");
2148        assert_eq!(f.tenants.metadata(id), Ok(Metadata::None), "back to none");
2149
2150        let admin = f.admin_records("tenant_metadata");
2151        assert_eq!(admin.len(), 2, "one admin record per change");
2152        for record in &admin {
2153            assert_eq!(record["actor"], "cli", "the command line made the change");
2154            assert_eq!(
2155                record["target"],
2156                id.to_string(),
2157                "the record names the tenant"
2158            );
2159            assert_eq!(record["status"], 200, "the change succeeded");
2160            assert_eq!(record["at"], "2026-10-23T13:00:00Z", "at the clock's time");
2161        }
2162        let copies: Vec<Value> = exported(&f.tenant_catalog(id), None)
2163            .into_iter()
2164            .filter(|r| r["action"] == "metadata")
2165            .collect();
2166        assert_eq!(copies.len(), 2, "a copy in the tenant's log for each");
2167    }
2168
2169    #[test]
2170    fn a_metadata_change_for_an_unknown_or_closing_tenant_is_refused() {
2171        let f = fixture();
2172        let unknown = TenantId::generate();
2173        assert_eq!(
2174            f.tenants
2175                .set_metadata(Actor::Cli, unknown, Metadata::Counts),
2176            Err(TenantError::NotFound),
2177            "no tenant has the id"
2178        );
2179        assert_eq!(
2180            f.tenants.metadata(unknown),
2181            Err(TenantError::NotFound),
2182            "nor a level"
2183        );
2184        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2185        f.tenants.drain(id).expect("drains");
2186        assert_eq!(
2187            f.tenants.set_metadata(Actor::Cli, id, Metadata::Counts),
2188            Err(TenantError::Closing),
2189            "a closing tenant's level does not change"
2190        );
2191        assert_eq!(f.own_metadata(id), None, "unchanged");
2192        let statuses: Vec<Value> = f
2193            .admin_records("tenant_metadata")
2194            .iter()
2195            .map(|r| r["status"].clone())
2196            .collect();
2197        assert_eq!(
2198            statuses,
2199            [404, 503],
2200            "both refusals are recorded, each with its status"
2201        );
2202    }
2203
2204    /// A level this build does not know is damage, never taken as `none`.
2205    #[test]
2206    fn a_stored_metadata_level_this_build_does_not_know_is_an_error() {
2207        let f = fixture();
2208        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2209        f.conn()
2210            .execute("UPDATE tenant SET metadata = 'most' WHERE id = ?1", [id.0])
2211            .expect("update");
2212        assert_eq!(
2213            f.tenants.metadata(id),
2214            Err(TenantError::Io),
2215            "an unknown level is an error"
2216        );
2217    }
2218
2219    /// A registry made at schema version 2 gains the column at version 3,
2220    /// every tenant at `none`; one made at version 1 does too.
2221    #[test]
2222    fn an_older_registry_gains_the_metadata_column() {
2223        for from in [1, 2] {
2224            let mut f = fixture();
2225            let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2226            f.conn()
2227                .execute_batch(&format!(
2228                    "ALTER TABLE tenant DROP COLUMN metadata; PRAGMA user_version = {from};"
2229                ))
2230                .expect("back to the old schema");
2231            f.restart();
2232            let version: i64 = f
2233                .conn()
2234                .pragma_query_value(None, "user_version", |r| r.get(0))
2235                .expect("version");
2236            assert_eq!(version, catalog::SCHEMA_VERSION, "from {from}");
2237            assert_eq!(f.tenants.metadata(id), Ok(Metadata::None), "from {from}");
2238            f.tenants
2239                .set_metadata(Actor::Cli, id, Metadata::Counts)
2240                .expect("the column is there");
2241        }
2242    }
2243
2244    #[test]
2245    fn a_retention_change_for_an_unknown_tenant_is_not_found() {
2246        let f = fixture();
2247        let id = TenantId::generate();
2248        assert_eq!(
2249            f.tenants
2250                .set_retention(Actor::Cli, id, Some(Retention::Years(1))),
2251            Err(TenantError::NotFound)
2252        );
2253        let admin = f.admin_records("tenant_retention");
2254        assert_eq!(admin.len(), 1);
2255        assert_eq!(admin[0]["status"], 404);
2256        assert_eq!(admin[0]["target"], id.to_string());
2257    }
2258
2259    #[test]
2260    fn a_retention_change_for_a_closing_tenant_is_refused() {
2261        let f = fixture();
2262        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2263        f.tenants.drain(id).expect("drains");
2264        assert_eq!(
2265            f.tenants
2266                .set_retention(Actor::Cli, id, Some(Retention::Years(1))),
2267            Err(TenantError::Closing)
2268        );
2269        assert_eq!(f.own_retention(id), None, "unchanged");
2270        assert_eq!(f.admin_records("tenant_retention")[0]["status"], 503);
2271    }
2272
2273    #[test]
2274    fn a_retention_change_whose_admin_record_fails_changes_nothing() {
2275        let f = fixture();
2276        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2277        f.conn()
2278            .execute_batch(
2279                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
2280                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
2281            )
2282            .expect("trigger");
2283        assert_eq!(
2284            f.tenants
2285                .set_retention(Actor::Cli, id, Some(Retention::Years(1))),
2286            Err(TenantError::AuditFailed)
2287        );
2288        assert_eq!(f.own_retention(id), None, "unchanged");
2289        let copies = exported(&f.tenant_catalog(id), None)
2290            .into_iter()
2291            .filter(|r| r["action"] == "retention")
2292            .count();
2293        assert_eq!(copies, 0, "no copy of a change that was not made");
2294    }
2295
2296    /// The change commits with its admin record; a failed copy in the
2297    /// tenant's log leaves the change standing and answers AuditFailed.
2298    #[test]
2299    fn a_retention_change_whose_tenant_copy_fails_stands() {
2300        let f = fixture();
2301        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2302        f.tenant_catalog(id)
2303            .execute_batch(
2304                "CREATE TRIGGER dead BEFORE INSERT ON audit
2305                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
2306            )
2307            .expect("trigger");
2308        assert_eq!(
2309            f.tenants
2310                .set_retention(Actor::Cli, id, Some(Retention::Years(10))),
2311            Err(TenantError::AuditFailed)
2312        );
2313        assert_eq!(
2314            f.own_retention(id).as_deref(),
2315            Some("10y"),
2316            "the change stands"
2317        );
2318        assert_eq!(f.admin_records("tenant_retention")[0]["status"], 200);
2319    }
2320
2321    /// A tenant with one vCon stored.
2322    fn with_vcon(f: &Fixture) -> TenantId {
2323        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2324        let (found, body) = vcon(UUID, "first");
2325        f.tenants
2326            .store(id)
2327            .expect("its store")
2328            .create(&caller(), &found, &body)
2329            .expect("stored");
2330        id
2331    }
2332
2333    fn hold(f: &Fixture, id: TenantId) {
2334        f.tenant_catalog(id)
2335            .execute("UPDATE vcon SET hold = 1", [])
2336            .expect("a hold");
2337    }
2338
2339    /// Waits until the tenant refuses new requests.
2340    fn until_closing(f: &Fixture, id: TenantId) {
2341        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2342        while f.tenants.store(id).is_ok() {
2343            assert!(std::time::Instant::now() < deadline, "never closing");
2344            std::thread::yield_now();
2345        }
2346    }
2347
2348    #[test]
2349    fn a_tenant_already_closing_cannot_be_drained_again() {
2350        let f = fixture();
2351        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2352        f.tenants.drain(id).expect("drains");
2353        assert_eq!(f.tenants.drain(id).err(), Some(TenantError::Closing));
2354    }
2355
2356    #[test]
2357    fn removing_an_unknown_tenant_is_not_found() {
2358        let f = fixture();
2359        let id = TenantId::generate();
2360        assert_eq!(
2361            f.tenants.remove(Actor::Cli, id, Removal::Empty),
2362            Err(TenantError::NotFound)
2363        );
2364        assert_eq!(f.tenants.store(id).err(), Some(TenantError::NotFound));
2365        let records = f.admin_records("tenant_remove");
2366        assert_eq!(records.len(), 1);
2367        assert_eq!(records[0]["status"], 404);
2368        assert_eq!(records[0]["target"], id.to_string());
2369    }
2370
2371    #[test]
2372    fn an_empty_tenant_is_removed_with_its_key_directory_and_row() {
2373        let f = fixture();
2374        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2375        drop(f.tenants.store(id).expect("opened by a request"));
2376        f.clock.set(datetime!(2026-10-23 13:00 UTC));
2377        f.tenants
2378            .remove(Actor::Cli, id, Removal::Empty)
2379            .expect("removed");
2380        assert!(!f.has_row(id), "its row and wrapped key are gone");
2381        assert_eq!(f.tenants.store(id).err(), Some(TenantError::NotFound));
2382        assert_eq!(f.dirs(), Vec::<OsString>::new(), "its directory is gone");
2383        assert_eq!(f.tenants.list(Actor::Cli), Ok(Vec::new()));
2384        let records = f.admin_records("tenant_remove");
2385        assert_eq!(records.len(), 1);
2386        assert_eq!(records[0]["actor"], "cli");
2387        assert_eq!(records[0]["target"], id.to_string());
2388        assert_eq!(records[0]["status"], 204);
2389        assert_eq!(records[0]["at"], "2026-10-23T13:00:00Z");
2390    }
2391
2392    #[test]
2393    fn an_empty_removal_is_refused_while_any_version_is_kept() {
2394        let f = fixture();
2395        let id = with_vcon(&f);
2396        let uuid = vcon(UUID, "first").0.uuid;
2397        f.tenants
2398            .store(id)
2399            .expect("its store")
2400            .delete(&caller(), uuid, Retention::Years(7))
2401            .expect("deleted, so only a hidden version is kept");
2402        assert_eq!(
2403            f.tenants.remove(Actor::Cli, id, Removal::Empty),
2404            Err(TenantError::HoldsVcons)
2405        );
2406        assert!(f.has_row(id));
2407        let store = f.tenants.store(id).expect("still served");
2408        assert_eq!(store.counts().map(|c| c.versions), Ok(1));
2409        assert_eq!(f.admin_records("tenant_remove")[0]["status"], 409);
2410    }
2411
2412    #[test]
2413    fn a_forced_removal_takes_the_vcons_too() {
2414        let f = fixture();
2415        let id = with_vcon(&f);
2416        f.tenants
2417            .remove(Actor::Cli, id, Removal::Force)
2418            .expect("removed");
2419        assert!(!f.has_row(id));
2420        assert_eq!(f.dirs(), Vec::<OsString>::new());
2421    }
2422
2423    #[test]
2424    fn a_forced_removal_is_refused_while_a_uuid_is_held() {
2425        let f = fixture();
2426        let id = with_vcon(&f);
2427        hold(&f, id);
2428        assert_eq!(
2429            f.tenants.remove(Actor::Cli, id, Removal::Force),
2430            Err(TenantError::UnderHold)
2431        );
2432        assert!(f.has_row(id));
2433        assert!(f.tenants.store(id).is_ok(), "still served");
2434        assert_eq!(f.admin_records("tenant_remove")[0]["status"], 409);
2435    }
2436
2437    #[test]
2438    fn overriding_holds_removes_a_held_tenant() {
2439        let f = fixture();
2440        let id = with_vcon(&f);
2441        hold(&f, id);
2442        f.tenants
2443            .remove(Actor::Cli, id, Removal::ForceOverridingHolds)
2444            .expect("removed");
2445        assert!(!f.has_row(id));
2446        assert_eq!(f.dirs(), Vec::<OsString>::new());
2447    }
2448
2449    /// The removal drains before it looks, so a write that lands meanwhile
2450    /// makes the tenant non-empty and the removal is refused: the write
2451    /// is never acknowledged and then lost.
2452    #[test]
2453    fn a_write_in_flight_during_an_empty_removal_is_kept() {
2454        let f = fixture();
2455        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2456        let (found, body) = vcon(UUID, "first");
2457        let held = f.tenants.store(id).expect("a request in flight");
2458        let tenants = &f.tenants;
2459        let fx = &f;
2460        let (found_ref, body_ref) = (&found, &body);
2461        let result = std::thread::scope(move |s| {
2462            let (done, removed) = std::sync::mpsc::channel();
2463            s.spawn(move || {
2464                done.send(tenants.remove(Actor::Cli, id, Removal::Empty))
2465                    .expect("send");
2466            });
2467            until_closing(fx, id);
2468            assert!(
2469                removed
2470                    .recv_timeout(std::time::Duration::from_millis(100))
2471                    .is_err(),
2472                "the removal waits for the write"
2473            );
2474            held.create(&caller(), found_ref, body_ref)
2475                .expect("the write is acknowledged");
2476            drop(held);
2477            removed
2478                .recv_timeout(std::time::Duration::from_secs(30))
2479                .expect("the removal ends")
2480        });
2481        assert_eq!(result, Err(TenantError::HoldsVcons));
2482        assert_eq!(
2483            f.tenants
2484                .store(id)
2485                .expect("still served")
2486                .get(&caller(), found.uuid)
2487                .expect("the write is kept")
2488                .body,
2489            body
2490        );
2491    }
2492
2493    #[test]
2494    fn a_second_removal_while_one_drains_is_refused() {
2495        let f = fixture();
2496        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2497        let held = f.tenants.store(id).expect("a request in flight");
2498        let tenants = &f.tenants;
2499        let fx = &f;
2500        std::thread::scope(move |s| {
2501            let first = s.spawn(move || tenants.remove(Actor::Cli, id, Removal::Force));
2502            until_closing(fx, id);
2503            assert_eq!(
2504                fx.tenants.remove(Actor::Cli, id, Removal::Empty),
2505                Err(TenantError::Closing)
2506            );
2507            drop(held);
2508            assert_eq!(first.join().expect("the first returns"), Ok(()));
2509        });
2510        let statuses: Vec<Value> = f
2511            .admin_records("tenant_remove")
2512            .into_iter()
2513            .map(|r| r["status"].clone())
2514            .collect();
2515        assert_eq!(statuses, [503, 204]);
2516    }
2517
2518    #[test]
2519    fn a_removal_whose_record_fails_removes_nothing() {
2520        let f = fixture();
2521        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2522        f.conn()
2523            .execute_batch(
2524                "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
2525                 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
2526            )
2527            .expect("trigger");
2528        assert_eq!(
2529            f.tenants.remove(Actor::Cli, id, Removal::Empty),
2530            Err(TenantError::AuditFailed)
2531        );
2532        assert!(f.has_row(id));
2533        assert_eq!(f.dirs().len(), 1);
2534        assert!(f.tenants.store(id).is_ok(), "served again");
2535    }
2536
2537    /// The row's commit is the removal; a directory a crash leaves after
2538    /// it goes at the next start.
2539    #[test]
2540    fn a_removal_cut_off_after_its_commit_finishes_at_the_next_start() {
2541        let mut f = fixture();
2542        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2543        drop(f.tenants.store(id).expect("opened by a request"));
2544        f.fs.fail_from(f.fs.log().len());
2545        assert_eq!(f.tenants.remove(Actor::Cli, id, Removal::Empty), Ok(()));
2546        assert_eq!(f.dirs().len(), 1, "the dead process removed nothing");
2547        f.restart();
2548        assert!(!f.has_row(id));
2549        assert_eq!(f.dirs(), Vec::<OsString>::new());
2550    }
2551
2552    #[test]
2553    fn removal_deletes_the_directory_on_disk() {
2554        let dir = tempfile::tempdir().expect("temp dir");
2555        let tenants = Tenants::open(
2556            dir.path(),
2557            Master::new().key(),
2558            Arc::new(crate::fs::OsFs),
2559            Arc::new(ManualClock::at(datetime!(2026-10-23 12:00 UTC))),
2560        )
2561        .expect("opens");
2562        let id = tenants.create(Actor::Cli, "acme", None).expect("made");
2563        let tenant_dir = dir.path().join("tenants").join(id.to_string());
2564        assert!(tenant_dir.join("catalog.sqlite").is_file());
2565        tenants
2566            .remove(Actor::Cli, id, Removal::Empty)
2567            .expect("removed");
2568        assert!(!tenant_dir.exists());
2569    }
2570
2571    #[test]
2572    fn two_tenants_keep_the_same_uuid_apart() {
2573        let f = fixture();
2574        let a = f.tenants.create(Actor::Cli, "a", None).expect("made");
2575        let b = f.tenants.create(Actor::Cli, "b", None).expect("made");
2576        let (found, kept_by_a) = vcon(UUID, "kept by a");
2577        let (_, kept_by_b) = vcon(UUID, "kept by b");
2578        let (store_a, store_b) = (
2579            f.tenants.store(a).expect("a's store"),
2580            f.tenants.store(b).expect("b's store"),
2581        );
2582        assert!(matches!(
2583            store_a.create(&caller(), &found, &kept_by_a),
2584            Ok(Created::New(_))
2585        ));
2586        assert!(matches!(
2587            store_b.create(&caller(), &found, &kept_by_b),
2588            Ok(Created::New(_))
2589        ));
2590        let read = |store: &Store| store.get(&caller(), found.uuid).map(|s| s.body);
2591        assert_eq!(read(&store_a), Ok(kept_by_a));
2592        assert_eq!(read(&store_b), Ok(kept_by_b.clone()));
2593        store_a
2594            .delete(&caller(), found.uuid, Retention::Years(7))
2595            .expect("a deletes its own");
2596        assert_eq!(read(&store_a), Err(StoreError::Gone));
2597        assert_eq!(read(&store_b), Ok(kept_by_b), "b's is untouched");
2598    }
2599
2600    /// A version's data key is wrapped for its tenant by id, so the
2601    /// tenant's Store must be opened under that id.
2602    #[test]
2603    fn a_tenants_data_keys_are_bound_to_its_own_id() {
2604        let f = fixture();
2605        let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
2606        let (found, body) = vcon(UUID, "first");
2607        let Ok(Created::New(version)) =
2608            f.tenants
2609                .store(id)
2610                .expect("its store")
2611                .create(&caller(), &found, &body)
2612        else {
2613            panic!("a new vCon")
2614        };
2615        let wrapped: Vec<u8> = f
2616            .tenant_catalog(id)
2617            .query_row(
2618                "SELECT wrapped_key FROM version WHERE version = ?1",
2619                [version.0],
2620                |r| r.get(0),
2621            )
2622            .expect("its row");
2623        let ctx = Context::Version {
2624            tenant: id,
2625            uuid: found.uuid,
2626            version,
2627        };
2628        assert!(envelope::unwrap(&f.tenant_key(id), &ctx, &wrapped).is_ok());
2629    }
2630
2631    /// The answers one tenant gets for a uuid, whatever another holds.
2632    fn answers(store: &Store) -> Vec<String> {
2633        let (found, body) = vcon(UUID, "b's own");
2634        let (_, newer) = vcon(UUID, "b's newer");
2635        let kind = |r: Result<Created, StoreError>| match r {
2636            Ok(Created::New(_)) => "new".to_owned(),
2637            Ok(Created::Same(_)) => "same".to_owned(),
2638            Err(e) => format!("{e:?}"),
2639        };
2640        let who = caller();
2641        let mut answers = vec![
2642            format!("{:?}", store.get(&who, found.uuid).map(|_| ())),
2643            format!("{:?}", store.delete(&who, found.uuid, Retention::Years(7))),
2644            kind(store.create(&who, &found, &body)),
2645            kind(store.create(&who, &found, &body)),
2646        ];
2647        let current = store.get(&who, found.uuid).expect("stored").version;
2648        answers.push(format!(
2649            "{:?}",
2650            store.replace(&who, current, &found, &newer).map(|_| ())
2651        ));
2652        answers.push(format!(
2653            "{:?}",
2654            store.delete(&who, found.uuid, Retention::Years(7))
2655        ));
2656        answers.push(format!("{:?}", store.get(&who, found.uuid).map(|_| ())));
2657        answers
2658    }
2659
2660    #[test]
2661    fn nothing_one_tenant_does_shows_whether_another_holds_a_uuid() {
2662        let alone = fixture();
2663        let b = alone.tenants.create(Actor::Cli, "b", None).expect("made");
2664        let expected = answers(&alone.tenants.store(b).expect("b's store"));
2665
2666        let f = fixture();
2667        let a = f.tenants.create(Actor::Cli, "a", None).expect("made");
2668        let b = f.tenants.create(Actor::Cli, "b", None).expect("made");
2669        let store_a = f.tenants.store(a).expect("a's store");
2670        let (found, body) = vcon(UUID, "a's");
2671        store_a
2672            .create(&caller(), &found, &body)
2673            .expect("a holds it");
2674        let (other, body) = vcon("01a07da8-c2bb-83e5-b9a2-279e0d16bc47", "a's other");
2675        store_a.create(&caller(), &other, &body).expect("stored");
2676        store_a
2677            .delete(&caller(), other.uuid, Retention::Years(7))
2678            .expect("and has deleted another");
2679        assert_eq!(answers(&f.tenants.store(b).expect("b's store")), expected);
2680        assert_eq!(
2681            expected,
2682            [
2683                "Err(NotFound)",
2684                "Err(NotFound)",
2685                "new",
2686                "same",
2687                "Ok(())",
2688                "Ok(())",
2689                "Err(Gone)"
2690            ]
2691        );
2692    }
2693}