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