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