logins/
store.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4use crate::db::{LoginDb, LoginsDeletionMetrics};
5use crate::encryption::EncryptorDecryptor;
6use crate::error::*;
7use crate::login::{BulkResultEntry, EncryptedLogin, Login, LoginEntry, LoginEntryWithMeta};
8use crate::schema;
9use crate::LoginsSyncEngine;
10use parking_lot::Mutex;
11use sql_support::run_maintenance;
12use std::path::Path;
13use std::sync::{Arc, Weak};
14use sync15::{
15    engine::{EngineSyncAssociation, SyncEngine, SyncEngineId},
16    ServerTimestamp,
17};
18
19#[derive(uniffi::Enum)]
20pub enum LoginOrErrorMessage {
21    Login,
22    String,
23}
24
25// Our "sync manager" will use whatever is stashed here.
26lazy_static::lazy_static! {
27    // Mutex: just taken long enough to update the inner stuff - needed
28    //        to wrap the RefCell as they aren't `Sync`
29    static ref STORE_FOR_MANAGER: Mutex<Weak<LoginStore>> = Mutex::new(Weak::new());
30}
31
32/// Called by the sync manager to get a sync engine via the store previously
33/// registered with the sync manager.
34pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
35    let weak = STORE_FOR_MANAGER.lock();
36    match weak.upgrade() {
37        None => None,
38        Some(store) => match create_sync_engine(store, engine_id) {
39            Ok(engine) => Some(engine),
40            Err(e) => {
41                report_error!("logins-sync-engine-create-error", "{e}");
42                None
43            }
44        },
45    }
46}
47
48fn create_sync_engine(
49    store: Arc<LoginStore>,
50    engine_id: &SyncEngineId,
51) -> Result<Box<dyn SyncEngine>> {
52    match engine_id {
53        SyncEngineId::Passwords => Ok(Box::new(LoginsSyncEngine::new(Arc::clone(&store))?)),
54        // panicking here seems reasonable - it's a static error if this
55        // it hit, not something that runtime conditions can influence.
56        _ => unreachable!("can't provide unknown engine: {}", engine_id),
57    }
58}
59
60fn map_bulk_result_entry(
61    enc_login: Result<EncryptedLogin>,
62    encdec: &dyn EncryptorDecryptor,
63) -> BulkResultEntry {
64    match enc_login {
65        Ok(enc_login) => match enc_login.decrypt(encdec) {
66            Ok(login) => BulkResultEntry::Success { login },
67            Err(error) => {
68                warn!("Login could not be decrypted. This indicates a fundamental problem with the encryption key.");
69                BulkResultEntry::Error {
70                    message: error.to_string(),
71                }
72            }
73        },
74        Err(error) => BulkResultEntry::Error {
75            message: error.to_string(),
76        },
77    }
78}
79
80pub struct LoginStore {
81    pub db: Mutex<Option<LoginDb>>,
82}
83
84impl LoginStore {
85    #[handle_error(Error)]
86    pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
87        let db = Mutex::new(Some(LoginDb::open(path, encdec)?));
88        Ok(Self { db })
89    }
90
91    pub fn new_from_db(db: LoginDb) -> Self {
92        let db = Mutex::new(Some(db));
93        Self { db }
94    }
95
96    // Only used for tests, but it's `pub` the `sync-test` crate uses it.
97    #[cfg(test)]
98    pub fn new_in_memory() -> Self {
99        let db = Mutex::new(Some(LoginDb::open_in_memory()));
100        Self { db }
101    }
102
103    pub fn lock_db(&self) -> Result<parking_lot::MappedMutexGuard<'_, LoginDb>> {
104        parking_lot::MutexGuard::try_map(self.db.lock(), |db| db.as_mut())
105            .map_err(|_| Error::DatabaseClosed)
106    }
107
108    #[handle_error(Error)]
109    pub fn is_empty(&self) -> ApiResult<bool> {
110        Ok(self.lock_db()?.count_all()? == 0)
111    }
112
113    #[handle_error(Error)]
114    pub fn list(&self) -> ApiResult<Vec<Login>> {
115        let db = self.lock_db()?;
116        db.get_all().and_then(|logins| {
117            logins
118                .into_iter()
119                .map(|login| login.decrypt(db.encdec.as_ref()))
120                .collect()
121        })
122    }
123
124    #[handle_error(Error)]
125    pub fn count(&self) -> ApiResult<i64> {
126        self.lock_db()?.count_all()
127    }
128
129    #[handle_error(Error)]
130    pub fn count_by_origin(&self, origin: &str) -> ApiResult<i64> {
131        self.lock_db()?.count_by_origin(origin)
132    }
133
134    #[handle_error(Error)]
135    pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> ApiResult<i64> {
136        self.lock_db()?
137            .count_by_form_action_origin(form_action_origin)
138    }
139
140    #[handle_error(Error)]
141    pub fn get(&self, id: &str) -> ApiResult<Option<Login>> {
142        let db = self.lock_db()?;
143        match db.get_by_id(id) {
144            Ok(result) => match result {
145                Some(enc_login) => enc_login.decrypt(db.encdec.as_ref()).map(Some),
146                None => Ok(None),
147            },
148            Err(err) => Err(err),
149        }
150    }
151
152    #[handle_error(Error)]
153    pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>> {
154        let db = self.lock_db()?;
155        db.get_by_base_domain(base_domain).and_then(|logins| {
156            logins
157                .into_iter()
158                .map(|login| login.decrypt(db.encdec.as_ref()))
159                .collect()
160        })
161    }
162
163    #[handle_error(Error)]
164    pub fn has_logins_by_base_domain(&self, base_domain: &str) -> ApiResult<bool> {
165        self.lock_db()?
166            .get_by_base_domain(base_domain)
167            .map(|logins| !logins.is_empty())
168    }
169
170    #[handle_error(Error)]
171    pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>> {
172        let db = self.lock_db()?;
173        db.find_login_to_update(entry, db.encdec.as_ref())
174    }
175
176    #[handle_error(Error)]
177    pub fn touch(&self, id: &str) -> ApiResult<()> {
178        self.lock_db()?.touch(id)
179    }
180
181    #[handle_error(Error)]
182    pub fn is_potentially_breached(&self, id: &str) -> ApiResult<bool> {
183        self.lock_db()?.is_potentially_breached(id)
184    }
185
186    #[handle_error(Error)]
187    pub fn are_potentially_vulnerable_passwords(&self, ids: Vec<String>) -> ApiResult<Vec<String>> {
188        // Note: Vec<&str> is not supported with UDL, so we receive Vec<String> and convert
189        let db = self.lock_db()?;
190        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
191        db.are_potentially_vulnerable_passwords(&ids, db.encdec.as_ref())
192    }
193
194    #[handle_error(Error)]
195    pub fn is_potentially_vulnerable_password(&self, id: &str) -> ApiResult<bool> {
196        let db = self.lock_db()?;
197        db.is_potentially_vulnerable_password(id, db.encdec.as_ref())
198    }
199
200    #[handle_error(Error)]
201    pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> ApiResult<()> {
202        let db = self.lock_db()?;
203        db.record_potentially_vulnerable_passwords(passwords, db.encdec.as_ref())
204    }
205
206    #[handle_error(Error)]
207    pub fn record_breach(&self, id: &str, timestamp: i64) -> ApiResult<()> {
208        let db = self.lock_db()?;
209        db.record_breach(id, timestamp, db.encdec.as_ref())
210    }
211
212    #[handle_error(Error)]
213    pub fn reset_all_breaches(&self) -> ApiResult<()> {
214        self.lock_db()?.reset_all_breaches()
215    }
216
217    #[handle_error(Error)]
218    pub fn is_breach_alert_dismissed(&self, id: &str) -> ApiResult<bool> {
219        self.lock_db()?.is_breach_alert_dismissed(id)
220    }
221
222    #[handle_error(Error)]
223    pub fn record_breach_alert_dismissal(&self, id: &str) -> ApiResult<()> {
224        self.lock_db()?.record_breach_alert_dismissal(id)
225    }
226
227    #[handle_error(Error)]
228    pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> ApiResult<()> {
229        self.lock_db()?
230            .record_breach_alert_dismissal_time(id, timestamp)
231    }
232
233    #[handle_error(Error)]
234    pub fn delete(&self, id: &str) -> ApiResult<bool> {
235        self.lock_db()?.delete(id)
236    }
237
238    #[handle_error(Error)]
239    pub fn delete_many(&self, ids: Vec<String>) -> ApiResult<Vec<bool>> {
240        // Note we need to receive a vector of String here because `Vec<&str>` is not supported
241        // with UDL.
242        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
243        self.lock_db()?.delete_many(ids)
244    }
245
246    #[handle_error(Error)]
247    pub fn delete_undecryptable_records_for_remote_replacement(
248        self: Arc<Self>,
249    ) -> ApiResult<LoginsDeletionMetrics> {
250        // This function was created for the iOS logins verification logic that will
251        // remove records that prevent logins syncing. Once the verification logic is
252        // removed from iOS, this function can be removed from the store.
253
254        // Creating an engine requires locking the DB, so make sure to do this first
255        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
256
257        let db = self.lock_db()?;
258        let deletion_stats =
259            db.delete_undecryptable_records_for_remote_replacement(db.encdec.as_ref())?;
260        engine.set_last_sync(&db, ServerTimestamp(0))?;
261        Ok(deletion_stats)
262    }
263
264    #[handle_error(Error)]
265    pub fn wipe_local(&self) -> ApiResult<()> {
266        self.lock_db()?.wipe_local()?;
267        Ok(())
268    }
269
270    #[handle_error(Error)]
271    pub fn reset(self: Arc<Self>) -> ApiResult<()> {
272        // Reset should not exist here - all resets should be done via the
273        // sync manager. It seems that actual consumers don't use this, but
274        // some tests do, so it remains for now.
275        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
276        engine.do_reset(&EngineSyncAssociation::Disconnected)?;
277        Ok(())
278    }
279
280    #[handle_error(Error)]
281    pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
282        let db = self.lock_db()?;
283        db.update(id, entry, db.encdec.as_ref())
284            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
285    }
286
287    #[handle_error(Error)]
288    pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
289        let db = self.lock_db()?;
290        db.add(entry, db.encdec.as_ref())
291            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
292    }
293
294    #[handle_error(Error)]
295    pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
296        let db = self.lock_db()?;
297        db.add_many(entries, db.encdec.as_ref()).map(|enc_logins| {
298            enc_logins
299                .into_iter()
300                .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
301                .collect()
302        })
303    }
304
305    /// This method is intended to preserve metadata (LoginMeta) during a migration.
306    /// In normal operation, this method should not be used; instead,
307    /// use `add(entry)`, which manages the corresponding fields itself.
308    #[handle_error(Error)]
309    pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
310        let db = self.lock_db()?;
311        db.add_with_meta(entry_with_meta, db.encdec.as_ref())
312            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
313    }
314
315    #[handle_error(Error)]
316    pub fn add_many_with_meta(
317        &self,
318        entries_with_meta: Vec<LoginEntryWithMeta>,
319    ) -> ApiResult<Vec<BulkResultEntry>> {
320        let db = self.lock_db()?;
321        db.add_many_with_meta(entries_with_meta, db.encdec.as_ref())
322            .map(|enc_logins| {
323                enc_logins
324                    .into_iter()
325                    .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
326                    .collect()
327            })
328    }
329
330    #[handle_error(Error)]
331    pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
332        let db = self.lock_db()?;
333        db.add_or_update(entry, db.encdec.as_ref())
334            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
335    }
336
337    #[handle_error(Error)]
338    pub fn set_checkpoint(&self, checkpoint: &str) -> ApiResult<()> {
339        self.lock_db()?
340            .put_meta(schema::CHECKPOINT_KEY, &checkpoint)
341    }
342
343    #[handle_error(Error)]
344    pub fn get_checkpoint(&self) -> ApiResult<Option<String>> {
345        self.lock_db()?.get_meta(schema::CHECKPOINT_KEY)
346    }
347
348    #[handle_error(Error)]
349    pub fn run_maintenance(&self) -> ApiResult<()> {
350        let conn = self.lock_db()?;
351        run_maintenance(&conn)?;
352        Ok(())
353    }
354
355    pub fn shutdown(&self) {
356        if let Some(db) = self.db.lock().take() {
357            let _ = db.shutdown();
358        }
359    }
360
361    // This allows the embedding app to say "make this instance available to
362    // the sync manager". The implementation is more like "offer to sync mgr"
363    // (thereby avoiding us needing to link with the sync manager) but
364    // `register_with_sync_manager()` is logically what's happening so that's
365    // the name it gets.
366    pub fn register_with_sync_manager(self: Arc<Self>) {
367        let mut state = STORE_FOR_MANAGER.lock();
368        *state = Arc::downgrade(&self);
369    }
370
371    // this isn't exposed by uniffi - currently the
372    // only consumer of this is our "example" (and hence why they
373    // are `pub` and not `pub(crate)`).
374    // We could probably make the example work with the sync manager - but then
375    // our example would link with places and logins etc, and it's not a big
376    // deal really.
377    #[handle_error(Error)]
378    pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
379        Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
380    }
381}
382
383#[cfg(not(feature = "keydb"))]
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use crate::encryption::test_utils::TEST_ENCDEC;
388    use crate::util;
389    use nss::ensure_initialized;
390    use std::cmp::Reverse;
391    use std::time::SystemTime;
392
393    fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
394        assert_eq!(a.origin, b.origin);
395        assert_eq!(a.form_action_origin, b.form_action_origin);
396        assert_eq!(a.http_realm, b.http_realm);
397        assert_eq!(a.username_field, b.username_field);
398        assert_eq!(a.password_field, b.password_field);
399        assert_eq!(b.username, a.username);
400        assert_eq!(b.password, a.password);
401    }
402
403    #[test]
404    fn test_general() {
405        ensure_initialized();
406
407        let store = LoginStore::new_in_memory();
408        let list = store.list().expect("Grabbing Empty list to work");
409        assert_eq!(list.len(), 0);
410        let start_us = util::system_time_ms_i64(SystemTime::now());
411
412        let a = LoginEntry {
413            origin: "https://www.example.com".into(),
414            form_action_origin: Some("https://www.example.com".into()),
415            username_field: "user_input".into(),
416            password_field: "pass_input".into(),
417            username: "coolperson21".into(),
418            password: "p4ssw0rd".into(),
419            ..Default::default()
420        };
421
422        let b = LoginEntry {
423            origin: "https://www.example2.com".into(),
424            http_realm: Some("Some String Here".into()),
425            username: "asdf".into(),
426            password: "fdsa".into(),
427            ..Default::default()
428        };
429        let a_id = store.add(a.clone()).expect("added a").id;
430        let b_id = store.add(b.clone()).expect("added b").id;
431
432        let a_from_db = store
433            .get(&a_id)
434            .expect("Not to error getting a")
435            .expect("a to exist");
436
437        assert_logins_equiv(&a, &a_from_db);
438        assert!(a_from_db.time_created >= start_us);
439        assert!(a_from_db.time_password_changed >= start_us);
440        assert!(a_from_db.time_last_used >= start_us);
441        assert_eq!(a_from_db.times_used, 1);
442
443        let b_from_db = store
444            .get(&b_id)
445            .expect("Not to error getting b")
446            .expect("b to exist");
447
448        assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
449        assert!(b_from_db.time_created >= start_us);
450        assert!(b_from_db.time_password_changed >= start_us);
451        assert!(b_from_db.time_last_used >= start_us);
452        assert_eq!(b_from_db.times_used, 1);
453
454        let mut list = store.list().expect("Grabbing list to work");
455        assert_eq!(list.len(), 2);
456
457        let mut expect = vec![a_from_db, b_from_db.clone()];
458
459        list.sort_by_key(|b| Reverse(b.guid()));
460        expect.sort_by_key(|b| Reverse(b.guid()));
461        assert_eq!(list, expect);
462
463        store.delete(&a_id).expect("Successful delete");
464        assert!(store
465            .get(&a_id)
466            .expect("get after delete should still work")
467            .is_none());
468
469        let list = store.list().expect("Grabbing list to work");
470        assert_eq!(list.len(), 1);
471        assert_eq!(list[0], b_from_db);
472
473        let has_logins = store
474            .has_logins_by_base_domain("example2.com")
475            .expect("Expect a result for this origin");
476        assert!(has_logins);
477
478        let list = store
479            .get_by_base_domain("example2.com")
480            .expect("Expect a list for this origin");
481        assert_eq!(list.len(), 1);
482        assert_eq!(list[0], b_from_db);
483
484        let has_logins = store
485            .has_logins_by_base_domain("www.example.com")
486            .expect("Expect a result for this origin");
487        assert!(!has_logins);
488
489        let list = store
490            .get_by_base_domain("www.example.com")
491            .expect("Expect an empty list");
492        assert_eq!(list.len(), 0);
493
494        let now_us = util::system_time_ms_i64(SystemTime::now());
495        let b2 = LoginEntry {
496            username: b.username.to_owned(),
497            password: "newpass".into(),
498            ..b
499        };
500
501        store
502            .update(&b_id, b2.clone())
503            .expect("update b should work");
504
505        let b_after_update = store
506            .get(&b_id)
507            .expect("Not to error getting b")
508            .expect("b to exist");
509
510        assert_logins_equiv(&b2, &b_after_update);
511        assert!(b_after_update.time_created >= start_us);
512        assert!(b_after_update.time_created <= now_us);
513        assert!(b_after_update.time_password_changed >= now_us);
514        assert!(b_after_update.time_last_used >= now_us);
515        // Should be two even though we updated twice
516        assert_eq!(b_after_update.times_used, 2);
517    }
518
519    #[test]
520    fn test_checkpoint() {
521        ensure_initialized();
522        let store = LoginStore::new_in_memory();
523        let checkpoint = "a-checkpoint";
524        store.set_checkpoint(checkpoint).ok();
525        assert_eq!(store.get_checkpoint().unwrap().unwrap(), checkpoint);
526    }
527
528    #[test]
529    fn test_sync_manager_registration() {
530        ensure_initialized();
531        let store = Arc::new(LoginStore::new_in_memory());
532        assert_eq!(Arc::strong_count(&store), 1);
533        assert_eq!(Arc::weak_count(&store), 0);
534        Arc::clone(&store).register_with_sync_manager();
535        assert_eq!(Arc::strong_count(&store), 1);
536        assert_eq!(Arc::weak_count(&store), 1);
537        let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
538        assert!(Arc::ptr_eq(&store, &registered));
539        drop(registered);
540        // should be no new references
541        assert_eq!(Arc::strong_count(&store), 1);
542        assert_eq!(Arc::weak_count(&store), 1);
543        // dropping the registered object should drop the registration.
544        drop(store);
545        assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
546    }
547
548    #[test]
549    fn test_wipe_local_on_a_fresh_database_is_a_noop() {
550        ensure_initialized();
551        // If the database has data, then wipe_local() returns > 0 rows deleted
552        let db = LoginDb::open_in_memory();
553        db.add_or_update(
554            LoginEntry {
555                origin: "https://www.example.com".into(),
556                form_action_origin: Some("https://www.example.com".into()),
557                username_field: "user_input".into(),
558                password_field: "pass_input".into(),
559                username: "coolperson21".into(),
560                password: "p4ssw0rd".into(),
561                ..Default::default()
562            },
563            &TEST_ENCDEC.clone(),
564        )
565        .unwrap();
566        assert!(db.wipe_local().unwrap() > 0);
567
568        // If the database is empty, then wipe_local() returns 0 rows deleted
569        let db = LoginDb::open_in_memory();
570        assert_eq!(db.wipe_local().unwrap(), 0);
571    }
572
573    #[test]
574    fn test_shutdown() {
575        ensure_initialized();
576        let store = LoginStore::new_in_memory();
577        store.shutdown();
578        assert!(matches!(
579            store.list(),
580            Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
581        ));
582        assert!(store.db.lock().is_none());
583    }
584
585    #[test]
586    fn test_delete_undecryptable_records_for_remote_replacement() {
587        ensure_initialized();
588        let store = Arc::new(LoginStore::new_in_memory());
589        // Not much of a test, but let's make sure this doesn't deadlock at least.
590        store
591            .delete_undecryptable_records_for_remote_replacement()
592            .unwrap();
593    }
594}
595
596#[test]
597fn test_send() {
598    fn ensure_send<T: Send>() {}
599    ensure_send::<LoginStore>();
600}
601
602#[cfg(feature = "keydb")]
603#[cfg(test)]
604mod tests_keydb {
605    use super::*;
606    use crate::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator};
607    use async_trait::async_trait;
608    use nss::ensure_initialized_with_profile_dir;
609    use std::path::PathBuf;
610
611    struct MockPrimaryPasswordAuthenticator {
612        password: String,
613    }
614
615    #[async_trait]
616    impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
617        async fn get_primary_password(&self) -> ApiResult<String> {
618            Ok(self.password.clone())
619        }
620        async fn on_authentication_success(&self) -> ApiResult<()> {
621            Ok(())
622        }
623        async fn on_authentication_failure(&self) -> ApiResult<()> {
624            Ok(())
625        }
626    }
627
628    fn profile_path() -> PathBuf {
629        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
630            .join("../support/rc_crypto/nss/fixtures/profile")
631    }
632
633    #[test]
634    fn decrypting_logins_with_primary_password() {
635        ensure_initialized_with_profile_dir(profile_path());
636
637        // `password` is the primary password of the profile fixture
638        let primary_password_authenticator = MockPrimaryPasswordAuthenticator {
639            password: "password".to_string(),
640        };
641        let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator));
642        let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager));
643        let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec))
644            .expect("store from fixtures");
645        let list = store.list().expect("Grabbing list to work");
646
647        assert_eq!(list.len(), 1);
648
649        assert_eq!(list[0].origin, "https://www.example.com");
650        assert_eq!(list[0].username, "test");
651        assert_eq!(list[0].password, "test");
652    }
653}