use std::{
collections::HashMap,
sync::{Arc, Mutex, Weak},
};
use ed25519_dalek::VerifyingKey;
use handle_trait::Handle;
use crate::{
Database, Entry, Result, auth::crypto::format_public_key, backend::BackendImpl, entry::ID,
sync::Sync, user::User,
};
pub mod backend;
pub mod errors;
pub mod legacy_ops;
pub mod settings_merge;
use backend::Backend;
pub use errors::InstanceError;
pub use legacy_ops::LegacyInstanceOps;
const DEVICE_KEY_NAME: &str = "_device_key";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WriteSource {
Local,
Remote,
}
pub trait WriteCallback:
Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync
{
}
impl<T> WriteCallback for T where
T: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync
{
}
type CallbackVec = Vec<Arc<dyn WriteCallback>>;
type CallbackKey = (WriteSource, ID);
pub(crate) struct InstanceInternal {
backend: Backend,
sync: std::sync::OnceLock<Arc<Sync>>,
users_db_id: ID,
databases_db_id: ID,
write_callbacks: Mutex<HashMap<CallbackKey, CallbackVec>>,
global_write_callbacks: Mutex<HashMap<WriteSource, CallbackVec>>,
}
impl std::fmt::Debug for InstanceInternal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InstanceInternal")
.field("backend", &"<BackendDB>")
.field("sync", &self.sync)
.field("users_db_id", &self.users_db_id)
.field("databases_db_id", &self.databases_db_id)
.field(
"write_callbacks",
&format!(
"<{} per-db callbacks>",
self.write_callbacks.lock().unwrap().len()
),
)
.field(
"global_write_callbacks",
&format!(
"<{} global callbacks>",
self.global_write_callbacks.lock().unwrap().len()
),
)
.finish()
}
}
#[derive(Clone, Debug, Handle)]
pub struct Instance {
inner: Arc<InstanceInternal>,
}
#[derive(Clone, Debug, Handle)]
pub struct WeakInstance {
inner: Weak<InstanceInternal>,
}
impl Instance {
pub fn open(backend: Box<dyn BackendImpl>) -> Result<Self> {
use crate::constants::{DATABASES, USERS};
let backend: Arc<dyn BackendImpl> = Arc::from(backend);
let _device_key = match backend.get_private_key(DEVICE_KEY_NAME)? {
Some(key) => key,
None => {
return Self::create_internal(backend);
}
};
let all_roots = backend.all_roots()?;
let mut users_db_root = None;
let mut databases_db_root = None;
for root_id in all_roots {
let temp_instance = Self {
inner: Arc::new(InstanceInternal {
backend: Backend::new(Arc::clone(&backend)),
sync: std::sync::OnceLock::new(),
users_db_id: ID::from(""), databases_db_id: ID::from(""), write_callbacks: Mutex::new(HashMap::new()),
global_write_callbacks: Mutex::new(HashMap::new()),
}),
};
let temp_db = Database::open_readonly(root_id.clone(), &temp_instance)?;
if let Ok(name) = temp_db.get_name() {
match name.as_str() {
USERS => {
if users_db_root.is_some() {
panic!(
"CRITICAL SECURITY ERROR: Multiple {USERS} databases found in backend. \
This indicates database corruption or a potential security breach. \
Backend integrity compromised."
);
}
users_db_root = Some(root_id);
}
DATABASES => {
if databases_db_root.is_some() {
panic!(
"CRITICAL SECURITY ERROR: Multiple {DATABASES} databases found in backend. \
This indicates database corruption or a potential security breach. \
Backend integrity compromised."
);
}
databases_db_root = Some(root_id);
}
_ => {} }
}
if users_db_root.is_some() && databases_db_root.is_some() {
break;
}
}
let users_db_root = users_db_root.ok_or(InstanceError::SystemDatabaseNotFound {
database_name: USERS.to_string(),
})?;
let databases_db_root = databases_db_root.ok_or(InstanceError::SystemDatabaseNotFound {
database_name: DATABASES.to_string(),
})?;
let inner = Arc::new(InstanceInternal {
backend: Backend::new(backend),
sync: std::sync::OnceLock::new(),
users_db_id: users_db_root,
databases_db_id: databases_db_root,
write_callbacks: Mutex::new(HashMap::new()),
global_write_callbacks: Mutex::new(HashMap::new()),
});
Ok(Self { inner })
}
pub fn create(backend: Box<dyn BackendImpl>) -> Result<Self> {
let backend: Arc<dyn BackendImpl> = Arc::from(backend);
if backend.get_private_key(DEVICE_KEY_NAME)?.is_some() {
return Err(InstanceError::InstanceAlreadyExists.into());
}
Self::create_internal(backend)
}
pub(crate) fn create_internal(backend: Arc<dyn BackendImpl>) -> Result<Self> {
use crate::{
auth::crypto::{format_public_key, generate_keypair},
user::system_databases::{create_databases_tracking, create_users_database},
};
let (device_key, device_pubkey) = generate_keypair();
let device_pubkey_str = format_public_key(&device_pubkey);
backend.store_private_key(DEVICE_KEY_NAME, device_key.clone())?;
let temp_instance = Self {
inner: Arc::new(InstanceInternal {
backend: Backend::new(Arc::clone(&backend)),
sync: std::sync::OnceLock::new(),
users_db_id: ID::from(""), databases_db_id: ID::from(""), write_callbacks: Mutex::new(HashMap::new()),
global_write_callbacks: Mutex::new(HashMap::new()),
}),
};
let users_db = create_users_database(&temp_instance, &device_key, &device_pubkey_str)?;
let databases_db =
create_databases_tracking(&temp_instance, &device_key, &device_pubkey_str)?;
let inner = Arc::new(InstanceInternal {
backend: Backend::new(backend),
sync: std::sync::OnceLock::new(),
users_db_id: users_db.root_id().clone(),
databases_db_id: databases_db.root_id().clone(),
write_callbacks: Mutex::new(HashMap::new()),
global_write_callbacks: Mutex::new(HashMap::new()),
});
Ok(Self { inner })
}
pub fn backend(&self) -> &Backend {
&self.inner.backend
}
pub(crate) fn get(&self, id: &crate::entry::ID) -> Result<crate::entry::Entry> {
self.inner.backend.get(id)
}
pub(crate) fn put(
&self,
verification_status: crate::backend::VerificationStatus,
entry: crate::entry::Entry,
) -> Result<()> {
self.inner.backend.put(verification_status, entry)
}
pub(crate) fn get_tips(&self, tree: &crate::entry::ID) -> Result<Vec<crate::entry::ID>> {
self.inner.backend.get_tips(tree)
}
pub(crate) fn users_db(&self) -> Result<Database> {
let device_key = self
.inner
.backend
.get_private_key(DEVICE_KEY_NAME)?
.ok_or(InstanceError::DeviceKeyNotFound)?;
Database::open(
self.clone(),
&self.inner.users_db_id,
device_key,
"_device_key".to_string(),
)
}
pub fn create_user(&self, user_id: &str, password: Option<&str>) -> Result<String> {
use crate::user::system_databases::create_user;
let users_db = self.users_db()?;
let (user_uuid, _user_info) = create_user(&users_db, self, user_id, password)?;
Ok(user_uuid)
}
pub fn login_user(&self, user_id: &str, password: Option<&str>) -> Result<User> {
use crate::user::system_databases::login_user;
let users_db = self.users_db()?;
login_user(&users_db, self, user_id, password)
}
pub fn list_users(&self) -> Result<Vec<String>> {
use crate::user::system_databases::list_users;
let users_db = self.users_db()?;
list_users(&users_db)
}
pub fn device_id(&self) -> Result<VerifyingKey> {
let device_key = self
.inner
.backend
.get_private_key(DEVICE_KEY_NAME)?
.ok_or_else(|| crate::Error::from(InstanceError::DeviceKeyNotFound))?;
Ok(device_key.verifying_key())
}
pub fn device_id_string(&self) -> Result<String> {
let device_key = self.device_id()?;
Ok(format_public_key(&device_key))
}
pub fn load_database(&self, root_id: &ID) -> Result<Database> {
self.inner.backend.get(root_id)?;
let database = Database::open_readonly(root_id.clone(), self)?;
Ok(database)
}
pub fn all_databases(&self) -> Result<Vec<Database>> {
let root_ids = self.inner.backend.all_roots()?;
let mut databases = Vec::new();
for root_id in root_ids {
let database = Database::open_readonly(root_id.clone(), self)?;
databases.push(database);
}
Ok(databases)
}
pub fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
let name = name.as_ref();
let all_databases = self.all_databases()?;
let mut matching_databases = Vec::new();
for database in all_databases {
if let Ok(database_name) = database.get_name()
&& database_name == name
{
matching_databases.push(database);
}
}
if matching_databases.is_empty() {
Err(InstanceError::DatabaseNotFound {
name: name.to_string(),
}
.into())
} else {
Ok(matching_databases)
}
}
pub fn list_private_keys(&self) -> Result<Vec<String>> {
self.inner.backend.list_private_keys()
}
pub fn enable_sync(&self) -> Result<()> {
if self.inner.sync.get().is_some() {
return Ok(());
}
let sync = Sync::new(self.clone())?;
let sync_arc = Arc::new(sync);
let sync_for_callback = Arc::clone(&sync_arc);
self.register_global_write_callback(
WriteSource::Local,
move |entry, database, instance| {
sync_for_callback.on_local_write(entry, database, instance)
},
)?;
let _ = self.inner.sync.set(sync_arc);
Ok(())
}
pub fn sync(&self) -> Option<Arc<Sync>> {
self.inner.sync.get().map(Arc::clone)
}
pub(crate) fn register_write_callback<F>(
&self,
source: WriteSource,
tree_id: ID,
callback: F,
) -> Result<()>
where
F: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync + 'static,
{
let mut callbacks = self.inner.write_callbacks.lock().unwrap();
callbacks
.entry((source, tree_id))
.or_default()
.push(Arc::new(callback));
Ok(())
}
pub(crate) fn register_global_write_callback<F>(
&self,
source: WriteSource,
callback: F,
) -> Result<()>
where
F: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync + 'static,
{
let mut callbacks = self.inner.global_write_callbacks.lock().unwrap();
callbacks
.entry(source)
.or_default()
.push(Arc::new(callback));
Ok(())
}
pub fn put_entry(
&self,
tree_id: &ID,
verification: crate::backend::VerificationStatus,
entry: Entry,
source: WriteSource,
) -> Result<()> {
self.backend().put(verification, entry.clone())?;
let per_db_callbacks = self
.inner
.write_callbacks
.lock()
.unwrap()
.get(&(source, tree_id.clone()))
.cloned();
let global_callbacks = self
.inner
.global_write_callbacks
.lock()
.unwrap()
.get(&source)
.cloned();
let has_callbacks = per_db_callbacks.is_some() || global_callbacks.is_some();
if has_callbacks {
let database = Database::open_readonly(tree_id.clone(), self)?;
if let Some(callbacks) = per_db_callbacks {
for callback in callbacks {
if let Err(e) = callback(&entry, &database, self) {
tracing::error!(
tree_id = %tree_id,
entry_id = %entry.id(),
source = ?source,
"Per-database callback failed: {}", e
);
}
}
}
if let Some(callbacks) = global_callbacks {
for callback in callbacks {
if let Err(e) = callback(&entry, &database, self) {
tracing::error!(
tree_id = %tree_id,
entry_id = %entry.id(),
source = ?source,
"Global callback failed: {}", e
);
}
}
}
}
Ok(())
}
pub fn downgrade(&self) -> WeakInstance {
WeakInstance {
inner: Arc::downgrade(&self.inner),
}
}
}
impl WeakInstance {
pub fn upgrade(&self) -> Option<Instance> {
self.inner.upgrade().map(|inner| Instance { inner })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Error, backend::database::InMemory, crdt::Doc, instance::LegacyInstanceOps};
#[test]
fn test_create_user() -> Result<(), Error> {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend))?;
let user_uuid = instance.create_user("alice", Some("password123")).unwrap();
assert!(!user_uuid.is_empty());
let users = instance.list_users().unwrap();
assert_eq!(users.len(), 1);
assert_eq!(users[0], "alice");
Ok(())
}
#[test]
fn test_login_user() -> Result<(), Error> {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend))?;
instance.create_user("alice", Some("password123")).unwrap();
let user = instance.login_user("alice", Some("password123")).unwrap();
assert_eq!(user.username(), "alice");
let result = instance.login_user("alice", Some("wrong_password"));
assert!(result.is_err());
Ok(())
}
#[test]
fn test_new_database() {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
let mut settings = Doc::new();
settings.set_string("name", "test_db");
let database = instance.new_database(settings, "_device_key").unwrap();
assert_eq!(database.get_name().unwrap(), "test_db");
}
#[test]
fn test_new_database_default() {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
let database = instance.new_database_default("_device_key").unwrap();
let settings = database.get_settings().unwrap();
assert!(settings.get_string("database_id").is_ok());
}
#[test]
fn test_new_database_without_key_fails() -> Result<(), Error> {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend))?;
let mut settings = Doc::new();
settings.set_string("name", "test_db");
let result = instance.new_database(settings, "nonexistent_key");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_load_database() {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
let mut settings = Doc::new();
settings.set_string("name", "test_db");
let database = instance.new_database(settings, "_device_key").unwrap();
let root_id = database.root_id().clone();
let loaded_database = instance.load_database(&root_id).unwrap();
assert_eq!(loaded_database.get_name().unwrap(), "test_db");
}
#[test]
fn test_all_databases() {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
let mut settings1 = Doc::new();
settings1.set_string("name", "db1");
instance.new_database(settings1, "_device_key").unwrap();
let mut settings2 = Doc::new();
settings2.set_string("name", "db2");
instance.new_database(settings2, "_device_key").unwrap();
let databases = instance.all_databases().unwrap();
assert!(databases.len() >= 2); }
#[test]
fn test_find_database() {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
let mut settings = Doc::new();
settings.set_string("name", "my_special_db");
instance.new_database(settings, "_device_key").unwrap();
let found = instance.find_database("my_special_db").unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].get_name().unwrap(), "my_special_db");
let result = instance.find_database("nonexistent");
assert!(result.is_err());
}
#[test]
fn test_instance_load_new_backend() -> Result<(), Error> {
let backend = InMemory::new();
let instance = Instance::open(Box::new(backend))?;
assert!(instance.device_id().is_ok());
instance.create_user("alice", None)?;
let user = instance.login_user("alice", None)?;
assert_eq!(user.username(), "alice");
Ok(())
}
#[test]
fn test_instance_load_existing_backend() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_instance_load.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
instance1.create_user("bob", None)?;
let mut user1 = instance1.login_user("bob", None)?;
let default_key = user1.get_default_key()?;
let mut settings = Doc::new();
settings.set_string("name", "bob_database");
user1.create_database(settings, &default_key)?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
drop(user1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let users = instance2.list_users()?;
assert_eq!(users.len(), 1);
assert_eq!(users[0], "bob");
let user2 = instance2.login_user("bob", None)?;
assert_eq!(user2.username(), "bob");
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_device_id_persistence() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_device_id.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
let device_id1 = instance1.device_id_string()?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let device_id2 = instance2.device_id_string()?;
assert_eq!(
device_id1, device_id2,
"Device ID should persist across reloads"
);
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_with_password_protected_users() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_password_users.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
instance1.create_user("secure_alice", Some("secret123"))?;
let user1 = instance1.login_user("secure_alice", Some("secret123"))?;
assert_eq!(user1.username(), "secure_alice");
drop(user1);
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let user2 = instance2.login_user("secure_alice", Some("secret123"))?;
assert_eq!(user2.username(), "secure_alice");
let result = instance2.login_user("secure_alice", Some("wrong_password"));
assert!(result.is_err(), "Login with wrong password should fail");
let result = instance2.login_user("secure_alice", None);
assert!(
result.is_err(),
"Login without password should fail for password-protected user"
);
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_multiple_users() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_multiple_users.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
instance1.create_user("alice", None)?;
instance1.create_user("bob", Some("bobpass"))?;
instance1.create_user("charlie", None)?;
instance1.create_user("diana", Some("dianapass"))?;
instance1.login_user("alice", None)?;
instance1.login_user("bob", Some("bobpass"))?;
instance1.login_user("charlie", None)?;
instance1.login_user("diana", Some("dianapass"))?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let users = instance2.list_users()?;
assert_eq!(users.len(), 4, "All 4 users should be present after reload");
assert!(users.contains(&"alice".to_string()));
assert!(users.contains(&"bob".to_string()));
assert!(users.contains(&"charlie".to_string()));
assert!(users.contains(&"diana".to_string()));
instance2.login_user("alice", None)?;
instance2.login_user("bob", Some("bobpass"))?;
instance2.login_user("charlie", None)?;
instance2.login_user("diana", Some("dianapass"))?;
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_user_databases_persist() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_user_dbs.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
instance1.create_user("eve", None)?;
let mut user1 = instance1.login_user("eve", None)?;
let default_key = user1.get_default_key()?;
let mut settings1 = Doc::new();
settings1.set_string("name", "database_one");
settings1.set_string("purpose", "testing");
let db1 = user1.create_database(settings1, &default_key)?;
let db1_root = db1.root_id().clone();
let mut settings2 = Doc::new();
settings2.set_string("name", "database_two");
settings2.set_string("purpose", "production");
let db2 = user1.create_database(settings2, &default_key)?;
let db2_root = db2.root_id().clone();
drop(db1);
drop(db2);
drop(user1);
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let _user2 = instance2.login_user("eve", None)?;
let loaded_db1 = instance2.load_database(&db1_root)?;
assert_eq!(loaded_db1.get_name()?, "database_one");
let settings1_doc = loaded_db1.get_settings()?;
assert_eq!(settings1_doc.get_string("purpose")?, "testing");
let loaded_db2 = instance2.load_database(&db2_root)?;
assert_eq!(loaded_db2.get_name()?, "database_two");
let settings2_doc = loaded_db2.get_settings()?;
assert_eq!(settings2_doc.get_string("purpose")?, "production");
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_idempotency() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_idempotency.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
instance1.create_user("frank", None)?;
let device_id1 = instance1.device_id_string()?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
for i in 0..3 {
let backend = InMemory::load_from_file(&path)?;
let instance = Instance::open(Box::new(backend))?;
let device_id = instance.device_id_string()?;
assert_eq!(
device_id, device_id1,
"Device ID should be consistent on reload {i}"
);
let users = instance.list_users()?;
assert_eq!(users.len(), 1);
assert_eq!(users[0], "frank");
let user = instance.login_user("frank", None)?;
assert_eq!(user.username(), "frank");
drop(user);
drop(instance);
}
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_load_new_vs_existing() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_new_vs_existing.json");
let backend1 = InMemory::new();
let instance1 = Instance::open(Box::new(backend1))?;
let device_id1 = instance1.device_id_string()?;
instance1.create_user("grace", None)?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let instance2 = Instance::open(Box::new(backend2))?;
let device_id2 = instance2.device_id_string()?;
assert_eq!(device_id1, device_id2);
let users = instance2.list_users()?;
assert_eq!(users.len(), 1);
assert_eq!(users[0], "grace");
drop(instance2);
let backend3 = InMemory::new();
let instance3 = Instance::open(Box::new(backend3))?;
let device_id3 = instance3.device_id_string()?;
assert_ne!(device_id1, device_id3);
let users = instance3.list_users()?;
assert_eq!(users.len(), 0);
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_create_strict_fails_on_existing() -> Result<(), Error> {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("eidetica_test_create_strict.json");
let backend1 = InMemory::new();
let instance1 = Instance::create(Box::new(backend1))?;
instance1.create_user("alice", None)?;
let backend_guard = instance1.backend();
if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
in_memory.save_to_file(&path)?;
}
drop(instance1);
let backend2 = InMemory::load_from_file(&path)?;
let result = Instance::create(Box::new(backend2));
assert!(result.is_err(), "create() should fail on existing backend");
if let Err(err) = result {
if let crate::Error::Instance(instance_err) = err {
assert!(
instance_err.is_already_exists(),
"Error should be InstanceAlreadyExists"
);
} else {
panic!("Expected Instance error");
}
}
let backend3 = InMemory::load_from_file(&path)?;
let instance3 = Instance::open(Box::new(backend3))?;
let users = instance3.list_users()?;
assert_eq!(users.len(), 1);
assert_eq!(users[0], "alice");
if path.exists() {
std::fs::remove_file(&path).ok();
}
Ok(())
}
#[test]
fn test_instance_create_on_fresh_backend() -> Result<(), Error> {
let backend = InMemory::new();
let instance = Instance::create(Box::new(backend))?;
assert!(instance.device_id().is_ok());
instance.create_user("bob", None)?;
let user = instance.login_user("bob", None)?;
assert_eq!(user.username(), "bob");
Ok(())
}
}