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::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
32const 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
64fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum TenantError {
74 NotFound,
76 NameTaken,
78 BadName,
80 HoldsVcons,
82 UnderHold,
85 LimitReached,
87 AuditFailed,
89 Closing,
91 CliOnly,
93 BadScopes,
95 WrongKey,
97 Io,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct TenantInfo {
103 pub id: TenantId,
104 pub name: String,
105 pub retention: Option<Retention>,
107 pub created_at: OffsetDateTime,
108}
109
110impl TenantInfo {
111 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 pub fn kept_for(&self, default: Retention) -> Retention {
124 self.retention.unwrap_or(default)
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum Removal {
131 Empty,
133 Force,
135 ForceOverridingHolds,
137}
138
139enum Slot {
141 Open(Arc<Store>),
142 Closing,
144}
145
146pub struct Tenants {
148 tenants: PathBuf,
150 master: Key,
151 conn: Mutex<Connection>,
152 stores: Mutex<HashMap<TenantId, Slot>>,
154 beside: bool,
156 fs: Arc<dyn Fs>,
157 clock: Arc<dyn Clock>,
158 #[cfg(test)]
159 hooks: tests::Hooks,
160}
161
162impl Tenants {
163 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 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 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 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 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 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 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 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 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 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 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 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 pub fn store(&self, id: TenantId) -> Result<Arc<Store>, TenantError> {
512 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 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 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 #[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 #[cfg(test)]
612 pub(crate) fn conn_for_test(&self) -> MutexGuard<'_, Connection> {
613 self.conn()
614 }
615
616 #[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 #[cfg(test)]
630 pub(crate) fn admin_records(&self) -> Vec<serde_json::Value> {
631 crate::audit::testing::exported(&self.conn(), None)
632 }
633
634 #[cfg(test)]
637 pub(crate) fn close(&self, id: TenantId) {
638 self.drain(id).expect("not closing yet");
639 }
640
641 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 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 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 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 pub fn check(&self) -> Result<(), TenantError> {
705 self.conn()
706 .query_row("SELECT count(*) FROM tenant", [], |_| Ok(()))
707 .map_err(io)
708 }
709
710 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 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
737fn 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 pub(super) between: Mutex<Option<Box<dyn FnOnce() + Send>>>,
815 pub(super) opening: Mutex<Option<Box<dyn FnOnce() + Send>>>,
817 pub(crate) rekey_stop: Mutex<Option<super::rekey::RekeyStep>>,
819 }
820
821 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 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 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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 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}