eidetica/instance/
mod.rs

1//!
2//! Provides the main database structures (`Instance` and `Database`).
3//!
4//! `Instance` manages multiple `Database` instances and interacts with the storage `Database`.
5//! `Database` represents a single, independent history of data entries, analogous to a table or branch.
6
7use std::{
8    collections::HashMap,
9    sync::{Arc, Mutex, Weak},
10};
11
12use ed25519_dalek::VerifyingKey;
13use handle_trait::Handle;
14
15use crate::{
16    Database, Entry, Result, auth::crypto::format_public_key, backend::BackendImpl, entry::ID,
17    sync::Sync, user::User,
18};
19
20pub mod backend;
21pub mod errors;
22pub mod legacy_ops;
23pub mod settings_merge;
24
25// Re-export main types for easier access
26use backend::Backend;
27pub use errors::InstanceError;
28pub use legacy_ops::LegacyInstanceOps;
29
30/// Private constants for device identity management
31const DEVICE_KEY_NAME: &str = "_device_key";
32
33/// Indicates whether an entry write originated locally or from a remote source (e.g., sync).
34///
35/// This distinction allows different callbacks to be triggered based on the write source,
36/// enabling behaviors like "only trigger sync for local writes" or "only update UI for remote writes".
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum WriteSource {
39    /// Write originated from a local transaction commit
40    Local,
41    /// Write originated from a remote source (e.g., sync, replication)
42    Remote,
43}
44
45/// Callback function trait for write operations.
46///
47/// Receives the entry that was written, the database it was written to, and the instance.
48/// Used for both local and remote write callbacks.
49///
50/// This trait alias can be used both as a trait bound (e.g., `F: WriteCallback`) and as a
51/// trait object type for storage (e.g., `Arc<dyn WriteCallback>`).
52pub trait WriteCallback:
53    Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync
54{
55}
56
57// Blanket implementation: any type that satisfies the bounds automatically implements WriteCallback
58impl<T> WriteCallback for T where
59    T: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync
60{
61}
62
63/// Type alias for a collection of write callbacks
64type CallbackVec = Vec<Arc<dyn WriteCallback>>;
65
66/// Type alias for the per-database callback map key
67type CallbackKey = (WriteSource, ID);
68
69/// Internal state for Instance
70///
71/// This structure holds the actual implementation data for Instance.
72/// Instance itself is just a cheap-to-clone handle wrapping Arc<InstanceInternal>.
73pub(crate) struct InstanceInternal {
74    /// The database storage backend
75    backend: Backend,
76    /// Synchronization module for this database instance
77    /// TODO: Overengineered, Sync can be created by default but disabled
78    sync: std::sync::OnceLock<Arc<Sync>>,
79    /// Root ID of the _users system database
80    users_db_id: ID,
81    /// Root ID of the _databases system database
82    databases_db_id: ID,
83    /// Per-database callbacks keyed by (WriteSource, tree_id)
84    write_callbacks: Mutex<HashMap<CallbackKey, CallbackVec>>,
85    /// Global callbacks keyed by WriteSource (triggered regardless of database)
86    global_write_callbacks: Mutex<HashMap<WriteSource, CallbackVec>>,
87}
88
89impl std::fmt::Debug for InstanceInternal {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("InstanceInternal")
92            .field("backend", &"<BackendDB>")
93            .field("sync", &self.sync)
94            .field("users_db_id", &self.users_db_id)
95            .field("databases_db_id", &self.databases_db_id)
96            .field(
97                "write_callbacks",
98                &format!(
99                    "<{} per-db callbacks>",
100                    self.write_callbacks.lock().unwrap().len()
101                ),
102            )
103            .field(
104                "global_write_callbacks",
105                &format!(
106                    "<{} global callbacks>",
107                    self.global_write_callbacks.lock().unwrap().len()
108                ),
109            )
110            .finish()
111    }
112}
113/// Database implementation on top of the storage backend.
114///
115/// Instance manages infrastructure only:
116/// - Backend storage and device identity (_device_key)
117/// - System databases (_users, _databases, _sync)
118/// - User account management (create, login, list)
119///
120/// All database creation and key operations happen through User after login.
121///
122/// Instance is a cheap-to-clone handle around `Arc<InstanceInternal>`.
123///
124/// ## Example
125///
126/// ```
127/// # use eidetica::{backend::database::InMemory, Instance, crdt::Doc};
128/// let instance = Instance::open(Box::new(InMemory::new()))?;
129///
130/// // Create passwordless user
131/// instance.create_user("alice", None)?;
132/// let mut user = instance.login_user("alice", None)?;
133///
134/// // Use User API for operations
135/// let mut settings = Doc::new();
136/// settings.set_string("name", "my_database");
137/// let default_key = user.get_default_key()?;
138/// let db = user.create_database(settings, &default_key)?;
139/// # Ok::<(), eidetica::Error>(())
140/// ```
141#[derive(Clone, Debug, Handle)]
142pub struct Instance {
143    inner: Arc<InstanceInternal>,
144}
145
146/// Weak reference to an Instance.
147///
148/// This is a weak handle that does not prevent the Instance from being dropped.
149/// Dependent objects (Database, Sync, BackgroundSync) hold weak references to avoid
150/// circular reference cycles that would leak memory.
151///
152/// Use `upgrade()` to convert to a strong `Instance` reference.
153#[derive(Clone, Debug, Handle)]
154pub struct WeakInstance {
155    inner: Weak<InstanceInternal>,
156}
157
158impl Instance {
159    /// Load an existing Instance or create a new one (recommended).
160    ///
161    /// This is the recommended method for initializing an Instance. It automatically detects
162    /// whether the backend contains existing system state (device key and system databases)
163    /// and loads them, or creates new ones if starting fresh.
164    ///
165    /// Instance manages infrastructure only:
166    /// - Backend storage and device identity (_device_key)
167    /// - System databases (_users, _databases, _sync)
168    /// - User account management (create, login, list)
169    ///
170    /// All database creation and key operations require explicit User login.
171    ///
172    /// # Arguments
173    /// * `backend` - The storage backend to use
174    ///
175    /// # Returns
176    /// A Result containing the configured Instance
177    ///
178    /// # Example
179    /// ```
180    /// # use eidetica::{backend::database::InMemory, Instance, crdt::Doc};
181    /// let backend = InMemory::new();
182    /// let instance = Instance::open(Box::new(backend))?;
183    ///
184    /// // Create and login user explicitly
185    /// instance.create_user("alice", None)?;
186    /// let mut user = instance.login_user("alice", None)?;
187    ///
188    /// // Use User API for operations
189    /// let mut settings = Doc::new();
190    /// settings.set_string("name", "my_database");
191    /// let default_key = user.get_default_key()?;
192    /// let db = user.create_database(settings, &default_key)?;
193    /// # Ok::<(), eidetica::Error>(())
194    /// ```
195    pub fn open(backend: Box<dyn BackendImpl>) -> Result<Self> {
196        use crate::constants::{DATABASES, USERS};
197
198        let backend: Arc<dyn BackendImpl> = Arc::from(backend);
199
200        // Load device_key first
201        let _device_key = match backend.get_private_key(DEVICE_KEY_NAME)? {
202            Some(key) => key,
203            None => {
204                // New backend: initialize like create()
205                return Self::create_internal(backend);
206            }
207        };
208
209        // Existing backend: load system databases
210        let all_roots = backend.all_roots()?;
211
212        // Find system databases by name
213        let mut users_db_root = None;
214        let mut databases_db_root = None;
215
216        for root_id in all_roots {
217            // FIXME(security): handle the security and loading of these databases in a better way
218            // Use open_readonly temporarily to check name without setting up auth
219            // Note: We can't use self.clone() here because self doesn't exist yet during construction
220            // So we create a temporary Instance just for this lookup
221            //
222            // SAFETY: The temporary instance has empty users_db_id and databases_db_id placeholders.
223            // This is safe because:
224            // 1. We only use it for Database::open_readonly() which doesn't access these fields
225            // 2. The Database only calls get_name() which reads from the settings store
226            // 3. The temporary instance is dropped immediately after name lookup
227            // 4. No other code paths will access the invalid system database IDs
228            let temp_instance = Self {
229                inner: Arc::new(InstanceInternal {
230                    backend: Backend::new(Arc::clone(&backend)),
231                    sync: std::sync::OnceLock::new(),
232                    users_db_id: ID::from(""), // Placeholder - not accessed during name lookup
233                    databases_db_id: ID::from(""), // Placeholder - not accessed during name lookup
234                    write_callbacks: Mutex::new(HashMap::new()),
235                    global_write_callbacks: Mutex::new(HashMap::new()),
236                }),
237            };
238            let temp_db = Database::open_readonly(root_id.clone(), &temp_instance)?;
239            if let Ok(name) = temp_db.get_name() {
240                match name.as_str() {
241                    USERS => {
242                        if users_db_root.is_some() {
243                            panic!(
244                                "CRITICAL SECURITY ERROR: Multiple {USERS} databases found in backend. \
245                                     This indicates database corruption or a potential security breach. \
246                                     Backend integrity compromised."
247                            );
248                        }
249                        users_db_root = Some(root_id);
250                    }
251                    DATABASES => {
252                        if databases_db_root.is_some() {
253                            panic!(
254                                "CRITICAL SECURITY ERROR: Multiple {DATABASES} databases found in backend. \
255                                     This indicates database corruption or a potential security breach. \
256                                     Backend integrity compromised."
257                            );
258                        }
259                        databases_db_root = Some(root_id);
260                    }
261                    _ => {} // Ignore other databases
262                }
263            }
264
265            // Stop searching if we found both
266            if users_db_root.is_some() && databases_db_root.is_some() {
267                break;
268            }
269        }
270
271        // Verify we found both system databases
272        let users_db_root = users_db_root.ok_or(InstanceError::SystemDatabaseNotFound {
273            database_name: USERS.to_string(),
274        })?;
275        let databases_db_root = databases_db_root.ok_or(InstanceError::SystemDatabaseNotFound {
276            database_name: DATABASES.to_string(),
277        })?;
278
279        let inner = Arc::new(InstanceInternal {
280            backend: Backend::new(backend),
281            sync: std::sync::OnceLock::new(),
282            users_db_id: users_db_root,
283            databases_db_id: databases_db_root,
284            write_callbacks: Mutex::new(HashMap::new()),
285            global_write_callbacks: Mutex::new(HashMap::new()),
286        });
287
288        Ok(Self { inner })
289    }
290
291    /// Create a new Instance on a fresh backend (strict creation).
292    ///
293    /// This method creates a new Instance and fails if the backend is already initialized
294    /// (contains a device key and system databases). Use this when you want to ensure
295    /// you're creating a fresh instance.
296    ///
297    /// Instance manages infrastructure only:
298    /// - Backend storage and device identity (_device_key)
299    /// - System databases (_users, _databases, _sync)
300    /// - User account management (create, login, list)
301    ///
302    /// All database creation and key operations require explicit User login.
303    ///
304    /// For most use cases, prefer `Instance::open()` which automatically handles both
305    /// new and existing backends.
306    ///
307    /// # Arguments
308    /// * `backend` - The storage backend to use (must be uninitialized)
309    ///
310    /// # Returns
311    /// A Result containing the configured Instance, or InstanceAlreadyExists error
312    /// if the backend is already initialized.
313    ///
314    /// # Example
315    /// ```
316    /// # use eidetica::{backend::database::InMemory, Instance, crdt::Doc};
317    /// let backend = InMemory::new();
318    /// let instance = Instance::create(Box::new(backend))?;
319    ///
320    /// // Create and login user explicitly
321    /// instance.create_user("alice", None)?;
322    /// let mut user = instance.login_user("alice", None)?;
323    ///
324    /// // Use User API for operations
325    /// let mut settings = Doc::new();
326    /// settings.set_string("name", "my_database");
327    /// let default_key = user.get_default_key()?;
328    /// let db = user.create_database(settings, &default_key)?;
329    /// # Ok::<(), eidetica::Error>(())
330    /// ```
331    pub fn create(backend: Box<dyn BackendImpl>) -> Result<Self> {
332        let backend: Arc<dyn BackendImpl> = Arc::from(backend);
333
334        // Check if already initialized
335        if backend.get_private_key(DEVICE_KEY_NAME)?.is_some() {
336            return Err(InstanceError::InstanceAlreadyExists.into());
337        }
338
339        // Create new instance
340        Self::create_internal(backend)
341    }
342
343    /// Internal implementation of new that works with Arc<dyn BackendImpl>
344    pub(crate) fn create_internal(backend: Arc<dyn BackendImpl>) -> Result<Self> {
345        use crate::{
346            auth::crypto::{format_public_key, generate_keypair},
347            user::system_databases::{create_databases_tracking, create_users_database},
348        };
349
350        // 1. Generate and store instance device key (_device_key)
351        let (device_key, device_pubkey) = generate_keypair();
352        let device_pubkey_str = format_public_key(&device_pubkey);
353        backend.store_private_key(DEVICE_KEY_NAME, device_key.clone())?;
354
355        // 2. Create system databases with device_key passed directly
356        // Create a temporary Instance for database creation (databases will store full IDs later)
357        //
358        // SAFETY: The temporary instance has empty users_db_id and databases_db_id placeholders.
359        // This is safe because:
360        // 1. We only use it to create new system databases via Database::create()
361        // 2. Database::create() doesn't access the instance's system database IDs
362        // 3. The system databases don't exist yet, so their IDs can't be referenced
363        // 4. The temporary instance is only used during initial setup and discarded
364        // 5. The real instance is constructed afterward with the correct database IDs
365        let temp_instance = Self {
366            inner: Arc::new(InstanceInternal {
367                backend: Backend::new(Arc::clone(&backend)),
368                sync: std::sync::OnceLock::new(),
369                users_db_id: ID::from(""), // Placeholder - system DBs don't exist yet
370                databases_db_id: ID::from(""), // Placeholder - system DBs don't exist yet
371                write_callbacks: Mutex::new(HashMap::new()),
372                global_write_callbacks: Mutex::new(HashMap::new()),
373            }),
374        };
375        let users_db = create_users_database(&temp_instance, &device_key, &device_pubkey_str)?;
376        let databases_db =
377            create_databases_tracking(&temp_instance, &device_key, &device_pubkey_str)?;
378
379        // 3. Store root IDs and return instance
380        let inner = Arc::new(InstanceInternal {
381            backend: Backend::new(backend),
382            sync: std::sync::OnceLock::new(),
383            users_db_id: users_db.root_id().clone(),
384            databases_db_id: databases_db.root_id().clone(),
385            write_callbacks: Mutex::new(HashMap::new()),
386            global_write_callbacks: Mutex::new(HashMap::new()),
387        });
388
389        Ok(Self { inner })
390    }
391
392    /// Get a reference to the backend
393    pub fn backend(&self) -> &Backend {
394        &self.inner.backend
395    }
396
397    // === Backend pass-through methods (pub(crate) for internal use) ===
398
399    /// Get an entry from the backend
400    pub(crate) fn get(&self, id: &crate::entry::ID) -> Result<crate::entry::Entry> {
401        self.inner.backend.get(id)
402    }
403
404    /// Put an entry into the backend
405    pub(crate) fn put(
406        &self,
407        verification_status: crate::backend::VerificationStatus,
408        entry: crate::entry::Entry,
409    ) -> Result<()> {
410        self.inner.backend.put(verification_status, entry)
411    }
412
413    /// Get tips for a tree
414    pub(crate) fn get_tips(&self, tree: &crate::entry::ID) -> Result<Vec<crate::entry::ID>> {
415        self.inner.backend.get_tips(tree)
416    }
417
418    // === System database accessors ===
419
420    /// Get the _users database
421    ///
422    /// This constructs a Database instance on-the-fly to avoid circular references.
423    pub(crate) fn users_db(&self) -> Result<Database> {
424        let device_key = self
425            .inner
426            .backend
427            .get_private_key(DEVICE_KEY_NAME)?
428            .ok_or(InstanceError::DeviceKeyNotFound)?;
429
430        Database::open(
431            self.clone(),
432            &self.inner.users_db_id,
433            device_key,
434            "_device_key".to_string(),
435        )
436    }
437
438    // === User Management ===
439
440    /// Create a new user account with flexible password handling.
441    ///
442    /// Creates a user with or without password protection. Passwordless users are appropriate
443    /// for embedded applications where filesystem access = database access.
444    ///
445    /// # Arguments
446    /// * `user_id` - Unique user identifier (username)
447    /// * `password` - Optional password. If None, user is passwordless (instant login, no encryption)
448    ///
449    /// # Returns
450    /// A Result containing the user's UUID (stable internal identifier)
451    pub fn create_user(&self, user_id: &str, password: Option<&str>) -> Result<String> {
452        use crate::user::system_databases::create_user;
453
454        let users_db = self.users_db()?;
455        let (user_uuid, _user_info) = create_user(&users_db, self, user_id, password)?;
456        Ok(user_uuid)
457    }
458
459    /// Login a user with flexible password handling.
460    ///
461    /// Returns a User session object that provides access to user operations.
462    /// For password-protected users, provide the password. For passwordless users, pass None.
463    ///
464    /// # Arguments
465    /// * `user_id` - User identifier (username)
466    /// * `password` - Optional password. None for passwordless users.
467    ///
468    /// # Returns
469    /// A Result containing the User session
470    pub fn login_user(&self, user_id: &str, password: Option<&str>) -> Result<User> {
471        use crate::user::system_databases::login_user;
472
473        let users_db = self.users_db()?;
474        login_user(&users_db, self, user_id, password)
475    }
476
477    /// List all user IDs.
478    ///
479    /// # Returns
480    /// A Result containing a vector of user IDs
481    pub fn list_users(&self) -> Result<Vec<String>> {
482        use crate::user::system_databases::list_users;
483
484        let users_db = self.users_db()?;
485        list_users(&users_db)
486    }
487
488    // === User-Sync Integration ===
489
490    // === Device Identity Management ===
491    //
492    // The Instance's device identity (_device_key) is stored in the backend.
493
494    /// Get the device ID (public key).
495    ///
496    /// The device key (_device_key) is stored in the backend.
497    ///
498    /// # Returns
499    /// A `Result` containing the device's public key (device ID).
500    pub fn device_id(&self) -> Result<VerifyingKey> {
501        let device_key = self
502            .inner
503            .backend
504            .get_private_key(DEVICE_KEY_NAME)?
505            .ok_or_else(|| crate::Error::from(InstanceError::DeviceKeyNotFound))?;
506        Ok(device_key.verifying_key())
507    }
508
509    /// Get the device ID as a formatted string.
510    ///
511    /// This is a convenience method that returns the device ID (public key)
512    /// in a standard formatted string representation.
513    ///
514    /// # Returns
515    /// A `Result` containing the formatted device ID string.
516    pub fn device_id_string(&self) -> Result<String> {
517        let device_key = self.device_id()?;
518        Ok(format_public_key(&device_key))
519    }
520
521    /// Load an existing database from the backend by its root ID.
522    ///
523    /// # Arguments
524    /// * `root_id` - The content-addressable ID of the root `Entry` of the database to load.
525    ///
526    /// # Returns
527    /// A `Result` containing the loaded `Database` or an error if the root ID is not found.
528    pub fn load_database(&self, root_id: &ID) -> Result<Database> {
529        // First validate the root_id exists in the backend
530        // Make sure the entry exists
531        self.inner.backend.get(root_id)?;
532
533        // Create a database object with the given root_id
534        let database = Database::open_readonly(root_id.clone(), self)?;
535        Ok(database)
536    }
537
538    /// Load all databases stored in the backend.
539    ///
540    /// This retrieves all known root entry IDs from the backend and constructs
541    /// `Database` instances for each.
542    ///
543    /// # Returns
544    /// A `Result` containing a vector of all `Database` instances or an error.
545    pub fn all_databases(&self) -> Result<Vec<Database>> {
546        let root_ids = self.inner.backend.all_roots()?;
547        let mut databases = Vec::new();
548
549        for root_id in root_ids {
550            let database = Database::open_readonly(root_id.clone(), self)?;
551            databases.push(database);
552        }
553
554        Ok(databases)
555    }
556
557    /// Find databases by their assigned name.
558    ///
559    /// Searches through all databases in the backend and returns those whose "name"
560    /// setting matches the provided name.
561    ///
562    /// # Arguments
563    /// * `name` - The name to search for.
564    ///
565    /// # Returns
566    /// A `Result` containing a vector of `Database` instances whose name matches,
567    /// or an error.
568    ///
569    /// # Errors
570    /// Returns `InstanceError::DatabaseNotFound` if no databases with the specified name are found.
571    pub fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
572        let name = name.as_ref();
573        let all_databases = self.all_databases()?;
574        let mut matching_databases = Vec::new();
575
576        for database in all_databases {
577            // Attempt to get the name from the database's settings
578            if let Ok(database_name) = database.get_name()
579                && database_name == name
580            {
581                matching_databases.push(database);
582            }
583            // Ignore databases where getting the name fails or doesn't match
584        }
585
586        if matching_databases.is_empty() {
587            Err(InstanceError::DatabaseNotFound {
588                name: name.to_string(),
589            }
590            .into())
591        } else {
592            Ok(matching_databases)
593        }
594    }
595
596    // === Authentication Key Management ===
597
598    /// List all private key IDs.
599    ///
600    /// # Returns
601    /// A `Result` containing a vector of key IDs or an error.
602    pub fn list_private_keys(&self) -> Result<Vec<String>> {
603        // List keys from backend storage
604        self.inner.backend.list_private_keys()
605    }
606
607    // === Synchronization Management ===
608    //
609    // These methods provide access to the Sync module for managing synchronization
610    // settings and state for this database instance.
611
612    /// Initializes the Sync module for this instance.
613    ///
614    /// Enables synchronization operations for this instance. This method is idempotent;
615    /// calling it multiple times has no effect.
616    ///
617    /// # Errors
618    /// Returns an error if the sync settings database cannot be created or if device key
619    /// generation/storage fails.
620    pub fn enable_sync(&self) -> Result<()> {
621        // Check if there is an existing Sync database already configured
622        if self.inner.sync.get().is_some() {
623            return Ok(());
624        }
625        let sync = Sync::new(self.clone())?;
626        let sync_arc = Arc::new(sync);
627
628        // Register global callback for automatic sync on local writes
629        let sync_for_callback = Arc::clone(&sync_arc);
630        self.register_global_write_callback(
631            WriteSource::Local,
632            move |entry, database, instance| {
633                sync_for_callback.on_local_write(entry, database, instance)
634            },
635        )?;
636
637        let _ = self.inner.sync.set(sync_arc);
638        Ok(())
639    }
640
641    /// Get a reference to the Sync module.
642    ///
643    /// Returns a cheap-to-clone Arc handle to the Sync module. The Sync module
644    /// uses interior mutability (AtomicBool and OnceLock) so &self methods are sufficient.
645    ///
646    /// # Returns
647    /// An `Option` containing an `Arc<Sync>` if the Sync module is initialized.
648    pub fn sync(&self) -> Option<Arc<Sync>> {
649        self.inner.sync.get().map(Arc::clone)
650    }
651
652    // === Entry Write Coordination ===
653    //
654    // All entry writes go through Instance::put_entry() which handles backend storage
655    // and callback dispatch. This centralizes write coordination and ensures hooks fire.
656
657    /// Register a callback to be invoked when entries are written to a database.
658    ///
659    /// The callback receives the entry, database, and instance as parameters.
660    ///
661    /// # Arguments
662    /// * `source` - The write source to monitor (Local or Remote)
663    /// * `tree_id` - The root ID of the database tree to monitor
664    /// * `callback` - Function to invoke on writes
665    ///
666    /// # Returns
667    /// A Result indicating success or failure
668    pub(crate) fn register_write_callback<F>(
669        &self,
670        source: WriteSource,
671        tree_id: ID,
672        callback: F,
673    ) -> Result<()>
674    where
675        F: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync + 'static,
676    {
677        let mut callbacks = self.inner.write_callbacks.lock().unwrap();
678        callbacks
679            .entry((source, tree_id))
680            .or_default()
681            .push(Arc::new(callback));
682        Ok(())
683    }
684
685    /// Register a global callback to be invoked on all writes of a specific source.
686    ///
687    /// Global callbacks are invoked for all writes of the specified source across all databases.
688    /// This is useful for system-wide operations like synchronization that need to track
689    /// changes across all databases.
690    ///
691    /// # Arguments
692    /// * `source` - The write source to monitor (Local or Remote)
693    /// * `callback` - Function to invoke on all writes
694    ///
695    /// # Returns
696    /// A Result indicating success or failure
697    pub(crate) fn register_global_write_callback<F>(
698        &self,
699        source: WriteSource,
700        callback: F,
701    ) -> Result<()>
702    where
703        F: Fn(&Entry, &Database, &Instance) -> Result<()> + Send + std::marker::Sync + 'static,
704    {
705        let mut callbacks = self.inner.global_write_callbacks.lock().unwrap();
706        callbacks
707            .entry(source)
708            .or_default()
709            .push(Arc::new(callback));
710        Ok(())
711    }
712
713    /// Write an entry to the backend and dispatch callbacks.
714    ///
715    /// This is the central coordination point for all entry writes in the system.
716    /// All writes must go through this method to ensure:
717    /// - Entries are persisted to the backend
718    /// - Appropriate callbacks are triggered based on write source
719    /// - Hooks have full context (entry, database, instance)
720    ///
721    /// # Arguments
722    /// * `tree_id` - The root ID of the database being written to
723    /// * `verification` - Authentication verification status of the entry
724    /// * `entry` - The entry to write
725    /// * `source` - Whether this is a local or remote write
726    ///
727    /// # Returns
728    /// A Result indicating success or failure
729    pub fn put_entry(
730        &self,
731        tree_id: &ID,
732        verification: crate::backend::VerificationStatus,
733        entry: Entry,
734        source: WriteSource,
735    ) -> Result<()> {
736        // 1. Persist to backend storage
737        self.backend().put(verification, entry.clone())?;
738
739        // 2. Look up and execute callbacks based on write source
740        // Clone the callbacks to avoid holding the lock while executing callbacks.
741        let per_db_callbacks = self
742            .inner
743            .write_callbacks
744            .lock()
745            .unwrap()
746            .get(&(source, tree_id.clone()))
747            .cloned();
748
749        let global_callbacks = self
750            .inner
751            .global_write_callbacks
752            .lock()
753            .unwrap()
754            .get(&source)
755            .cloned();
756
757        // 3. Execute callbacks if any are registered
758        let has_callbacks = per_db_callbacks.is_some() || global_callbacks.is_some();
759        if has_callbacks {
760            // Create a Database handle for the callbacks
761            // Use open_readonly since we only need it for callback context
762            let database = Database::open_readonly(tree_id.clone(), self)?;
763
764            // Execute per-database callbacks
765            if let Some(callbacks) = per_db_callbacks {
766                for callback in callbacks {
767                    if let Err(e) = callback(&entry, &database, self) {
768                        tracing::error!(
769                            tree_id = %tree_id,
770                            entry_id = %entry.id(),
771                            source = ?source,
772                            "Per-database callback failed: {}", e
773                        );
774                        // Continue executing other callbacks even if one fails
775                    }
776                }
777            }
778
779            // Execute global callbacks
780            if let Some(callbacks) = global_callbacks {
781                for callback in callbacks {
782                    if let Err(e) = callback(&entry, &database, self) {
783                        tracing::error!(
784                            tree_id = %tree_id,
785                            entry_id = %entry.id(),
786                            source = ?source,
787                            "Global callback failed: {}", e
788                        );
789                        // Continue executing other callbacks even if one fails
790                    }
791                }
792            }
793        }
794
795        Ok(())
796    }
797
798    /// Downgrade to a weak reference.
799    ///
800    /// Creates a weak reference that does not prevent the Instance from being dropped.
801    /// This is useful for preventing circular reference cycles in dependent objects.
802    ///
803    /// # Returns
804    /// A `WeakInstance` that can be upgraded back to a strong reference.
805    pub fn downgrade(&self) -> WeakInstance {
806        WeakInstance {
807            inner: Arc::downgrade(&self.inner),
808        }
809    }
810}
811
812impl WeakInstance {
813    /// Upgrade to a strong reference.
814    ///
815    /// Attempts to upgrade this weak reference to a strong `Instance` reference.
816    /// Returns `None` if the Instance has already been dropped.
817    ///
818    /// # Returns
819    /// `Some(Instance)` if the Instance still exists, `None` otherwise.
820    ///
821    /// # Example
822    /// ```
823    /// # use eidetica::{backend::database::InMemory, Instance};
824    /// let instance = Instance::open(Box::new(InMemory::new()))?;
825    /// let weak = instance.downgrade();
826    ///
827    /// // Upgrade works while instance exists
828    /// assert!(weak.upgrade().is_some());
829    ///
830    /// drop(instance);
831    /// // Upgrade fails after instance is dropped
832    /// assert!(weak.upgrade().is_none());
833    /// # Ok::<(), eidetica::Error>(())
834    /// ```
835    pub fn upgrade(&self) -> Option<Instance> {
836        self.inner.upgrade().map(|inner| Instance { inner })
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use crate::{Error, backend::database::InMemory, crdt::Doc, instance::LegacyInstanceOps};
844
845    #[test]
846    fn test_create_user() -> Result<(), Error> {
847        let backend = InMemory::new();
848        let instance = Instance::open(Box::new(backend))?;
849
850        // Create user with password
851        let user_uuid = instance.create_user("alice", Some("password123")).unwrap();
852
853        assert!(!user_uuid.is_empty());
854
855        // Verify user appears in list
856        let users = instance.list_users().unwrap();
857        assert_eq!(users.len(), 1);
858        assert_eq!(users[0], "alice");
859        Ok(())
860    }
861
862    #[test]
863    fn test_login_user() -> Result<(), Error> {
864        let backend = InMemory::new();
865        let instance = Instance::open(Box::new(backend))?;
866
867        // Create user
868        instance.create_user("alice", Some("password123")).unwrap();
869
870        // Login user
871        let user = instance.login_user("alice", Some("password123")).unwrap();
872        assert_eq!(user.username(), "alice");
873
874        // Invalid password should fail
875        let result = instance.login_user("alice", Some("wrong_password"));
876        assert!(result.is_err());
877        Ok(())
878    }
879
880    #[test]
881    fn test_new_database() {
882        let backend = InMemory::new();
883        let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
884
885        // Create database with deprecated API
886        let mut settings = Doc::new();
887        settings.set_string("name", "test_db");
888
889        let database = instance.new_database(settings, "_device_key").unwrap();
890        assert_eq!(database.get_name().unwrap(), "test_db");
891    }
892
893    #[test]
894    fn test_new_database_default() {
895        let backend = InMemory::new();
896        let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
897
898        // Create database with default settings
899        let database = instance.new_database_default("_device_key").unwrap();
900        let settings = database.get_settings().unwrap();
901
902        // Should have auto-generated database_id
903        assert!(settings.get_string("database_id").is_ok());
904    }
905
906    #[test]
907    fn test_new_database_without_key_fails() -> Result<(), Error> {
908        let backend = InMemory::new();
909        let instance = Instance::open(Box::new(backend))?;
910
911        // Create database requires a signing key
912        let mut settings = Doc::new();
913        settings.set_string("name", "test_db");
914
915        // This will succeed if a valid key is provided, but we're testing without a valid key
916        let result = instance.new_database(settings, "nonexistent_key");
917        assert!(result.is_err());
918        Ok(())
919    }
920
921    #[test]
922    fn test_load_database() {
923        let backend = InMemory::new();
924        let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
925
926        // Create a database
927        let mut settings = Doc::new();
928        settings.set_string("name", "test_db");
929        let database = instance.new_database(settings, "_device_key").unwrap();
930        let root_id = database.root_id().clone();
931
932        // Load the database
933        let loaded_database = instance.load_database(&root_id).unwrap();
934        assert_eq!(loaded_database.get_name().unwrap(), "test_db");
935    }
936
937    #[test]
938    fn test_all_databases() {
939        let backend = InMemory::new();
940        let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
941
942        // Create multiple databases
943        let mut settings1 = Doc::new();
944        settings1.set_string("name", "db1");
945        instance.new_database(settings1, "_device_key").unwrap();
946
947        let mut settings2 = Doc::new();
948        settings2.set_string("name", "db2");
949        instance.new_database(settings2, "_device_key").unwrap();
950
951        // Get all databases (should include system databases + user databases)
952        let databases = instance.all_databases().unwrap();
953        assert!(databases.len() >= 2); // At least our 2 databases + system databases
954    }
955
956    #[test]
957    fn test_find_database() {
958        let backend = InMemory::new();
959        let instance = Instance::open(Box::new(backend)).expect("Failed to create test instance");
960
961        // Create database with name
962        let mut settings = Doc::new();
963        settings.set_string("name", "my_special_db");
964        instance.new_database(settings, "_device_key").unwrap();
965
966        // Find by name
967        let found = instance.find_database("my_special_db").unwrap();
968        assert_eq!(found.len(), 1);
969        assert_eq!(found[0].get_name().unwrap(), "my_special_db");
970
971        // Not found
972        let result = instance.find_database("nonexistent");
973        assert!(result.is_err());
974    }
975
976    #[test]
977    fn test_instance_load_new_backend() -> Result<(), Error> {
978        // Test that Instance::load() creates new system state for empty backend
979        let backend = InMemory::new();
980        let instance = Instance::open(Box::new(backend))?;
981
982        // Verify device key was created
983        assert!(instance.device_id().is_ok());
984
985        // Verify we can create and login a user
986        instance.create_user("alice", None)?;
987        let user = instance.login_user("alice", None)?;
988        assert_eq!(user.username(), "alice");
989
990        Ok(())
991    }
992
993    #[test]
994    fn test_instance_load_existing_backend() -> Result<(), Error> {
995        // Use a temporary file path for testing
996        let temp_dir = std::env::temp_dir();
997        let path = temp_dir.join("eidetica_test_instance_load.json");
998
999        // Create an instance and user, then save the backend
1000        let backend1 = InMemory::new();
1001        let instance1 = Instance::open(Box::new(backend1))?;
1002        instance1.create_user("bob", None)?;
1003        let mut user1 = instance1.login_user("bob", None)?;
1004
1005        // Get the default key (earliest created key)
1006        let default_key = user1.get_default_key()?;
1007
1008        // Create a user database to verify it persists
1009        let mut settings = Doc::new();
1010        settings.set_string("name", "bob_database");
1011        user1.create_database(settings, &default_key)?;
1012
1013        // Save the backend to file
1014        let backend_guard = instance1.backend();
1015        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1016            in_memory.save_to_file(&path)?;
1017        }
1018
1019        // Drop the first instance
1020        drop(instance1);
1021        drop(user1);
1022
1023        // Load a new backend from the saved file
1024        let backend2 = InMemory::load_from_file(&path)?;
1025        let instance2 = Instance::open(Box::new(backend2))?;
1026
1027        // Verify the user still exists
1028        let users = instance2.list_users()?;
1029        assert_eq!(users.len(), 1);
1030        assert_eq!(users[0], "bob");
1031
1032        // Verify we can login the existing user
1033        let user2 = instance2.login_user("bob", None)?;
1034        assert_eq!(user2.username(), "bob");
1035
1036        // Clean up the temporary file
1037        if path.exists() {
1038            std::fs::remove_file(&path).ok();
1039        }
1040
1041        Ok(())
1042    }
1043
1044    #[test]
1045    fn test_instance_load_device_id_persistence() -> Result<(), Error> {
1046        // Test that device_id remains the same across reloads
1047        let temp_dir = std::env::temp_dir();
1048        let path = temp_dir.join("eidetica_test_device_id.json");
1049
1050        // Create instance and get device_id
1051        let backend1 = InMemory::new();
1052        let instance1 = Instance::open(Box::new(backend1))?;
1053        let device_id1 = instance1.device_id_string()?;
1054
1055        // Save backend
1056        let backend_guard = instance1.backend();
1057        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1058            in_memory.save_to_file(&path)?;
1059        }
1060        drop(instance1);
1061
1062        // Load backend and verify device_id is the same
1063        let backend2 = InMemory::load_from_file(&path)?;
1064        let instance2 = Instance::open(Box::new(backend2))?;
1065        let device_id2 = instance2.device_id_string()?;
1066
1067        assert_eq!(
1068            device_id1, device_id2,
1069            "Device ID should persist across reloads"
1070        );
1071
1072        // Clean up
1073        if path.exists() {
1074            std::fs::remove_file(&path).ok();
1075        }
1076
1077        Ok(())
1078    }
1079
1080    #[test]
1081    fn test_instance_load_with_password_protected_users() -> Result<(), Error> {
1082        // Test that password-protected users work correctly after reload
1083        let temp_dir = std::env::temp_dir();
1084        let path = temp_dir.join("eidetica_test_password_users.json");
1085
1086        // Create instance with password-protected user
1087        let backend1 = InMemory::new();
1088        let instance1 = Instance::open(Box::new(backend1))?;
1089        instance1.create_user("secure_alice", Some("secret123"))?;
1090        let user1 = instance1.login_user("secure_alice", Some("secret123"))?;
1091        assert_eq!(user1.username(), "secure_alice");
1092        drop(user1);
1093
1094        // Save backend
1095        let backend_guard = instance1.backend();
1096        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1097            in_memory.save_to_file(&path)?;
1098        }
1099        drop(instance1);
1100
1101        // Reload and verify password still works
1102        let backend2 = InMemory::load_from_file(&path)?;
1103        let instance2 = Instance::open(Box::new(backend2))?;
1104
1105        // Correct password should work
1106        let user2 = instance2.login_user("secure_alice", Some("secret123"))?;
1107        assert_eq!(user2.username(), "secure_alice");
1108
1109        // Wrong password should fail
1110        let result = instance2.login_user("secure_alice", Some("wrong_password"));
1111        assert!(result.is_err(), "Login with wrong password should fail");
1112
1113        // No password should fail
1114        let result = instance2.login_user("secure_alice", None);
1115        assert!(
1116            result.is_err(),
1117            "Login without password should fail for password-protected user"
1118        );
1119
1120        // Clean up
1121        if path.exists() {
1122            std::fs::remove_file(&path).ok();
1123        }
1124
1125        Ok(())
1126    }
1127
1128    #[test]
1129    fn test_instance_load_multiple_users() -> Result<(), Error> {
1130        // Test that multiple users persist correctly
1131        let temp_dir = std::env::temp_dir();
1132        let path = temp_dir.join("eidetica_test_multiple_users.json");
1133
1134        // Create instance with multiple users (mix of passwordless and password-protected)
1135        let backend1 = InMemory::new();
1136        let instance1 = Instance::open(Box::new(backend1))?;
1137
1138        instance1.create_user("alice", None)?;
1139        instance1.create_user("bob", Some("bobpass"))?;
1140        instance1.create_user("charlie", None)?;
1141        instance1.create_user("diana", Some("dianapass"))?;
1142
1143        // Verify all users can login
1144        instance1.login_user("alice", None)?;
1145        instance1.login_user("bob", Some("bobpass"))?;
1146        instance1.login_user("charlie", None)?;
1147        instance1.login_user("diana", Some("dianapass"))?;
1148
1149        // Save backend
1150        let backend_guard = instance1.backend();
1151        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1152            in_memory.save_to_file(&path)?;
1153        }
1154        drop(instance1);
1155
1156        // Reload and verify all users still exist and can login
1157        let backend2 = InMemory::load_from_file(&path)?;
1158        let instance2 = Instance::open(Box::new(backend2))?;
1159
1160        let users = instance2.list_users()?;
1161        assert_eq!(users.len(), 4, "All 4 users should be present after reload");
1162        assert!(users.contains(&"alice".to_string()));
1163        assert!(users.contains(&"bob".to_string()));
1164        assert!(users.contains(&"charlie".to_string()));
1165        assert!(users.contains(&"diana".to_string()));
1166
1167        // Verify login still works for all users
1168        instance2.login_user("alice", None)?;
1169        instance2.login_user("bob", Some("bobpass"))?;
1170        instance2.login_user("charlie", None)?;
1171        instance2.login_user("diana", Some("dianapass"))?;
1172
1173        // Clean up
1174        if path.exists() {
1175            std::fs::remove_file(&path).ok();
1176        }
1177
1178        Ok(())
1179    }
1180
1181    #[test]
1182    fn test_instance_load_user_databases_persist() -> Result<(), Error> {
1183        // Test that user-created databases persist across reloads
1184        let temp_dir = std::env::temp_dir();
1185        let path = temp_dir.join("eidetica_test_user_dbs.json");
1186
1187        // Create instance, user, and multiple databases
1188        let backend1 = InMemory::new();
1189        let instance1 = Instance::open(Box::new(backend1))?;
1190        instance1.create_user("eve", None)?;
1191        let mut user1 = instance1.login_user("eve", None)?;
1192
1193        // Get the default key (earliest created key)
1194        let default_key = user1.get_default_key()?;
1195
1196        // Create multiple databases
1197        let mut settings1 = Doc::new();
1198        settings1.set_string("name", "database_one");
1199        settings1.set_string("purpose", "testing");
1200        let db1 = user1.create_database(settings1, &default_key)?;
1201        let db1_root = db1.root_id().clone();
1202
1203        let mut settings2 = Doc::new();
1204        settings2.set_string("name", "database_two");
1205        settings2.set_string("purpose", "production");
1206        let db2 = user1.create_database(settings2, &default_key)?;
1207        let db2_root = db2.root_id().clone();
1208
1209        drop(db1);
1210        drop(db2);
1211        drop(user1);
1212
1213        // Save backend
1214        let backend_guard = instance1.backend();
1215        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1216            in_memory.save_to_file(&path)?;
1217        }
1218        drop(instance1);
1219
1220        // Reload and verify databases still exist
1221        let backend2 = InMemory::load_from_file(&path)?;
1222        let instance2 = Instance::open(Box::new(backend2))?;
1223        let _user2 = instance2.login_user("eve", None)?;
1224
1225        // Load databases by root_id and verify their settings
1226        let loaded_db1 = instance2.load_database(&db1_root)?;
1227        assert_eq!(loaded_db1.get_name()?, "database_one");
1228        let settings1_doc = loaded_db1.get_settings()?;
1229        assert_eq!(settings1_doc.get_string("purpose")?, "testing");
1230
1231        let loaded_db2 = instance2.load_database(&db2_root)?;
1232        assert_eq!(loaded_db2.get_name()?, "database_two");
1233        let settings2_doc = loaded_db2.get_settings()?;
1234        assert_eq!(settings2_doc.get_string("purpose")?, "production");
1235
1236        // Clean up
1237        if path.exists() {
1238            std::fs::remove_file(&path).ok();
1239        }
1240
1241        Ok(())
1242    }
1243
1244    #[test]
1245    fn test_instance_load_idempotency() -> Result<(), Error> {
1246        // Test that loading the same backend multiple times gives consistent results
1247        let temp_dir = std::env::temp_dir();
1248        let path = temp_dir.join("eidetica_test_idempotency.json");
1249
1250        // Create and save initial state
1251        let backend1 = InMemory::new();
1252        let instance1 = Instance::open(Box::new(backend1))?;
1253        instance1.create_user("frank", None)?;
1254        let device_id1 = instance1.device_id_string()?;
1255
1256        let backend_guard = instance1.backend();
1257        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1258            in_memory.save_to_file(&path)?;
1259        }
1260        drop(instance1);
1261
1262        // Load the same backend multiple times and verify consistency
1263        for i in 0..3 {
1264            let backend = InMemory::load_from_file(&path)?;
1265            let instance = Instance::open(Box::new(backend))?;
1266
1267            // Device ID should be the same every time
1268            let device_id = instance.device_id_string()?;
1269            assert_eq!(
1270                device_id, device_id1,
1271                "Device ID should be consistent on reload {i}"
1272            );
1273
1274            // User list should be the same
1275            let users = instance.list_users()?;
1276            assert_eq!(users.len(), 1);
1277            assert_eq!(users[0], "frank");
1278
1279            // Should be able to login
1280            let user = instance.login_user("frank", None)?;
1281            assert_eq!(user.username(), "frank");
1282
1283            drop(user);
1284            drop(instance);
1285        }
1286
1287        // Clean up
1288        if path.exists() {
1289            std::fs::remove_file(&path).ok();
1290        }
1291
1292        Ok(())
1293    }
1294
1295    #[test]
1296    fn test_instance_load_new_vs_existing() -> Result<(), Error> {
1297        // Test the difference between loading new and existing backends
1298        let temp_dir = std::env::temp_dir();
1299        let path = temp_dir.join("eidetica_test_new_vs_existing.json");
1300
1301        // Create first instance (new backend)
1302        let backend1 = InMemory::new();
1303        let instance1 = Instance::open(Box::new(backend1))?;
1304        let device_id1 = instance1.device_id_string()?;
1305        instance1.create_user("grace", None)?;
1306
1307        let backend_guard = instance1.backend();
1308        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1309            in_memory.save_to_file(&path)?;
1310        }
1311        drop(instance1);
1312
1313        // Load existing backend
1314        let backend2 = InMemory::load_from_file(&path)?;
1315        let instance2 = Instance::open(Box::new(backend2))?;
1316        let device_id2 = instance2.device_id_string()?;
1317
1318        // Device ID should match (existing backend)
1319        assert_eq!(device_id1, device_id2);
1320
1321        // User should exist (existing backend)
1322        let users = instance2.list_users()?;
1323        assert_eq!(users.len(), 1);
1324        assert_eq!(users[0], "grace");
1325        drop(instance2);
1326
1327        // Create completely new instance (different backend)
1328        let backend3 = InMemory::new();
1329        let instance3 = Instance::open(Box::new(backend3))?;
1330        let device_id3 = instance3.device_id_string()?;
1331
1332        // Device ID should be different (new backend)
1333        assert_ne!(device_id1, device_id3);
1334
1335        // No users should exist (new backend)
1336        let users = instance3.list_users()?;
1337        assert_eq!(users.len(), 0);
1338
1339        // Clean up
1340        if path.exists() {
1341            std::fs::remove_file(&path).ok();
1342        }
1343
1344        Ok(())
1345    }
1346
1347    #[test]
1348    fn test_instance_create_strict_fails_on_existing() -> Result<(), Error> {
1349        // Test that Instance::create() fails on already-initialized backend
1350        let temp_dir = std::env::temp_dir();
1351        let path = temp_dir.join("eidetica_test_create_strict.json");
1352
1353        // Create first instance
1354        let backend1 = InMemory::new();
1355        let instance1 = Instance::create(Box::new(backend1))?;
1356        instance1.create_user("alice", None)?;
1357
1358        // Save backend
1359        let backend_guard = instance1.backend();
1360        if let Some(in_memory) = backend_guard.as_any().downcast_ref::<InMemory>() {
1361            in_memory.save_to_file(&path)?;
1362        }
1363        drop(instance1);
1364
1365        // Try to create() on the existing backend - should fail
1366        let backend2 = InMemory::load_from_file(&path)?;
1367        let result = Instance::create(Box::new(backend2));
1368        assert!(result.is_err(), "create() should fail on existing backend");
1369
1370        // Verify error type
1371        if let Err(err) = result {
1372            if let crate::Error::Instance(instance_err) = err {
1373                assert!(
1374                    instance_err.is_already_exists(),
1375                    "Error should be InstanceAlreadyExists"
1376                );
1377            } else {
1378                panic!("Expected Instance error");
1379            }
1380        }
1381
1382        // Verify open() still works
1383        let backend3 = InMemory::load_from_file(&path)?;
1384        let instance3 = Instance::open(Box::new(backend3))?;
1385        let users = instance3.list_users()?;
1386        assert_eq!(users.len(), 1);
1387        assert_eq!(users[0], "alice");
1388
1389        // Clean up
1390        if path.exists() {
1391            std::fs::remove_file(&path).ok();
1392        }
1393
1394        Ok(())
1395    }
1396
1397    #[test]
1398    fn test_instance_create_on_fresh_backend() -> Result<(), Error> {
1399        // Test that Instance::create() succeeds on fresh backend
1400        let backend = InMemory::new();
1401        let instance = Instance::create(Box::new(backend))?;
1402
1403        // Verify instance is properly initialized
1404        assert!(instance.device_id().is_ok());
1405
1406        // Verify we can create users
1407        instance.create_user("bob", None)?;
1408        let user = instance.login_user("bob", None)?;
1409        assert_eq!(user.username(), "bob");
1410
1411        Ok(())
1412    }
1413}