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