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}