1use 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
34const 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
66fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TenantError {
76 NotFound,
78 NameTaken,
80 BadName,
82 HoldsVcons,
84 UnderHold,
87 LimitReached,
89 AuditFailed,
91 Closing,
93 CliOnly,
95 BadScopes,
97 WrongKey,
99 Io,
100}
101
102#[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#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct TenantInfo {
144 pub id: TenantId,
145 pub name: String,
146 pub retention: Option<Retention>,
148 pub created_at: OffsetDateTime,
149}
150
151impl TenantInfo {
152 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 pub fn kept_for(&self, default: Retention) -> Retention {
165 self.retention.unwrap_or(default)
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum Removal {
172 Empty,
174 Force,
176 ForceOverridingHolds,
178}
179
180enum Slot {
182 Open(Arc<Store>),
183 Closing,
185}
186
187pub struct Tenants {
189 tenants: PathBuf,
191 master: Key,
192 conn: Mutex<Connection>,
193 stores: Mutex<HashMap<TenantId, Slot>>,
195 beside: bool,
197 fs: Arc<dyn Fs>,
198 clock: Arc<dyn Clock>,
199 #[cfg(test)]
200 hooks: tests::Hooks,
201}
202
203impl Tenants {
204 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 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 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 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 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 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 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 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 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 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 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 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 pub fn store(&self, id: TenantId) -> Result<Arc<Store>, TenantError> {
553 self.store_or_why(id).map_err(TenantError::from)
554 }
555
556 pub fn store_or_why(&self, id: TenantId) -> Result<Arc<Store>, OpenFailure> {
558 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 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 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 #[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 #[cfg(test)]
658 pub(crate) fn conn_for_test(&self) -> MutexGuard<'_, Connection> {
659 self.conn()
660 }
661
662 #[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 #[cfg(test)]
676 pub(crate) fn admin_records(&self) -> Vec<serde_json::Value> {
677 crate::audit::testing::exported(&self.conn(), None)
678 }
679
680 #[cfg(test)]
683 pub(crate) fn close(&self, id: TenantId) {
684 self.drain(id).expect("not closing yet");
685 }
686
687 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 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 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 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 pub fn check(&self) -> Result<(), TenantError> {
751 self.conn()
752 .query_row("SELECT count(*) FROM tenant", [], |_| Ok(()))
753 .map_err(io)
754 }
755
756 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 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
783fn 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 pub(super) type RegistryHook = Box<dyn FnMut(&Tenants) + Send>;
858
859 #[derive(Default)]
860 pub(super) struct Hooks {
861 pub(super) between: Mutex<Option<Box<dyn FnOnce() + Send>>>,
864 pub(super) opening: Mutex<Option<Box<dyn FnOnce() + Send>>>,
866 pub(crate) rekey_stop: Mutex<Option<super::rekey::RekeyStep>>,
868 pub(super) registry_copied: Mutex<Option<RegistryHook>>,
871 }
872
873 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 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 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 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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 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}