rucksack_db/db/
manager.rs

1// Database encryption <-> decryption flow:
2//
3// When a rucksack database is serialised, the following happens:
4// * Its hashmap (DashMap) is converted to a sorted vec (for stable serialisation)
5// * the sorted vec is bincoded to bytes
6// * The bytes are stored on a field of the VersionedDB struct
7// * The VersionDB struct is bincoded to bytes
8// * The bytes are stored on a field of the EncryptedDB struct
9// * The bytes are encrypted
10// * The encrypted bytes are saved to a file
11//
12// Then, in the reverse, when a database is read from disk, this is how it's done:
13// * The file is read into memory as bytes and stored on a field of the EncryptedDB struct
14// * The encrypted bytes are decrypted
15// * The decrypted bytes are then bincode-decoded (deserialised) to a VersionedDB struct
16// * The bytes of the VersionDB are bincode-decoded to a vector of (string, record) tuples
17// * The sorted vector of tuples is converted to a hashmap (DashMap)
18// * The hashmap is stored as a field on the DB struct
19//
20use std::fmt;
21
22use anyhow::{anyhow, Context, Error, Result};
23use dashmap::DashMap;
24use secrecy::{ExposeSecret, SecretString};
25
26use rucksack_lib::{file, util};
27
28use crate::db::encrypted::EncryptedDB;
29use crate::db::versioned::VersionedDB;
30use crate::records;
31use crate::records::{DecryptedRecord, EncryptedRecord, Metadata};
32use crate::store;
33use crate::store::manager::StoreManager;
34
35pub struct DB {
36    pub file_name: String,
37    backup_dir: String,
38    enabled: bool,
39    hash_map: records::HashMap,
40    manager: Box<dyn StoreManager>,
41    salt: Option<SecretString>,
42    store_hash: u32,
43    store_pwd: Option<SecretString>,
44    version: versions::SemVer,
45}
46
47impl fmt::Debug for DB {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.debug_struct("DB")
50            .field("path", &self.file_name)
51            .field("hash_map", &self.hash_map)
52            .finish()
53    }
54}
55
56impl DB {
57    pub fn new(
58        file_name: String,
59        backup_dir: String,
60        store_pwd: Option<String>,
61        salt: Option<String>,
62    ) -> DB {
63        DB {
64            file_name,
65            backup_dir,
66            store_pwd: store_pwd.map(SecretString::new),
67            salt: salt.map(SecretString::new),
68            manager: store::manager::new(),
69            enabled: true,
70            hash_map: DashMap::new(),
71            store_hash: 0,
72            version: records::version(),
73        }
74    }
75
76    // Moved in v0.9.0
77    pub fn init(
78        file_name: String,
79        backup_dir: String,
80        store_pwd: Option<String>,
81        salt: Option<String>,
82    ) -> Result<()> {
83        log::debug!(operation = "init"; "Initialising database");
84        let mut db = DB::new(file_name, backup_dir, store_pwd, salt);
85        db.open()?;
86        db.close()
87    }
88
89    // Moved in v0.9.0
90    #[must_use = "database operations must be checked for errors"]
91    pub fn open(&mut self) -> Result<()> {
92        log::debug!(operation = "open"; "Opening database");
93        let store_pwd = self
94            .store_pwd
95            .as_ref()
96            .expect("store_pwd must be set to open database")
97            .expose_secret()
98            .to_string();
99        let salt = self
100            .salt
101            .as_ref()
102            .expect("salt must be set to open database")
103            .expose_secret()
104            .to_string();
105        let file_path = file::create_parents(self.file_name.clone()).with_context(|| {
106            format!(
107                "failed to create parent directory for database: {}",
108                self.file_name
109            )
110        })?;
111        if file_path.exists() {
112            log::debug!(operation = "decrypt", db_file = self.file_name.as_str(); "Creating encrypted DB");
113            let enc_db = self
114                .manager
115                .read(self.file_name.clone(), store_pwd, salt)
116                .with_context(|| {
117                    format!(
118                        "failed to read database file: {} (check password and salt)",
119                        self.file_name
120                    )
121                })?;
122            let vsn_db = match VersionedDB::deserialise(enc_db.decrypted()) {
123                Ok(db) => db,
124                Err(_) => {
125                    log::info!(db_file = self.file_name.as_str(), format = "non-versioned"; "Given database appears to be non-versioned; be sure to upgrade to the latest micro release of our old version before continuing");
126                    log::trace!(bytes_len = enc_db.decrypted().len(); "Database bytes");
127                    VersionedDB::from_bytes(enc_db.decrypted()).with_context(|| {
128                        format!("failed to parse database version from: {}", self.file_name)
129                    })?
130                }
131            };
132            log::debug!(operation = "hash_compute"; "Getting database hash");
133            self.store_hash = vsn_db.hash();
134            self.version = vsn_db.version();
135            // Decode the versioned DB's bytes to a hashmap
136            self.hash_map = records::decode_hashmap(vsn_db.bytes(), self.version.clone())
137                .with_context(|| {
138                    format!(
139                        "failed to decode database records (version: {})",
140                        self.version
141                    )
142                })?;
143        };
144
145        self.file_name = file_path.display().to_string();
146        self.enabled = true;
147        log::debug!(db_file = self.file_name.as_str(); "Set database path");
148        Ok(())
149    }
150
151    pub fn backup_dir(&self) -> String {
152        self.backup_dir.clone()
153    }
154
155    #[must_use = "database operations must be checked for errors"]
156    pub fn close(&self) -> Result<()> {
157        log::debug!(operation = "close", db_file = self.file_name().as_str(); "Closing DB file");
158        let path = file::create_parents(self.file_name()).with_context(|| {
159            format!(
160                "failed to create parent directory for database: {}",
161                self.file_name()
162            )
163        })?;
164        if path.exists() {
165            log::debug!(db_file = self.file_name().as_str(), operation = "backup"; "Database file exists; backing up");
166            let backup_file = self
167                .manager
168                .backup(
169                    self.file_name(),
170                    self.backup_dir(),
171                    self.schema_version().to_string(),
172                )
173                .with_context(|| {
174                    format!("failed to create backup of database: {}", self.file_name())
175                })?;
176            log::debug!(backup_file = backup_file.as_str(), operation = "backup_complete"; "Backed up file");
177        }
178
179        // Reverse the workflow of `open` ... encode the hashmap
180        let srl = self
181            .serialise()
182            .with_context(|| format!("failed to serialize database: {}", self.file_name()))?;
183
184        // Create versioned data
185        let vsn_db = VersionedDB::from_bytes(srl).with_context(|| {
186            format!(
187                "failed to create versioned database wrapper: {}",
188                self.file_name()
189            )
190        })?;
191        let encoded = vsn_db.serialise().with_context(|| {
192            format!(
193                "failed to serialize versioned database: {}",
194                self.file_name()
195            )
196        })?;
197        // Get the hash for the versioned data
198        let store_hash = vsn_db.hash();
199        if store_hash == self.store_hash {
200            log::debug!(hash = store_hash, operation = "persist_skip"; "No change in store hash; not persisting");
201            return Ok(());
202        }
203        // Encrypt the versioned data
204        let enc_db =
205            EncryptedDB::from_decrypted(encoded, self.file_name(), self.store_pwd(), self.salt())
206                .with_context(|| format!("failed to encrypt database: {}", self.file_name()))?;
207
208        // Save the encrypted data
209        enc_db
210            .write()
211            .with_context(|| format!("failed to write database to disk: {}", self.file_name()))
212    }
213
214    #[must_use = "database operations must be checked for errors"]
215    pub fn collect_decrypted(&self) -> Result<Vec<DecryptedRecord>, Error> {
216        let mut decrypted: Vec<DecryptedRecord> = Vec::new();
217        for i in self.iter() {
218            let record = records::decrypt_versioned(
219                i.value(),
220                self.store_pwd(),
221                self.salt(),
222                self.version.clone(),
223            )?;
224            decrypted.push(record);
225        }
226        Ok(decrypted)
227    }
228
229    // Added in v0.7.0
230    pub fn delete(&self, key: String) -> Option<bool> {
231        log::debug!(key = key.as_str(), operation = "delete"; "Deleting record");
232        match self.hash_map.remove(&key) {
233            Some(_) => Some(true),
234            None => Some(false),
235        }
236    }
237
238    // Added in v0.7.0
239    pub fn enabled(&self) -> bool {
240        self.enabled
241    }
242
243    pub fn get(&self, key: String) -> Option<DecryptedRecord> {
244        log::trace!(key = key.as_str(), operation = "get"; "Getting record");
245        self.hash_map.get(&key).and_then(|encrypted| {
246            records::decrypt_versioned(
247                encrypted.value(),
248                self.store_pwd(),
249                self.salt(),
250                self.version.clone(),
251            )
252            .ok()
253        })
254    }
255
256    pub fn get_metadata(&self, key: String) -> Option<Metadata> {
257        log::trace!(key = key.as_str(), operation = "get_metadata"; "Getting metadata of record");
258        match self.get(key.clone()) {
259            Some(r) => Some(r.metadata()),
260            None => {
261                log::debug!(key = key.as_str(), status = "not_found"; "Key not found");
262                None
263            }
264        }
265    }
266
267    pub fn hash_map(&self) -> records::HashMap {
268        self.hash_map.clone()
269    }
270
271    #[must_use = "database operations must be checked for errors"]
272    pub fn insert(&self, record: DecryptedRecord) -> Result<Option<EncryptedRecord>> {
273        let key = record.key();
274        log::debug!(key = key.as_str(), operation = "insert"; "Inserting record");
275        if let Some(r) = self.get(record.key()) {
276            log::trace!(key = key.as_str(), status = "exists"; "Record exists; skipping insert");
277            return Ok(Some(
278                r.encrypt(self.store_pwd(), self.salt())
279                    .with_context(|| format!("failed to encrypt existing record: {}", key))?,
280            ));
281        };
282        let encrypted = record
283            .encrypt(self.store_pwd(), self.salt())
284            .with_context(|| format!("failed to encrypt new record: {}", key))?;
285        Ok(self.hash_map.insert(key, encrypted))
286    }
287
288    pub fn iter(&self) -> dashmap::iter::Iter<'_, String, EncryptedRecord> {
289        self.hash_map.iter()
290    }
291
292    pub fn file_name(&self) -> String {
293        self.file_name.clone()
294    }
295
296    pub fn salt(&self) -> String {
297        self.salt
298            .as_ref()
299            .expect(
300                "BUG: salt should be Some when database operations are performed. \
301                This indicates the database was not properly initialized with a salt.",
302            )
303            .expose_secret()
304            .to_string()
305    }
306
307    fn serialise(&self) -> Result<Vec<u8>> {
308        log::debug!(operation = "serialize"; "Serialising data");
309        let mut data: Vec<(String, EncryptedRecord)> = Vec::new();
310        for i in self.iter() {
311            data.push((i.key().clone(), i.value().clone()))
312        }
313        log::trace!(operation = "serialize_convert"; "Converted hashmap to vec");
314        data.sort_by_key(|k| k.0.clone());
315        log::trace!(operation = "serialize_sort"; "Sorted vec");
316        match bincode::encode_to_vec(data, util::bincode_cfg()) {
317            Ok(encoded) => {
318                log::trace!(operation = "serialize_encode"; "Encoded vector");
319                Ok(encoded)
320            }
321            Err(e) => {
322                let msg = format!("couldn't encode DB hashmap ({e:?})");
323                log::error!(error = e.to_string().as_str(), operation = "serialize_encode"; "{}", msg);
324                Err(anyhow!("{}", msg))
325            }
326        }
327    }
328
329    pub fn store_pwd(&self) -> String {
330        self.store_pwd
331            .as_ref()
332            .expect(
333                "BUG: store_pwd should be Some when database operations are performed. \
334                This indicates the database was not properly initialized with a password.",
335            )
336            .expose_secret()
337            .to_string()
338    }
339
340    // Note that the key has to be passed here, even though the
341    // updated record has a key() method; this is because an update
342    // might involved a field used to create the key (and since that
343    // new key hasn't been saved yet, there's no record for it --
344    // just one for the old key).
345    #[must_use = "database operations must be checked for errors"]
346    pub fn update(&self, key: String, updated: DecryptedRecord) -> Result<()> {
347        log::debug!(key = key.as_str(), operation = "update"; "Updating record");
348        match self.delete(key.clone()) {
349            Some(true) => {
350                self.insert(updated)
351                    .with_context(|| format!("failed to insert updated record: {}", key))?;
352                Ok(())
353            }
354            Some(false) => {
355                log::error!(key = key.as_str(), operation = "update"; "Could not update record");
356                Err(anyhow!("failed to delete record '{}' for update", key))
357            }
358            None => unreachable!(),
359        }
360    }
361
362    #[must_use = "database operations must be checked for errors"]
363    pub fn update_metadata(&self, key: String, metadata: Metadata) -> Result<()> {
364        log::debug!(key = key.as_str(), operation = "update_metadata"; "Updating metadata on record");
365        let key_for_error = key.clone();
366        match self.hash_map.try_entry(key) {
367            Some(entry) => {
368                entry.and_modify(|r| r.metadata = metadata);
369                log::trace!(key = key_for_error.as_str(), status = "success"; "Updated metadata");
370                Ok(())
371            }
372            None => Err(anyhow!("record '{}' not found or locked", key_for_error)),
373        }
374    }
375
376    // Added in v0.7.0
377    pub fn version(&self) -> versions::SemVer {
378        self.version.clone()
379    }
380
381    // Added in v0.10.1
382    pub fn schema_version(&self) -> versions::SemVer {
383        records::version()
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use rucksack_lib::time;
391
392    use crate::testing;
393
394    #[test]
395    fn db_basics() {
396        let pwd = Some(testing::data::store_pwd());
397        let salt = Some(time::now());
398        let mut db_handler = testing::db::new();
399        let mut r = db_handler.setup();
400        assert!(r.is_ok());
401        let db_file = db_handler.file_name().unwrap();
402        let backups = db_handler.backups_path().unwrap().display().to_string();
403        println!("Got db_file: {db_file}");
404        println!("Got backups_path: {backups}");
405
406        // Store data and close
407        let mut tmp_db =
408            super::DB::new(db_file.clone(), backups.clone(), pwd.clone(), salt.clone());
409        assert!(tmp_db.open().is_ok());
410        assert!(tmp_db.version() > versions::SemVer::new("0.8.0").unwrap());
411        let dpr = testing::data::plaintext_record_v090();
412        tmp_db.insert(dpr.clone()).unwrap();
413        let re_dpr = tmp_db.get(dpr.key()).unwrap();
414        assert_eq!(re_dpr.secrets.user, "alice@site.com");
415        assert_eq!(re_dpr.secrets.password, "6 s3kr1t");
416        assert!(tmp_db.close().is_ok());
417
418        // Re-open DB and check stored data
419        let mut tmp_db = super::DB::new(db_file, backups, pwd, salt);
420        assert!(tmp_db.open().is_ok());
421        let read_dpr = tmp_db.get(dpr.key()).unwrap();
422        assert_eq!(read_dpr.secrets.user, "alice@site.com");
423        assert_eq!(read_dpr.secrets.password, "6 s3kr1t");
424        assert_eq!(read_dpr.history.len(), 2);
425        assert_eq!(read_dpr.history[0].secrets.password, "4 s3kr1t");
426        assert_eq!(read_dpr.history[1].secrets.password, "5 s3kr1t");
427        assert!(tmp_db.close().is_ok());
428        r = db_handler.teardown();
429        assert!(r.is_ok());
430    }
431
432    #[test]
433    fn test_new_db() {
434        let pwd = Some("password".to_string());
435        let salt = Some("salt".to_string());
436        let db = DB::new(
437            "/tmp/test.db".to_string(),
438            "/tmp/backups".to_string(),
439            pwd,
440            salt,
441        );
442
443        assert_eq!(db.file_name(), "/tmp/test.db");
444        assert_eq!(db.backup_dir(), "/tmp/backups");
445        assert!(db.enabled());
446        assert_eq!(db.hash_map().len(), 0);
447    }
448
449    #[test]
450    fn test_getters() {
451        let pwd = Some("test_pwd".to_string());
452        let salt = Some("test_salt".to_string());
453        let db = DB::new(
454            "/path/to/db".to_string(),
455            "/path/to/backups".to_string(),
456            pwd.clone(),
457            salt.clone(),
458        );
459
460        assert_eq!(db.file_name(), "/path/to/db");
461        assert_eq!(db.backup_dir(), "/path/to/backups");
462        assert_eq!(db.store_pwd(), pwd.unwrap());
463        assert_eq!(db.salt(), salt.unwrap());
464        assert!(db.enabled());
465        assert_eq!(db.schema_version(), records::version());
466    }
467
468    #[test]
469    fn test_init_creates_db() {
470        let pwd = Some(testing::data::store_pwd());
471        let salt = Some(time::now());
472        let mut db_handler = testing::db::new();
473        assert!(db_handler.setup().is_ok());
474        let db_file = db_handler.file_name().unwrap();
475        let backups = db_handler.backups_path().unwrap().display().to_string();
476
477        let result = DB::init(db_file.clone(), backups, pwd, salt);
478        assert!(result.is_ok());
479
480        // Verify file was created
481        assert!(std::path::Path::new(&db_file).exists());
482
483        assert!(db_handler.teardown().is_ok());
484    }
485
486    #[test]
487    fn test_insert_and_get() {
488        let pwd = Some(testing::data::store_pwd());
489        let salt = Some(time::now());
490        let mut db_handler = testing::db::new();
491        assert!(db_handler.setup().is_ok());
492        let db_file = db_handler.file_name().unwrap();
493        let backups = db_handler.backups_path().unwrap().display().to_string();
494
495        let mut db = DB::new(db_file, backups, pwd, salt);
496        assert!(db.open().is_ok());
497
498        let record = testing::data::plaintext_record_v090();
499        let key = record.key();
500        db.insert(record.clone()).unwrap();
501
502        let retrieved = db.get(key).unwrap();
503        assert_eq!(retrieved.secrets.user, record.secrets.user);
504        assert_eq!(retrieved.secrets.password, record.secrets.password);
505
506        assert!(db.close().is_ok());
507        assert!(db_handler.teardown().is_ok());
508    }
509
510    #[test]
511    fn test_insert_duplicate_returns_existing() {
512        let pwd = Some(testing::data::store_pwd());
513        let salt = Some(time::now());
514        let mut db_handler = testing::db::new();
515        assert!(db_handler.setup().is_ok());
516        let db_file = db_handler.file_name().unwrap();
517        let backups = db_handler.backups_path().unwrap().display().to_string();
518
519        let mut db = DB::new(db_file, backups, pwd, salt);
520        assert!(db.open().is_ok());
521
522        let record = testing::data::plaintext_record_v090();
523        let result1 = db.insert(record.clone()).unwrap();
524        assert!(result1.is_none(), "First insert should return None");
525
526        let result2 = db.insert(record.clone()).unwrap();
527        assert!(result2.is_some(), "Duplicate insert should return existing");
528
529        assert!(db.close().is_ok());
530        assert!(db_handler.teardown().is_ok());
531    }
532
533    #[test]
534    fn test_get_nonexistent() {
535        let pwd = Some(testing::data::store_pwd());
536        let salt = Some(time::now());
537        let mut db_handler = testing::db::new();
538        assert!(db_handler.setup().is_ok());
539        let db_file = db_handler.file_name().unwrap();
540        let backups = db_handler.backups_path().unwrap().display().to_string();
541
542        let mut db = DB::new(db_file, backups, pwd, salt);
543        assert!(db.open().is_ok());
544
545        let result = db.get("nonexistent_key".to_string());
546        assert!(result.is_none());
547
548        assert!(db.close().is_ok());
549        assert!(db_handler.teardown().is_ok());
550    }
551
552    #[test]
553    fn test_delete_existing() {
554        let pwd = Some(testing::data::store_pwd());
555        let salt = Some(time::now());
556        let mut db_handler = testing::db::new();
557        assert!(db_handler.setup().is_ok());
558        let db_file = db_handler.file_name().unwrap();
559        let backups = db_handler.backups_path().unwrap().display().to_string();
560
561        let mut db = DB::new(db_file, backups, pwd, salt);
562        assert!(db.open().is_ok());
563
564        let record = testing::data::plaintext_record_v090();
565        let key = record.key();
566        db.insert(record).unwrap();
567
568        let result = db.delete(key.clone());
569        assert_eq!(result, Some(true));
570
571        let retrieved = db.get(key);
572        assert!(retrieved.is_none(), "Record should be deleted");
573
574        assert!(db.close().is_ok());
575        assert!(db_handler.teardown().is_ok());
576    }
577
578    #[test]
579    fn test_delete_nonexistent() {
580        let pwd = Some(testing::data::store_pwd());
581        let salt = Some(time::now());
582        let mut db_handler = testing::db::new();
583        assert!(db_handler.setup().is_ok());
584        let db_file = db_handler.file_name().unwrap();
585        let backups = db_handler.backups_path().unwrap().display().to_string();
586
587        let mut db = DB::new(db_file, backups, pwd, salt);
588        assert!(db.open().is_ok());
589
590        let result = db.delete("nonexistent_key".to_string());
591        assert_eq!(result, Some(false));
592
593        assert!(db.close().is_ok());
594        assert!(db_handler.teardown().is_ok());
595    }
596
597    #[test]
598    fn test_update_record() {
599        let pwd = Some(testing::data::store_pwd());
600        let salt = Some(time::now());
601        let mut db_handler = testing::db::new();
602        assert!(db_handler.setup().is_ok());
603        let db_file = db_handler.file_name().unwrap();
604        let backups = db_handler.backups_path().unwrap().display().to_string();
605
606        let mut db = DB::new(db_file, backups, pwd, salt);
607        assert!(db.open().is_ok());
608
609        let mut record = testing::data::plaintext_record_v090();
610        let key = record.key();
611        db.insert(record.clone()).unwrap();
612
613        // Update the record
614        record.secrets.password = "new_password".to_string();
615        db.update(key.clone(), record).unwrap();
616
617        let retrieved = db.get(key).unwrap();
618        assert_eq!(retrieved.secrets.password, "new_password");
619
620        assert!(db.close().is_ok());
621        assert!(db_handler.teardown().is_ok());
622    }
623
624    #[test]
625    fn test_get_metadata() {
626        let pwd = Some(testing::data::store_pwd());
627        let salt = Some(time::now());
628        let mut db_handler = testing::db::new();
629        assert!(db_handler.setup().is_ok());
630        let db_file = db_handler.file_name().unwrap();
631        let backups = db_handler.backups_path().unwrap().display().to_string();
632
633        let mut db = DB::new(db_file, backups, pwd, salt);
634        assert!(db.open().is_ok());
635
636        let record = testing::data::plaintext_record_v090();
637        let key = record.key();
638        db.insert(record.clone()).unwrap();
639
640        let metadata = db.get_metadata(key).unwrap();
641        assert_eq!(metadata.name, record.metadata.name);
642
643        assert!(db.close().is_ok());
644        assert!(db_handler.teardown().is_ok());
645    }
646
647    #[test]
648    fn test_get_metadata_nonexistent() {
649        let pwd = Some(testing::data::store_pwd());
650        let salt = Some(time::now());
651        let mut db_handler = testing::db::new();
652        assert!(db_handler.setup().is_ok());
653        let db_file = db_handler.file_name().unwrap();
654        let backups = db_handler.backups_path().unwrap().display().to_string();
655
656        let mut db = DB::new(db_file, backups, pwd, salt);
657        assert!(db.open().is_ok());
658
659        let result = db.get_metadata("nonexistent_key".to_string());
660        assert!(result.is_none());
661
662        assert!(db.close().is_ok());
663        assert!(db_handler.teardown().is_ok());
664    }
665
666    #[test]
667    fn test_update_metadata() {
668        let pwd = Some(testing::data::store_pwd());
669        let salt = Some(time::now());
670        let mut db_handler = testing::db::new();
671        assert!(db_handler.setup().is_ok());
672        let db_file = db_handler.file_name().unwrap();
673        let backups = db_handler.backups_path().unwrap().display().to_string();
674
675        let mut db = DB::new(db_file, backups, pwd, salt);
676        assert!(db.open().is_ok());
677
678        let record = testing::data::plaintext_record_v090();
679        let key = record.key();
680        db.insert(record.clone()).unwrap();
681
682        // Update metadata
683        let mut new_metadata = record.metadata.clone();
684        new_metadata.name = "Updated Name".to_string();
685        db.update_metadata(key.clone(), new_metadata.clone())
686            .unwrap();
687
688        let retrieved_metadata = db.get_metadata(key).unwrap();
689        assert_eq!(retrieved_metadata.name, "Updated Name");
690
691        assert!(db.close().is_ok());
692        assert!(db_handler.teardown().is_ok());
693    }
694
695    #[test]
696    fn test_collect_decrypted() {
697        let pwd = Some(testing::data::store_pwd());
698        let salt = Some(time::now());
699        let mut db_handler = testing::db::new();
700        assert!(db_handler.setup().is_ok());
701        let db_file = db_handler.file_name().unwrap();
702        let backups = db_handler.backups_path().unwrap().display().to_string();
703
704        let mut db = DB::new(db_file, backups, pwd, salt);
705        assert!(db.open().is_ok());
706
707        let record = testing::data::plaintext_record_v090();
708        db.insert(record.clone()).unwrap();
709
710        let decrypted = db.collect_decrypted().unwrap();
711        assert_eq!(decrypted.len(), 1);
712        assert_eq!(decrypted[0].secrets.user, record.secrets.user);
713
714        assert!(db.close().is_ok());
715        assert!(db_handler.teardown().is_ok());
716    }
717
718    #[test]
719    fn test_iter() {
720        let pwd = Some(testing::data::store_pwd());
721        let salt = Some(time::now());
722        let mut db_handler = testing::db::new();
723        assert!(db_handler.setup().is_ok());
724        let db_file = db_handler.file_name().unwrap();
725        let backups = db_handler.backups_path().unwrap().display().to_string();
726
727        let mut db = DB::new(db_file, backups, pwd, salt);
728        assert!(db.open().is_ok());
729
730        let record = testing::data::plaintext_record_v090();
731        db.insert(record).unwrap();
732
733        let count = db.iter().count();
734        assert_eq!(count, 1);
735
736        assert!(db.close().is_ok());
737        assert!(db_handler.teardown().is_ok());
738    }
739
740    #[test]
741    fn test_hash_map_getter() {
742        let pwd = Some(testing::data::store_pwd());
743        let salt = Some(time::now());
744        let mut db_handler = testing::db::new();
745        assert!(db_handler.setup().is_ok());
746        let db_file = db_handler.file_name().unwrap();
747        let backups = db_handler.backups_path().unwrap().display().to_string();
748
749        let mut db = DB::new(db_file, backups, pwd, salt);
750        assert!(db.open().is_ok());
751
752        let record = testing::data::plaintext_record_v090();
753        db.insert(record).unwrap();
754
755        let hash_map = db.hash_map();
756        assert_eq!(hash_map.len(), 1);
757
758        assert!(db.close().is_ok());
759        assert!(db_handler.teardown().is_ok());
760    }
761
762    #[test]
763    fn test_close_without_changes_no_write() {
764        let pwd = Some(testing::data::store_pwd());
765        let salt = Some(time::now());
766        let mut db_handler = testing::db::new();
767        assert!(db_handler.setup().is_ok());
768        let db_file = db_handler.file_name().unwrap();
769        let backups = db_handler.backups_path().unwrap().display().to_string();
770
771        // Create and close empty DB
772        let mut db = DB::new(db_file.clone(), backups.clone(), pwd.clone(), salt.clone());
773        assert!(db.open().is_ok());
774        assert!(db.close().is_ok());
775
776        // Reopen and close again (no changes)
777        let mut db = DB::new(db_file, backups, pwd, salt);
778        assert!(db.open().is_ok());
779        let _initial_hash = db.store_hash;
780        assert!(db.close().is_ok());
781        // If hash didn't change, close returns early without writing
782
783        assert!(db_handler.teardown().is_ok());
784    }
785
786    #[test]
787    fn test_version_tracking() {
788        let pwd = Some(testing::data::store_pwd());
789        let salt = Some(time::now());
790        let mut db_handler = testing::db::new();
791        assert!(db_handler.setup().is_ok());
792        let db_file = db_handler.file_name().unwrap();
793        let backups = db_handler.backups_path().unwrap().display().to_string();
794
795        let mut db = DB::new(db_file, backups, pwd, salt);
796        assert!(db.open().is_ok());
797
798        let version = db.version();
799        assert!(version >= versions::SemVer::new("0.7.0").unwrap());
800
801        assert!(db.close().is_ok());
802        assert!(db_handler.teardown().is_ok());
803    }
804
805    #[test]
806    fn test_debug_impl() {
807        let pwd = Some("pwd".to_string());
808        let salt = Some("salt".to_string());
809        let db = DB::new(
810            "/test/path".to_string(),
811            "/test/backups".to_string(),
812            pwd,
813            salt,
814        );
815
816        let debug_str = format!("{:?}", db);
817        assert!(debug_str.contains("DB"));
818        assert!(debug_str.contains("/test/path"));
819    }
820}