kpdb/types/
database.rs

1// Copyright (c) 2016-2017 Martijn Rijkeboer <[email protected]>
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 chrono::{DateTime, Utc};
10use common;
11use format::{kdb2_reader, kdb2_writer};
12use io::{Log, LogReader, LogWriter};
13use std::io::{Read, Write};
14use super::binaries_map::BinariesMap;
15use super::color::Color;
16use super::comment::Comment;
17use super::composite_key::CompositeKey;
18use super::compression::Compression;
19use super::custom_data_map::CustomDataMap;
20use super::custom_icons_map::CustomIconsMap;
21use super::db_type::DbType;
22use super::entry::Entry;
23use super::entry_uuid::EntryUuid;
24use super::error::Error;
25use super::group::Group;
26use super::group_uuid::GroupUuid;
27use super::master_cipher::MasterCipher;
28use super::result::Result;
29use super::stream_cipher::StreamCipher;
30use super::string_value::StringValue;
31use super::transform_rounds::TransformRounds;
32use super::version::Version;
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
244    /// Returns a vector with mutable entries that match (case insensitive) the supplied text.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use kpdb::{CompositeKey, Database, Entry, Group};
250    ///
251    /// let mut protonmail = Entry::new();
252    /// protonmail.set_title("ProtonMail");
253    /// protonmail.set_username("puser");
254    /// protonmail.set_password("ppass");
255    /// protonmail.set_url("https://mail.protonmail.com");
256    ///
257    /// let mut group = Group::new("Email");
258    /// group.add_entry(protonmail);
259    ///
260    /// let mut db = Database::new(&CompositeKey::from_password("test"));
261    /// db.root_group.add_group(group);
262    ///
263    /// let result = db.find_entries_mut("Protonm");
264    /// assert_eq!(result.len(), 1);
265    /// ```
266    pub fn find_entries_mut<'a, S: Into<String>>(&'a mut self, text: S) -> Vec<&'a mut Entry> {
267        let mut list = Vec::new();
268        let text = &text.into().to_lowercase();
269        for group in self.root_group.iter_mut() {
270            for entry in group.entries.iter_mut() {
271                if entry_contains_string(entry, text) {
272                    list.push(entry);
273                }
274            }
275        }
276        list
277    }
278
279    /// Returns a vector with groups that match (case insensitive) the supplied name.
280    ///
281    /// # Examples
282    ///
283    /// ```rust
284    /// use kpdb::{CompositeKey, Database, Group};
285    ///
286    /// let group = Group::new("Email");
287    ///
288    /// let mut db = Database::new(&CompositeKey::from_password("test"));
289    /// db.root_group.add_group(group);
290    ///
291    /// let result = db.find_groups("mail");
292    /// assert_eq!(result.len(), 1);
293    ///
294    /// let result = db.find_groups("unknown");
295    /// assert_eq!(result.len(), 0);
296    /// ```
297    pub fn find_groups<'a, S: Into<String>>(&'a self, name: S) -> Vec<&'a Group> {
298        let name = &name.into().to_lowercase();
299        self.root_group
300            .iter()
301            .filter(|g| g.name.to_lowercase().contains(name))
302            .collect::<Vec<&'a Group>>()
303    }
304
305    /// Returns a vector with mutable groups that match (case insensitive) the supplied name.
306    ///
307    /// # Examples
308    ///
309    /// ```rust
310    /// use kpdb::{CompositeKey, Database, Group};
311    ///
312    /// let group = Group::new("Email");
313    ///
314    /// let mut db = Database::new(&CompositeKey::from_password("test"));
315    /// db.root_group.add_group(group);
316    ///
317    /// let result = db.find_groups_mut("mail");
318    /// assert_eq!(result.len(), 1);
319    /// ```
320    pub fn find_groups_mut<'a, S: Into<String>>(&'a mut self, name: S) -> Vec<&'a mut Group> {
321        let name = &name.into().to_lowercase();
322        self.root_group
323            .iter_mut()
324            .filter(|g| g.name.to_lowercase().contains(name))
325            .collect::<Vec<&'a mut Group>>()
326    }
327
328    /// Returns the entry that matches the UUID or None if not found.
329    ///
330    /// # Examples
331    ///
332    /// ```rust
333    /// use kpdb::{CompositeKey, Database, Entry, Group};
334    ///
335    /// let entry = Entry::new();
336    /// let entry_uuid = entry.uuid;
337    ///
338    /// let mut db = Database::new(&CompositeKey::from_password("test"));
339    /// assert_eq!(db.get_entry(entry_uuid), None);
340    ///
341    /// db.root_group.add_entry(entry.clone());
342    /// assert_eq!(db.get_entry(entry_uuid), Some(&entry));
343    /// ```
344    pub fn get_entry<'a>(&'a self, uuid: EntryUuid) -> Option<&'a Entry> {
345        for group in self.root_group.iter() {
346            for entry in group.entries.iter() {
347                if entry.uuid == uuid {
348                    return Some(entry);
349                }
350            }
351        }
352        None
353    }
354
355    /// Returns the mutable entry that matches the UUID or None if not found.
356    ///
357    /// # Examples
358    ///
359    /// ```rust
360    /// use kpdb::{CompositeKey, Database, Entry, Group};
361    ///
362    /// let mut entry = Entry::new();
363    /// let entry_uuid = entry.uuid;
364    ///
365    /// let mut db = Database::new(&CompositeKey::from_password("test"));
366    /// assert_eq!(db.get_entry_mut(entry_uuid), None);
367    ///
368    /// db.root_group.add_entry(entry.clone());
369    /// assert_eq!(db.get_entry_mut(entry_uuid), Some(&mut entry));
370    /// ```
371    pub fn get_entry_mut<'a>(&'a mut self, uuid: EntryUuid) -> Option<&'a mut Entry> {
372        for group in self.root_group.iter_mut() {
373            for entry in group.entries.iter_mut() {
374                if entry.uuid == uuid {
375                    return Some(entry);
376                }
377            }
378        }
379        None
380    }
381
382    /// Returns the group that matches the UUID or None if not found.
383    ///
384    /// # Examples
385    ///
386    /// ```rust
387    /// use kpdb::{CompositeKey, Database, Group};
388    ///
389    /// let group = Group::new("Group");
390    /// let group_uuid = group.uuid;
391    ///
392    /// let mut db = Database::new(&CompositeKey::from_password("test"));
393    /// assert_eq!(db.get_group(group_uuid), None);
394    ///
395    /// db.root_group.add_group(group.clone());
396    /// assert_eq!(db.get_group(group_uuid), Some(&group));
397    /// ```
398    pub fn get_group<'a>(&'a self, uuid: GroupUuid) -> Option<&'a Group> {
399        self.root_group.iter().find(|g| g.uuid == uuid)
400    }
401
402    /// Returns the mutable group that matches the UUID or None if not found.
403    ///
404    /// # Examples
405    ///
406    /// ```rust
407    /// use kpdb::{CompositeKey, Database, Group};
408    ///
409    /// let mut group = Group::new("Group");
410    /// let group_uuid = group.uuid;
411    ///
412    /// let mut db = Database::new(&CompositeKey::from_password("test"));
413    /// assert_eq!(db.get_group(group_uuid), None);
414    ///
415    /// db.root_group.add_group(group.clone());
416    /// assert_eq!(db.get_group_mut(group_uuid), Some(&mut group));
417    /// ```
418    pub fn get_group_mut<'a>(&'a mut self, uuid: GroupUuid) -> Option<&'a mut Group> {
419        self.root_group.iter_mut().find(|g| g.uuid == uuid)
420    }
421
422    /// Attempts to open an existing database.
423    ///
424    /// # Examples
425    ///
426    /// ```rust,no_run
427    /// # use kpdb::Result;
428    /// use kpdb::{CompositeKey, Database};
429    /// use std::fs::File;
430    ///
431    /// # fn open_example() -> Result<()> {
432    /// let mut file = try!(File::open("passwords.kdbx"));
433    /// let key = CompositeKey::from_password("password");
434    /// let db = try!(Database::open(&mut file, &key));
435    /// # Ok(())
436    /// # }
437    /// ```
438    pub fn open<R: Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
439        let mut reader = LogReader::new(reader);
440        let mut buffer = [0u8; 4];
441
442        try!(reader.read(&mut buffer));
443        if buffer != common::DB_SIGNATURE {
444            return Err(Error::InvalidDbSignature(buffer));
445        }
446
447        try!(reader.read(&mut buffer));
448        if buffer == common::KDB1_SIGNATURE {
449            return Err(Error::UnhandledDbType(buffer));
450        } else if buffer == common::KDB2_SIGNATURE {
451            Database::open_kdb2(&mut reader, key)
452        } else {
453            return Err(Error::UnhandledDbType(buffer));
454        }
455    }
456
457    /// Attempts to save the database.
458    ///
459    /// # Examples
460    ///
461    /// ```rust,no_run
462    /// # use kpdb::Result;
463    /// use kpdb::{CompositeKey, Database};
464    /// use std::fs::File;
465    ///
466    /// # fn save_example() -> Result<()> {
467    /// let key = CompositeKey::from_password("password");
468    /// let db = Database::new(&key);
469    /// let mut file = try!(File::create("new.kdbx"));
470    ///
471    /// try!(db.save(&mut file));
472    /// # Ok(())
473    /// # }
474    /// ```
475    pub fn save<W: Write>(&self, writer: &mut W) -> Result<()> {
476        let mut writer = LogWriter::new(writer);
477        match self.db_type {
478            DbType::Kdb1 => Err(Error::Unimplemented(String::from("KeePass v1 not supported"))),
479            DbType::Kdb2 => kdb2_writer::write(&mut writer, self),
480        }
481    }
482
483    fn open_kdb2<R: Log + Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
484        let (meta_data, xml_data) = try!(kdb2_reader::read(reader, key));
485        match xml_data.header_hash {
486            Some(header_hash) => {
487                if meta_data.header_hash != header_hash {
488                    return Err(Error::InvalidHeaderHash);
489                }
490            }
491            None => {}
492        }
493
494        let root_group = match xml_data.root_group {
495            Some(group) => group,
496            None => Group::new(common::ROOT_GROUP_NAME),
497        };
498
499        let db = Database {
500            comment: meta_data.comment,
501            composite_key: key.clone(),
502            compression: meta_data.compression,
503            db_type: DbType::Kdb2,
504            master_cipher: meta_data.master_cipher,
505            stream_cipher: meta_data.stream_cipher,
506            transform_rounds: meta_data.transform_rounds,
507            version: meta_data.version,
508
509            binaries: xml_data.binaries,
510            color: xml_data.color,
511            custom_data: xml_data.custom_data,
512            custom_icons: xml_data.custom_icons,
513            def_username: xml_data.def_username,
514            def_username_changed: xml_data.def_username_changed,
515            description: xml_data.description,
516            description_changed: xml_data.description_changed,
517            entry_templates_group_changed: xml_data.entry_templates_group_changed,
518            entry_templates_group_uuid: xml_data.entry_templates_group_uuid,
519            generator: xml_data.generator,
520            history_max_items: xml_data.history_max_items,
521            history_max_size: xml_data.history_max_size,
522            last_selected_group: xml_data.last_selected_group,
523            last_top_visible_group: xml_data.last_top_visible_group,
524            maintenance_history_days: xml_data.maintenance_history_days,
525            master_key_change_force: xml_data.master_key_change_force,
526            master_key_change_rec: xml_data.master_key_change_rec,
527            master_key_changed: xml_data.master_key_changed,
528            name: xml_data.name,
529            name_changed: xml_data.name_changed,
530            protect_notes: xml_data.protect_notes,
531            protect_password: xml_data.protect_password,
532            protect_title: xml_data.protect_title,
533            protect_url: xml_data.protect_url,
534            protect_username: xml_data.protect_username,
535            recycle_bin_changed: xml_data.recycle_bin_changed,
536            recycle_bin_enabled: xml_data.recycle_bin_enabled,
537            recycle_bin_uuid: xml_data.recycle_bin_uuid,
538            root_group: root_group,
539        };
540
541        Ok(db)
542    }
543}
544
545fn entry_contains_string(entry: &Entry, name: &String) -> bool {
546    for value in entry.strings.values() {
547        match *value {
548            StringValue::Plain(ref string) => {
549                if string.to_lowercase().contains(name) {
550                    return true;
551                }
552            }
553            StringValue::Protected(_) => {}
554        }
555    }
556    false
557}
558
559#[cfg(test)]
560mod tests {
561
562    use chrono::Utc;
563    use super::*;
564    use types::BinariesMap;
565    use types::CompositeKey;
566    use types::Compression;
567    use types::CustomDataMap;
568    use types::CustomIconsMap;
569    use types::DbType;
570    use types::GroupUuid;
571    use types::MasterCipher;
572    use types::StreamCipher;
573    use types::TransformRounds;
574    use types::Version;
575    use utils::test::approx_equal_datetime;
576
577    #[test]
578    fn test_new_returns_correct_instance() {
579        let now = Utc::now();
580        let key = CompositeKey::from_password("5pZ5mgpTkLCDaM46IuH7yGafZFIICyvC");
581        let db = Database::new(&key);
582        assert_eq!(db.comment, None);
583        assert_eq!(db.composite_key, key);
584        assert_eq!(db.compression, Compression::GZip);
585        assert_eq!(db.db_type, DbType::Kdb2);
586        assert_eq!(db.master_cipher, MasterCipher::Aes256);
587        assert_eq!(db.stream_cipher, StreamCipher::Salsa20);
588        assert_eq!(db.transform_rounds, TransformRounds(10000));
589        assert_eq!(db.version, Version::new_kdb2());
590        assert_eq!(db.binaries, BinariesMap::new());
591        assert_eq!(db.color, None);
592        assert_eq!(db.custom_data, CustomDataMap::new());
593        assert_eq!(db.custom_icons, CustomIconsMap::new());
594        assert_eq!(db.def_username, "");
595        assert!(approx_equal_datetime(db.def_username_changed, now));
596        assert_eq!(db.description, "");
597        assert!(approx_equal_datetime(db.description_changed, now));
598        assert!(approx_equal_datetime(db.entry_templates_group_changed, now));
599        assert_eq!(db.entry_templates_group_uuid, GroupUuid::nil());
600        assert_eq!(db.generator, "rust-kpdb");
601        assert_eq!(db.history_max_items, 10);
602        assert_eq!(db.history_max_size, 6291456);
603        assert_eq!(db.last_selected_group, GroupUuid::nil());
604        assert_eq!(db.last_top_visible_group, GroupUuid::nil());
605        assert_eq!(db.maintenance_history_days, 365);
606        assert_eq!(db.master_key_change_force, -1);
607        assert_eq!(db.master_key_change_rec, -1);
608        assert!(approx_equal_datetime(db.master_key_changed, now));
609        assert_eq!(db.name, "");
610        assert!(approx_equal_datetime(db.name_changed, now));
611        assert_eq!(db.protect_notes, false);
612        assert_eq!(db.protect_password, true);
613        assert_eq!(db.protect_title, false);
614        assert_eq!(db.protect_url, false);
615        assert_eq!(db.protect_username, false);
616        assert!(approx_equal_datetime(db.recycle_bin_changed, now));
617        assert_eq!(db.recycle_bin_enabled, true);
618        assert_eq!(db.recycle_bin_uuid, GroupUuid::nil());
619        assert!(db.root_group.uuid != GroupUuid::nil());
620    }
621
622    #[test]
623    fn test_find_entries_returns_correct_entries() {
624        let db = db_with_groups_and_entries();
625        let result = db.find_entries("Proton");
626        assert_eq!(result.len(), 2);
627
628        let result = db.find_entries("Unknown");
629        assert_eq!(result.len(), 0);
630    }
631
632    #[test]
633    fn test_find_entries_mut_returns_correct_entries() {
634        let mut db = db_with_groups_and_entries();
635        {
636            let result = db.find_entries_mut("Proton");
637            assert_eq!(result.len(), 2);
638        }
639        {
640            let result = db.find_entries_mut("Unknown");
641            assert_eq!(result.len(), 0);
642        }
643    }
644
645    #[test]
646    fn test_find_groups_returns_correct_groups() {
647        let db = db_with_groups_and_entries();
648        let result = db.find_groups("mail");
649        assert_eq!(result.len(), 1);
650
651        let result = db.find_groups("Unknown");
652        assert_eq!(result.len(), 0);
653    }
654
655    #[test]
656    fn test_find_groups_mut_returns_correct_groups() {
657        let mut db = db_with_groups_and_entries();
658        {
659            let result = db.find_groups_mut("mail");
660            assert_eq!(result.len(), 1);
661        }
662        {
663            let result = db.find_groups_mut("Unknown");
664            assert_eq!(result.len(), 0);
665        }
666    }
667
668    #[test]
669    fn test_get_entry_returns_correct_entry() {
670        let entry = Entry::new();
671        let entry_uuid = entry.uuid;
672
673        let mut group = Group::new("Group");
674        group.add_entry(entry.clone());
675
676        let mut db = Database::new(&CompositeKey::from_password("test"));
677        assert_eq!(db.get_entry(entry_uuid), None);
678
679        db.root_group.add_group(group);
680        assert_eq!(db.get_entry(entry_uuid), Some(&entry));
681    }
682
683    #[test]
684    fn test_get_entry_mut_returns_correct_entry() {
685        let mut entry = Entry::new();
686        let entry_uuid = entry.uuid;
687
688        let mut group = Group::new("Group");
689        group.add_entry(entry.clone());
690
691        let mut db = Database::new(&CompositeKey::from_password("test"));
692        assert_eq!(db.get_entry_mut(entry_uuid), None);
693
694        db.root_group.add_group(group);
695        assert_eq!(db.get_entry_mut(entry_uuid), Some(&mut entry));
696    }
697
698    #[test]
699    fn test_get_group_returns_correct_group() {
700        let group = Group::new("Group");
701        let group_uuid = group.uuid;
702
703        let mut db = Database::new(&CompositeKey::from_password("test"));
704        assert_eq!(db.get_group(group_uuid), None);
705
706        db.root_group.add_group(group.clone());
707        assert_eq!(db.get_group(group_uuid), Some(&group));
708    }
709
710    #[test]
711    fn test_get_group_mut_returns_correct_group() {
712        let mut group = Group::new("Group");
713        let group_uuid = group.uuid;
714
715        let mut db = Database::new(&CompositeKey::from_password("test"));
716        assert_eq!(db.get_group_mut(group_uuid), None);
717
718        db.root_group.add_group(group.clone());
719        assert_eq!(db.get_group_mut(group_uuid), Some(&mut group));
720    }
721
722    fn db_with_groups_and_entries() -> Database {
723        let mut gmail = Entry::new();
724        gmail.set_title("Gmail");
725        gmail.set_username("guser");
726        gmail.set_password("gpass");
727        gmail.set_url("https://mail.google.com");
728
729        let mut protonmail = Entry::new();
730        protonmail.set_title("ProtonMail");
731        protonmail.set_username("puser");
732        protonmail.set_password("ppass");
733        protonmail.set_url("https://mail.protonmail.com");
734
735        let mut protonvpn = Entry::new();
736        protonvpn.set_title("ProtonVPN");
737        protonvpn.set_username("puser");
738        protonvpn.set_password("ppass");
739        protonvpn.set_url("https://prontvpn.com");
740
741        let mut email_group = Group::new("Email");
742        email_group.add_entry(gmail);
743        email_group.add_entry(protonmail);
744
745        let mut vpn_group = Group::new("VPN");
746        vpn_group.add_entry(protonvpn);
747
748        let mut db = Database::new(&CompositeKey::from_password("test"));
749        db.root_group.add_group(email_group);
750        db.root_group.add_group(vpn_group);
751        db
752    }
753}