kpdb/types/
database.rs

1// Copyright (c) 2016-2017 Martijn Rijkeboer <mrr@sru-systems.com>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use super::binaries_map::BinariesMap;
10use super::color::Color;
11use super::comment::Comment;
12use super::composite_key::CompositeKey;
13use super::compression::Compression;
14use super::custom_data_map::CustomDataMap;
15use super::custom_icons_map::CustomIconsMap;
16use super::db_type::DbType;
17use super::entry::Entry;
18use super::entry_uuid::EntryUuid;
19use super::error::Error;
20use super::group::Group;
21use super::group_uuid::GroupUuid;
22use super::master_cipher::MasterCipher;
23use super::result::Result;
24use super::stream_cipher::StreamCipher;
25use super::string_value::StringValue;
26use super::transform_rounds::TransformRounds;
27use super::version::Version;
28use crate::common;
29use crate::format::{kdb2_reader, kdb2_writer};
30use crate::io::{Log, LogReader, LogWriter};
31use chrono::{DateTime, Utc};
32use std::io::{Read, Write};
33
34/// The KeePass database.
35#[derive(Clone, Debug, PartialEq)]
36pub struct Database {
37    /// Content of the comment header.
38    pub comment: Option<Comment>,
39
40    /// Composite key.
41    pub composite_key: CompositeKey,
42
43    /// Compression algorithm.
44    pub compression: Compression,
45
46    /// Type of the database.
47    pub db_type: DbType,
48
49    /// Master encryption algorithm.
50    pub master_cipher: MasterCipher,
51
52    /// Stream encryption algorithm (e.g. passwords).
53    pub stream_cipher: StreamCipher,
54
55    /// Number of times the composite key must be transformed.
56    pub transform_rounds: TransformRounds,
57
58    /// The database version.
59    pub version: Version,
60
61    /// Map with binary data.
62    pub binaries: BinariesMap,
63
64    /// Optional color.
65    pub color: Option<Color>,
66
67    /// Map with custom data.
68    pub custom_data: CustomDataMap,
69
70    /// Map with custom icons.
71    pub custom_icons: CustomIconsMap,
72
73    /// Default username for new entries.
74    pub def_username: String,
75
76    /// The date and time the default username was changed.
77    pub def_username_changed: DateTime<Utc>,
78
79    /// Description of this database.
80    pub description: String,
81
82    /// The date and time the description was changed.
83    pub description_changed: DateTime<Utc>,
84
85    /// The date and time the entry templates group was changed.
86    pub entry_templates_group_changed: DateTime<Utc>,
87
88    /// The identifier of the group containing entry templates.
89    pub entry_templates_group_uuid: GroupUuid,
90
91    /// Name of the generator.
92    pub generator: String,
93
94    /// Maximum number of history items.
95    pub history_max_items: i32,
96
97    /// Maximum size of the history data.
98    pub history_max_size: i32,
99
100    /// The identifier of the last selected group.
101    pub last_selected_group: GroupUuid,
102
103    /// The identifier of the last top visible group.
104    pub last_top_visible_group: GroupUuid,
105
106    /// Number of days until history entries are being deleted.
107    pub maintenance_history_days: i32,
108
109    pub master_key_change_force: i32,
110
111    pub master_key_change_rec: i32,
112
113    /// The date and time the master key was changed.
114    pub master_key_changed: DateTime<Utc>,
115
116    /// Name of this database.
117    pub name: String,
118
119    /// The date and time the name was changed.
120    pub name_changed: DateTime<Utc>,
121
122    /// Whether notes must be protected.
123    pub protect_notes: bool,
124
125    /// Whether passwords must be protected.
126    pub protect_password: bool,
127
128    /// Whether titles must be protected.
129    pub protect_title: bool,
130
131    /// Whether URL's must be protected.
132    pub protect_url: bool,
133
134    /// Whether usernames must be protected.
135    pub protect_username: bool,
136
137    /// The date and time the recycle bin was changed.
138    pub recycle_bin_changed: DateTime<Utc>,
139
140    /// Whether the recycle bin is enabled.
141    pub recycle_bin_enabled: bool,
142
143    /// The identifier of the recycle bin.
144    pub recycle_bin_uuid: GroupUuid,
145
146    /// The root group.
147    pub root_group: Group,
148}
149
150impl Database {
151    /// Create a new database.
152    ///
153    /// # Examples
154    ///
155    /// ```rust
156    /// use kpdb::{CompositeKey, Database};
157    ///
158    /// let key = CompositeKey::from_password("password");
159    /// let db = Database::new(&key);
160    /// ```
161    pub fn new(key: &CompositeKey) -> Database {
162        let now = Utc::now();
163        Database {
164            comment: None,
165            composite_key: key.clone(),
166            compression: Compression::GZip,
167            db_type: DbType::Kdb2,
168            master_cipher: MasterCipher::Aes256,
169            stream_cipher: StreamCipher::Salsa20,
170            transform_rounds: TransformRounds(10000),
171            version: Version::new_kdb2(),
172            binaries: BinariesMap::new(),
173            color: None,
174            custom_data: CustomDataMap::new(),
175            custom_icons: CustomIconsMap::new(),
176            def_username: String::new(),
177            def_username_changed: now,
178            description: String::new(),
179            description_changed: now,
180            entry_templates_group_changed: now,
181            entry_templates_group_uuid: GroupUuid::nil(),
182            generator: String::from(common::GENERATOR_NAME),
183            history_max_items: common::HISTORY_MAX_ITEMS_DEFAULT,
184            history_max_size: common::HISTORY_MAX_SIZE_DEFAULT,
185            last_selected_group: GroupUuid::nil(),
186            last_top_visible_group: GroupUuid::nil(),
187            maintenance_history_days: common::MAINTENANCE_HISTORY_DAYS_DEFAULT,
188            master_key_change_force: common::MASTER_KEY_CHANGE_FORCE_DEFAULT,
189            master_key_change_rec: common::MASTER_KEY_CHANGE_REC_DEFAULT,
190            master_key_changed: now,
191            name: String::new(),
192            name_changed: now,
193            protect_notes: common::PROTECT_NOTES_DEFAULT,
194            protect_password: common::PROTECT_PASSWORD_DEFAULT,
195            protect_title: common::PROTECT_TITLE_DEFAULT,
196            protect_url: common::PROTECT_URL_DEFAULT,
197            protect_username: common::PROTECT_USERNAME_DEFAULT,
198            recycle_bin_changed: now,
199            recycle_bin_enabled: common::RECYCLE_BIN_ENABLED_DEFAULT,
200            recycle_bin_uuid: GroupUuid::nil(),
201            root_group: Group::new(common::ROOT_GROUP_NAME),
202        }
203    }
204
205    /// Returns a vector with entries that match (case insensitive) the supplied text.
206    ///
207    /// # Examples
208    ///
209    /// ```rust
210    /// use kpdb::{CompositeKey, Database, Entry, Group};
211    ///
212    /// let mut protonmail = Entry::new();
213    /// protonmail.set_title("ProtonMail");
214    /// protonmail.set_username("puser");
215    /// protonmail.set_password("ppass");
216    /// protonmail.set_url("https://mail.protonmail.com");
217    ///
218    /// let mut group = Group::new("Email");
219    /// group.add_entry(protonmail);
220    ///
221    /// let mut db = Database::new(&CompositeKey::from_password("test"));
222    /// db.root_group.add_group(group);
223    ///
224    /// let result = db.find_entries("Protonm");
225    /// assert_eq!(result.len(), 1);
226    ///
227    /// let result = db.find_entries("gmail");
228    /// assert_eq!(result.len(), 0);
229    /// ```
230    pub fn find_entries<'a, S: Into<String>>(&'a self, text: S) -> Vec<&'a Entry> {
231        let mut list = Vec::new();
232        let text = &text.into().to_lowercase();
233        for group in self.root_group.iter() {
234            for entry in group.entries.iter() {
235                if entry_contains_string(entry, text) {
236                    list.push(entry);
237                }
238            }
239        }
240        list
241    }
242
243    /// Returns a vector with mutable entries that match (case insensitive) the supplied text.
244    ///
245    /// # Examples
246    ///
247    /// ```rust
248    /// use kpdb::{CompositeKey, Database, Entry, Group};
249    ///
250    /// let mut protonmail = Entry::new();
251    /// protonmail.set_title("ProtonMail");
252    /// protonmail.set_username("puser");
253    /// protonmail.set_password("ppass");
254    /// protonmail.set_url("https://mail.protonmail.com");
255    ///
256    /// let mut group = Group::new("Email");
257    /// group.add_entry(protonmail);
258    ///
259    /// let mut db = Database::new(&CompositeKey::from_password("test"));
260    /// db.root_group.add_group(group);
261    ///
262    /// let result = db.find_entries_mut("Protonm");
263    /// assert_eq!(result.len(), 1);
264    /// ```
265    pub fn find_entries_mut<'a, S: Into<String>>(&'a mut self, text: S) -> Vec<&'a mut Entry> {
266        let mut list = Vec::new();
267        let text = &text.into().to_lowercase();
268        for group in self.root_group.iter_mut() {
269            for entry in group.entries.iter_mut() {
270                if entry_contains_string(entry, text) {
271                    list.push(entry);
272                }
273            }
274        }
275        list
276    }
277
278    /// Returns a vector with groups that match (case insensitive) the supplied name.
279    ///
280    /// # Examples
281    ///
282    /// ```rust
283    /// use kpdb::{CompositeKey, Database, Group};
284    ///
285    /// let group = Group::new("Email");
286    ///
287    /// let mut db = Database::new(&CompositeKey::from_password("test"));
288    /// db.root_group.add_group(group);
289    ///
290    /// let result = db.find_groups("mail");
291    /// assert_eq!(result.len(), 1);
292    ///
293    /// let result = db.find_groups("unknown");
294    /// assert_eq!(result.len(), 0);
295    /// ```
296    pub fn find_groups<'a, S: Into<String>>(&'a self, name: S) -> Vec<&'a Group> {
297        let name = &name.into().to_lowercase();
298        self.root_group
299            .iter()
300            .filter(|g| g.name.to_lowercase().contains(name))
301            .collect::<Vec<&'a Group>>()
302    }
303
304    /// Returns a vector with mutable groups that match (case insensitive) the supplied name.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use kpdb::{CompositeKey, Database, Group};
310    ///
311    /// let group = Group::new("Email");
312    ///
313    /// let mut db = Database::new(&CompositeKey::from_password("test"));
314    /// db.root_group.add_group(group);
315    ///
316    /// let result = db.find_groups_mut("mail");
317    /// assert_eq!(result.len(), 1);
318    /// ```
319    pub fn find_groups_mut<'a, S: Into<String>>(&'a mut self, name: S) -> Vec<&'a mut Group> {
320        let name = &name.into().to_lowercase();
321        self.root_group
322            .iter_mut()
323            .filter(|g| g.name.to_lowercase().contains(name))
324            .collect::<Vec<&'a mut Group>>()
325    }
326
327    /// Returns the entry that matches the UUID or None if not found.
328    ///
329    /// # Examples
330    ///
331    /// ```rust
332    /// use kpdb::{CompositeKey, Database, Entry, Group};
333    ///
334    /// let entry = Entry::new();
335    /// let entry_uuid = entry.uuid;
336    ///
337    /// let mut db = Database::new(&CompositeKey::from_password("test"));
338    /// assert_eq!(db.get_entry(entry_uuid), None);
339    ///
340    /// db.root_group.add_entry(entry.clone());
341    /// assert_eq!(db.get_entry(entry_uuid), Some(&entry));
342    /// ```
343    pub fn get_entry<'a>(&'a self, uuid: EntryUuid) -> Option<&'a Entry> {
344        for group in self.root_group.iter() {
345            for entry in group.entries.iter() {
346                if entry.uuid == uuid {
347                    return Some(entry);
348                }
349            }
350        }
351        None
352    }
353
354    /// Returns the mutable entry that matches the UUID or None if not found.
355    ///
356    /// # Examples
357    ///
358    /// ```rust
359    /// use kpdb::{CompositeKey, Database, Entry, Group};
360    ///
361    /// let mut entry = Entry::new();
362    /// let entry_uuid = entry.uuid;
363    ///
364    /// let mut db = Database::new(&CompositeKey::from_password("test"));
365    /// assert_eq!(db.get_entry_mut(entry_uuid), None);
366    ///
367    /// db.root_group.add_entry(entry.clone());
368    /// assert_eq!(db.get_entry_mut(entry_uuid), Some(&mut entry));
369    /// ```
370    pub fn get_entry_mut<'a>(&'a mut self, uuid: EntryUuid) -> Option<&'a mut Entry> {
371        for group in self.root_group.iter_mut() {
372            for entry in group.entries.iter_mut() {
373                if entry.uuid == uuid {
374                    return Some(entry);
375                }
376            }
377        }
378        None
379    }
380
381    /// Returns the group that matches the UUID or None if not found.
382    ///
383    /// # Examples
384    ///
385    /// ```rust
386    /// use kpdb::{CompositeKey, Database, Group};
387    ///
388    /// let group = Group::new("Group");
389    /// let group_uuid = group.uuid;
390    ///
391    /// let mut db = Database::new(&CompositeKey::from_password("test"));
392    /// assert_eq!(db.get_group(group_uuid), None);
393    ///
394    /// db.root_group.add_group(group.clone());
395    /// assert_eq!(db.get_group(group_uuid), Some(&group));
396    /// ```
397    pub fn get_group<'a>(&'a self, uuid: GroupUuid) -> Option<&'a Group> {
398        self.root_group.iter().find(|g| g.uuid == uuid)
399    }
400
401    /// Returns the mutable group that matches the UUID or None if not found.
402    ///
403    /// # Examples
404    ///
405    /// ```rust
406    /// use kpdb::{CompositeKey, Database, Group};
407    ///
408    /// let mut group = Group::new("Group");
409    /// let group_uuid = group.uuid;
410    ///
411    /// let mut db = Database::new(&CompositeKey::from_password("test"));
412    /// assert_eq!(db.get_group(group_uuid), None);
413    ///
414    /// db.root_group.add_group(group.clone());
415    /// assert_eq!(db.get_group_mut(group_uuid), Some(&mut group));
416    /// ```
417    pub fn get_group_mut<'a>(&'a mut self, uuid: GroupUuid) -> Option<&'a mut Group> {
418        self.root_group.iter_mut().find(|g| g.uuid == uuid)
419    }
420
421    /// Attempts to open an existing database.
422    ///
423    /// # Examples
424    ///
425    /// ```rust,no_run
426    /// # use kpdb::Result;
427    /// use kpdb::{CompositeKey, Database};
428    /// use std::fs::File;
429    ///
430    /// # fn open_example() -> Result<()> {
431    /// let mut file = File::open("passwords.kdbx")?;
432    /// let key = CompositeKey::from_password("password");
433    /// let db = Database::open(&mut file, &key)?;
434    /// # Ok(())
435    /// # }
436    /// ```
437    pub fn open<R: Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
438        let mut reader = LogReader::new(reader);
439        let mut buffer = [0u8; 4];
440
441        reader.read(&mut buffer)?;
442        if buffer != common::DB_SIGNATURE {
443            return Err(Error::InvalidDbSignature(buffer));
444        }
445
446        reader.read(&mut buffer)?;
447        if buffer == common::KDB1_SIGNATURE {
448            return Err(Error::UnhandledDbType(buffer));
449        } else if buffer == common::KDB2_SIGNATURE {
450            Database::open_kdb2(&mut reader, key)
451        } else {
452            return Err(Error::UnhandledDbType(buffer));
453        }
454    }
455
456    /// Attempts to save the database.
457    ///
458    /// # Examples
459    ///
460    /// ```rust,no_run
461    /// # use kpdb::Result;
462    /// use kpdb::{CompositeKey, Database};
463    /// use std::fs::File;
464    ///
465    /// # fn save_example() -> Result<()> {
466    /// let key = CompositeKey::from_password("password");
467    /// let db = Database::new(&key);
468    /// let mut file = File::create("new.kdbx")?;
469    ///
470    /// db.save(&mut file);
471    /// # Ok(())
472    /// # }
473    /// ```
474    pub fn save<W: Write>(&self, writer: &mut W) -> Result<()> {
475        let mut writer = LogWriter::new(writer);
476        match self.db_type {
477            DbType::Kdb1 => Err(Error::Unimplemented(String::from("KeePass v1 not supported"))),
478            DbType::Kdb2 => kdb2_writer::write(&mut writer, self),
479        }
480    }
481
482    fn open_kdb2<R: Log + Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
483        let (meta_data, xml_data) = kdb2_reader::read(reader, key)?;
484        match xml_data.header_hash {
485            Some(header_hash) => {
486                if meta_data.header_hash != header_hash {
487                    return Err(Error::InvalidHeaderHash);
488                }
489            }
490            None => {}
491        }
492
493        let root_group = match xml_data.root_group {
494            Some(group) => group,
495            None => Group::new(common::ROOT_GROUP_NAME),
496        };
497
498        let db = Database {
499            comment: meta_data.comment,
500            composite_key: key.clone(),
501            compression: meta_data.compression,
502            db_type: DbType::Kdb2,
503            master_cipher: meta_data.master_cipher,
504            stream_cipher: meta_data.stream_cipher,
505            transform_rounds: meta_data.transform_rounds,
506            version: meta_data.version,
507
508            binaries: xml_data.binaries,
509            color: xml_data.color,
510            custom_data: xml_data.custom_data,
511            custom_icons: xml_data.custom_icons,
512            def_username: xml_data.def_username,
513            def_username_changed: xml_data.def_username_changed,
514            description: xml_data.description,
515            description_changed: xml_data.description_changed,
516            entry_templates_group_changed: xml_data.entry_templates_group_changed,
517            entry_templates_group_uuid: xml_data.entry_templates_group_uuid,
518            generator: xml_data.generator,
519            history_max_items: xml_data.history_max_items,
520            history_max_size: xml_data.history_max_size,
521            last_selected_group: xml_data.last_selected_group,
522            last_top_visible_group: xml_data.last_top_visible_group,
523            maintenance_history_days: xml_data.maintenance_history_days,
524            master_key_change_force: xml_data.master_key_change_force,
525            master_key_change_rec: xml_data.master_key_change_rec,
526            master_key_changed: xml_data.master_key_changed,
527            name: xml_data.name,
528            name_changed: xml_data.name_changed,
529            protect_notes: xml_data.protect_notes,
530            protect_password: xml_data.protect_password,
531            protect_title: xml_data.protect_title,
532            protect_url: xml_data.protect_url,
533            protect_username: xml_data.protect_username,
534            recycle_bin_changed: xml_data.recycle_bin_changed,
535            recycle_bin_enabled: xml_data.recycle_bin_enabled,
536            recycle_bin_uuid: xml_data.recycle_bin_uuid,
537            root_group: root_group,
538        };
539
540        Ok(db)
541    }
542}
543
544fn entry_contains_string(entry: &Entry, name: &String) -> bool {
545    for value in entry.strings.values() {
546        match *value {
547            StringValue::Plain(ref string) => {
548                if string.to_lowercase().contains(name) {
549                    return true;
550                }
551            }
552            StringValue::Protected(_) => {}
553        }
554    }
555    false
556}
557
558#[cfg(test)]
559mod tests {
560
561    use super::*;
562    use crate::types::BinariesMap;
563    use crate::types::CompositeKey;
564    use crate::types::Compression;
565    use crate::types::CustomDataMap;
566    use crate::types::CustomIconsMap;
567    use crate::types::DbType;
568    use crate::types::GroupUuid;
569    use crate::types::MasterCipher;
570    use crate::types::StreamCipher;
571    use crate::types::TransformRounds;
572    use crate::types::Version;
573    use crate::utils::test::approx_equal_datetime;
574    use chrono::Utc;
575
576    #[test]
577    fn test_new_returns_correct_instance() {
578        let now = Utc::now();
579        let key = CompositeKey::from_password("5pZ5mgpTkLCDaM46IuH7yGafZFIICyvC");
580        let db = Database::new(&key);
581        assert_eq!(db.comment, None);
582        assert_eq!(db.composite_key, key);
583        assert_eq!(db.compression, Compression::GZip);
584        assert_eq!(db.db_type, DbType::Kdb2);
585        assert_eq!(db.master_cipher, MasterCipher::Aes256);
586        assert_eq!(db.stream_cipher, StreamCipher::Salsa20);
587        assert_eq!(db.transform_rounds, TransformRounds(10000));
588        assert_eq!(db.version, Version::new_kdb2());
589        assert_eq!(db.binaries, BinariesMap::new());
590        assert_eq!(db.color, None);
591        assert_eq!(db.custom_data, CustomDataMap::new());
592        assert_eq!(db.custom_icons, CustomIconsMap::new());
593        assert_eq!(db.def_username, "");
594        assert!(approx_equal_datetime(db.def_username_changed, now));
595        assert_eq!(db.description, "");
596        assert!(approx_equal_datetime(db.description_changed, now));
597        assert!(approx_equal_datetime(db.entry_templates_group_changed, now));
598        assert_eq!(db.entry_templates_group_uuid, GroupUuid::nil());
599        assert_eq!(db.generator, "rust-kpdb");
600        assert_eq!(db.history_max_items, 10);
601        assert_eq!(db.history_max_size, 6291456);
602        assert_eq!(db.last_selected_group, GroupUuid::nil());
603        assert_eq!(db.last_top_visible_group, GroupUuid::nil());
604        assert_eq!(db.maintenance_history_days, 365);
605        assert_eq!(db.master_key_change_force, -1);
606        assert_eq!(db.master_key_change_rec, -1);
607        assert!(approx_equal_datetime(db.master_key_changed, now));
608        assert_eq!(db.name, "");
609        assert!(approx_equal_datetime(db.name_changed, now));
610        assert_eq!(db.protect_notes, false);
611        assert_eq!(db.protect_password, true);
612        assert_eq!(db.protect_title, false);
613        assert_eq!(db.protect_url, false);
614        assert_eq!(db.protect_username, false);
615        assert!(approx_equal_datetime(db.recycle_bin_changed, now));
616        assert_eq!(db.recycle_bin_enabled, true);
617        assert_eq!(db.recycle_bin_uuid, GroupUuid::nil());
618        assert!(db.root_group.uuid != GroupUuid::nil());
619    }
620
621    #[test]
622    fn test_find_entries_returns_correct_entries() {
623        let db = db_with_groups_and_entries();
624        let result = db.find_entries("Proton");
625        assert_eq!(result.len(), 2);
626
627        let result = db.find_entries("Unknown");
628        assert_eq!(result.len(), 0);
629    }
630
631    #[test]
632    fn test_find_entries_mut_returns_correct_entries() {
633        let mut db = db_with_groups_and_entries();
634        {
635            let result = db.find_entries_mut("Proton");
636            assert_eq!(result.len(), 2);
637        }
638        {
639            let result = db.find_entries_mut("Unknown");
640            assert_eq!(result.len(), 0);
641        }
642    }
643
644    #[test]
645    fn test_find_groups_returns_correct_groups() {
646        let db = db_with_groups_and_entries();
647        let result = db.find_groups("mail");
648        assert_eq!(result.len(), 1);
649
650        let result = db.find_groups("Unknown");
651        assert_eq!(result.len(), 0);
652    }
653
654    #[test]
655    fn test_find_groups_mut_returns_correct_groups() {
656        let mut db = db_with_groups_and_entries();
657        {
658            let result = db.find_groups_mut("mail");
659            assert_eq!(result.len(), 1);
660        }
661        {
662            let result = db.find_groups_mut("Unknown");
663            assert_eq!(result.len(), 0);
664        }
665    }
666
667    #[test]
668    fn test_get_entry_returns_correct_entry() {
669        let entry = Entry::new();
670        let entry_uuid = entry.uuid;
671
672        let mut group = Group::new("Group");
673        group.add_entry(entry.clone());
674
675        let mut db = Database::new(&CompositeKey::from_password("test"));
676        assert_eq!(db.get_entry(entry_uuid), None);
677
678        db.root_group.add_group(group);
679        assert_eq!(db.get_entry(entry_uuid), Some(&entry));
680    }
681
682    #[test]
683    fn test_get_entry_mut_returns_correct_entry() {
684        let mut entry = Entry::new();
685        let entry_uuid = entry.uuid;
686
687        let mut group = Group::new("Group");
688        group.add_entry(entry.clone());
689
690        let mut db = Database::new(&CompositeKey::from_password("test"));
691        assert_eq!(db.get_entry_mut(entry_uuid), None);
692
693        db.root_group.add_group(group);
694        assert_eq!(db.get_entry_mut(entry_uuid), Some(&mut entry));
695    }
696
697    #[test]
698    fn test_get_group_returns_correct_group() {
699        let group = Group::new("Group");
700        let group_uuid = group.uuid;
701
702        let mut db = Database::new(&CompositeKey::from_password("test"));
703        assert_eq!(db.get_group(group_uuid), None);
704
705        db.root_group.add_group(group.clone());
706        assert_eq!(db.get_group(group_uuid), Some(&group));
707    }
708
709    #[test]
710    fn test_get_group_mut_returns_correct_group() {
711        let mut group = Group::new("Group");
712        let group_uuid = group.uuid;
713
714        let mut db = Database::new(&CompositeKey::from_password("test"));
715        assert_eq!(db.get_group_mut(group_uuid), None);
716
717        db.root_group.add_group(group.clone());
718        assert_eq!(db.get_group_mut(group_uuid), Some(&mut group));
719    }
720
721    fn db_with_groups_and_entries() -> Database {
722        let mut gmail = Entry::new();
723        gmail.set_title("Gmail");
724        gmail.set_username("guser");
725        gmail.set_password("gpass");
726        gmail.set_url("https://mail.google.com");
727
728        let mut protonmail = Entry::new();
729        protonmail.set_title("ProtonMail");
730        protonmail.set_username("puser");
731        protonmail.set_password("ppass");
732        protonmail.set_url("https://mail.protonmail.com");
733
734        let mut protonvpn = Entry::new();
735        protonvpn.set_title("ProtonVPN");
736        protonvpn.set_username("puser");
737        protonvpn.set_password("ppass");
738        protonvpn.set_url("https://prontvpn.com");
739
740        let mut email_group = Group::new("Email");
741        email_group.add_entry(gmail);
742        email_group.add_entry(protonmail);
743
744        let mut vpn_group = Group::new("VPN");
745        vpn_group.add_entry(protonvpn);
746
747        let mut db = Database::new(&CompositeKey::from_password("test"));
748        db.root_group.add_group(email_group);
749        db.root_group.add_group(vpn_group);
750        db
751    }
752}