1use 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 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 #[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 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 let srl = self
181 .serialise()
182 .with_context(|| format!("failed to serialize database: {}", self.file_name()))?;
183
184 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 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 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 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 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 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 #[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 pub fn version(&self) -> versions::SemVer {
378 self.version.clone()
379 }
380
381 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 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 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 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 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 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 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 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 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}