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