Repository: firecat53/keepmenu (language: python)

Issue:
Feature: Add CLI options for databases
I manage multiple databases that I access with keepmenu via a keyboard shortcut. While using the 'Open another database' dialog is functional, after multiple uses a lot of time is wasted typing, and it's confusing to know what database you are on when it loads from cache.

Using keepmenu with a CLI option for the database you want would be helpful as I could set multiple shortcuts to distinguish them.
Database specific autotype
closes #92


Discussion / hints:
Would including the database path in the title on the first page make it easier to see which database?

I only ask that because it's a much quicker solution to implement than CLI flags 😀
It would help a lot, although the 'Open another database' dialog issue remains. I frequently switch from my passwords database to my TOTP database to enter information and the time to switch really builds up.

I was thinking it could input the argument and bypass the "Select Database" prompt if it used. The databases would already be configured in config.ini, but the user could specify one with something like '--database 2'.


I'd probably use an absolute path and also include a path to keyfile option.

Out of curiosity will you be taking advantage of the new TOTP feature I merged yesterday? If so, I'd love some feedback on usability and usefulness!
I agree, just thought it would be the easiest to add.

I've been using the totp branch in the pull request for a long time now and I think it works pretty well. Adding/editing is seamless and so is the autofill, but for my particular case I had a small issue with it. Now that it's been merged I've added an issue here #92.


Fix patch:
diff --git a/README.rst b/README.rst
index 46266a7..61314e5 100644
--- a/README.rst
+++ b/README.rst
@@ -138,12 +138,16 @@ Usage
 
 Options
 -------
-keepmenu [-h] [-a AUTOTYPE]
+usage: keepmenu [-h] [-a AUTOTYPE] [-d DATABASE] [-k KEY_FILE]
 
 --help, -h Output a usage message and exit.
 
 -a AUTOTYPE, --autotype AUTOTYPE Override autotype sequence in config.ini
 
+-d DATABASE, --database DATABASE File path to a database to open, skipping the database selection menu
+
+-k KEYFILE, --keyfile KEY_FILE File path of the keyfile needed to open the database specified by --database/-d
+
 Tests
 -----
 
diff --git a/keepmenu b/keepmenu
index d6e7d6f..5b39eac 100755
--- a/keepmenu
+++ b/keepmenu
@@ -7,15 +7,16 @@ import argparse
 import base64
 import configparser
 from contextlib import closing
+from copy import deepcopy
 from datetime import datetime, timedelta
 import errno
 import functools
 import hmac
 import locale
-from multiprocessing import Event, Process
+from multiprocessing import Event, Process, Pipe
 from multiprocessing.managers import BaseManager
 import os
-from os.path import exists, expanduser
+from os.path import exists, expanduser, realpath
 import random
 import re
 from secrets import choice
@@ -34,7 +35,6 @@ from urllib import parse
 import construct
 from pynput import keyboard
 
-
 AUTH_FILE = expanduser("~/.cache/.keepmenu-auth")
 CONF_FILE = expanduser("~/.config/keepmenu/config.ini")
 SERCRET_VALID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
@@ -286,29 +286,59 @@ def get_password_chars():
     return {k: presets[k] for k in char_sel.split('\n')} if char_sel else False
 
 
-def get_database(open_databases=[]):
-    """Read databases from config or ask for user input.
+class DataBase():  # pylint: disable=too-few-public-methods
+    """Define a database class for clearer reference to variables
 
-    Args: open_databases - list [(db_name, keyfile, pw, autotype), (db_name2, kf, pw, autotype),...]
-    Returns: (database name, keyfile, passphrase, autotype)
-             Returns (None, None, None, None) on error selecting database or password
+        dbase - string, filename
+        kfile - string, filename
+        pword - string, password
+        atype - string, autotype sequence
+        kpo - PyKeePass object
+        is_active - bool, is this the currently active database
 
     """
-    args = CONF.items('database')
-    args_dict = dict(args)
+    def __init__(self, dbase=None, kfile=None, pword=None, atype=None, kpo=None):
+        self.dbase = expanduser('' if dbase is None else dbase)
+        self.dbase = realpath(self.dbase) if self.dbase else ''
+        self.kfile = expanduser('' if kfile is None else kfile)
+        self.kfile = realpath(self.kfile) if self.kfile else ''
+        self.pword = pword
+        self.atype = atype
+        self.kpo = kpo
+        self.is_active = False
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return self.__dict__ == other.__dict__
+        return False
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+def get_databases():
+    """Read databases from config
+
+    Returns: [DataBase obj, DataBase obj2,...]
+             If not specified in the config, the value will be None
+             If database name is None, an error has occurred
+
+    """
+    dargs = CONF.items('database')
+    args_dict = dict(dargs)
     dbases = [i for i in args_dict if i.startswith('database')]
     dbs = []
     for dbase in dbases:
-        dbn = expanduser(args_dict[dbase])
+        dbn = args_dict[dbase]
         idx = dbase.rsplit('_', 1)[-1]
         try:
-            keyfile = expanduser(args_dict['keyfile_{}'.format(idx)])
+            keyfile = args_dict['keyfile_{}'.format(idx)]
         except KeyError:
-            keyfile = ''
+            keyfile = None
         try:
             passw = args_dict['password_{}'.format(idx)]
         except KeyError:
-            passw = ''
+            passw = None
         try:
             autotype = args_dict['autotype_default_{}'.format(idx)]
         except KeyError:
@@ -324,34 +354,87 @@ def get_database(open_databases=[]):
         except KeyError:
             pass
         if dbn:
-            dbs.append((dbn, keyfile, passw, autotype))
-    if not dbs:
+            dbo = DataBase(dbase=dbn, kfile=keyfile, pword=passw, atype=autotype)
+            dbs.append(dbo)
+
+    return dbs
+
+
+def get_database(open_databases=None, **kwargs):
+    """Read databases/keyfile/autotype from config, CLI, or ask for user input.
+
+    Args: open_databases - list [DataBase1, DataBase2,...]
+          kwargs - possibly 'database', 'keyfile'
+    Returns: DataBase obj or None on error selecting database or password
+             open_databases - list [DataBase1, DataBase2,...]
+                (open_databases is managed in get_database but stored in
+                DmenuRunner for persistence instead of using a global var)
+
+    """
+    dbs_cfg = get_databases()
+    dbs_cfg_n = [i.dbase for i in dbs_cfg]
+    open_databases = open_databases or {}
+    clidb = DataBase(dbase=kwargs.get('database'),
+                     kfile=kwargs.get('keyfile'),
+                     atype=kwargs.get('autotype'))
+    if not dbs_cfg and not clidb.dbase and not open_databases:
+        # First run  database opening
         res = get_initial_db()
         if res is True:
-            dbs = [get_database()]
+            db_, open_databases = get_database()
+            dbs = [db_]
+        else:
+            return None, open_databases
+    elif clidb.dbase:
+        # Prefer db and autotype passed via cli
+        db_ = [i for i in open_databases.values() if i.dbase == clidb.dbase]
+        if db_:
+            dbs = [deepcopy(db_[0])]
+            if clidb.atype:
+                dbs[0].atype = clidb.atype
         else:
-            return (None, None, None, None)
+            dbs = [deepcopy(clidb)]
+            # Use existing keyfile if available
+            if not dbs[0].kfile and dbs[0].dbase in dbs_cfg_n:
+                dbs[0].kfile = dbs_cfg[dbs_cfg_n.index(dbs[0].dbase)].kfile
+            # Use existing password if available
+            if not dbs[0].kfile and dbs[0].dbase in dbs_cfg_n:
+                dbs[0].kfile = dbs_cfg[dbs_cfg_n.index(dbs[0].dbase)].kfile
+    elif clidb.atype:
+        # If only autotype is passed, use current db
+        db_ = [i for i in open_databases.values() if i.is_active is True][0]
+        dbs = [deepcopy(db_)]
+        dbs[0].atype = clidb.atype
+    elif open_databases:
+        # if there are dbs already open, make a list of those + dbs from config.ini
+        dbs = [deepcopy(i) for i in open_databases.values()]
+        for db_ in dbs_cfg:
+            if db_.dbase not in open_databases:
+                dbs.append(deepcopy(db_))
+    else:
+        dbs = dbs_cfg
     if len(dbs) > 1:
-        inp_bytes = "\n".join(i[0] for i in dbs).encode(ENC)
+        inp_bytes = "\n".join(i.dbase for i in dbs).encode(ENC)
         sel = dmenu_select(len(dbs), "Select Database", inp=inp_bytes)
-        dbs = [i for i in dbs if i[0] == sel]
+        dbs = [i for i in dbs if i.dbase == sel]
         if not sel or not dbs:
-            return (None, None, None, None)
-    for dbr in open_databases:
-        # Match db name to open_databases
-        if dbr[0] == dbs[0][0]:
-            return dbr
-    if not dbs[0][-2]:
-        db_l = list(dbs[0])
-        db_l[-2] = get_passphrase()
-        if not db_l[-2]:
-            return (None, None, None, None)
-        dbs[0] = db_l
-    return tuple(dbs[0])
+            return None, open_databases
+    if not dbs[0].pword:
+        dbs[0].pword = get_passphrase()
+        if not dbs[0].pword:
+            return None, open_databases
+    for db_ in open_databases.values():
+        db_.is_active = False
+    if dbs[0].dbase not in open_databases:
+        open_databases[dbs[0].dbase] = deepcopy(dbs[0])
+    if dbs[0].dbase in dbs_cfg_n:
+        open_databases[dbs[0].dbase].atype = dbs_cfg[dbs_cfg_n.index(dbs[0].dbase)].atype
+    open_databases[dbs[0].dbase].is_active = True
+    return dbs[0], open_databases
 
 
 def get_initial_db():
-    """Ask for initial database name and keyfile if not entered in config file
+    """Ask for and set initial database name and keyfile if not entered in config file
 
     """
     db_name = dmenu_select(0, "Enter path to existing "
@@ -371,26 +454,25 @@ def get_initial_db():
 def get_entries(dbo):
     """Open keepass database and return the PyKeePass object
 
-        Args: dbo: tuple (db path, keyfile path, password)
-        Returns: PyKeePass object
+        Args: dbo: DataBase object
+        Returns: PyKeePass object or None
 
     """
     from pykeepass import PyKeePass
-    dbf, keyfile, password, _ = dbo
-    if dbf is None:
+    if dbo.dbase is None:
         return None
     try:
-        kpo = PyKeePass(dbf, password, keyfile=keyfile)
+        kpo = PyKeePass(dbo.dbase, dbo.pword, keyfile=dbo.kfile)
     except (FileNotFoundError, construct.core.ChecksumError) as err:
         if str(err.args[0]).startswith("wrong checksum"):
             dmenu_err("Invalid Password or keyfile")
             return None
         try:
             if err.errno == errno.ENOENT:
-                if not os.path.isfile(dbf):
-                    dmenu_err("Database does not exist. Edit ~/.config/keepmenu/config.ini")
-                elif not os.path.isfile(keyfile):
-                    dmenu_err("Keyfile does not exist. Edit ~/.config/keepmenu/config.ini")
+                if not os.path.isfile(dbo.dbase):
+                    dmenu_err("Database does not exist. Check path and filename.")
+                elif not os.path.isfile(dbo.kfile):
+                    dmenu_err("Keyfile does not exist. Check path and filename.")
         except AttributeError:
             pass
         return None
@@ -507,8 +589,6 @@ def type_entry(entry, db_autotype=None):
             entry.autotype_sequence is not None and \
             entry.autotype_sequence != 'None':
         sequence = entry.autotype_sequence
-    if ARGS.autotype is not None:
-        sequence = ARGS.autotype
     tokens = tokenize_autotype(sequence)
 
     library = 'pynput'
@@ -891,7 +971,7 @@ def type_text(data):
                       "Try setting `type_library = xdotool` in config.ini")
 
 
-def view_all_entries(options, kp_entries):
+def view_all_entries(options, kp_entries, dbname):
     """Generate numbered list of all Keepass entries and open with dmenu.
 
     Returns: dmenu selection
@@ -911,7 +991,9 @@ def view_all_entries(options, kp_entries):
         entries_b = options_b + kp_entries_b
     else:
         entries_b = kp_entries_b
-    return dmenu_select(min(MAX_LEN, len(options) + len(kp_entries)), inp=entries_b)
+    return dmenu_select(min(MAX_LEN, len(options) + len(kp_entries)),
+                        inp=entries_b,
+                        prompt="Entries: {}".format(dbname))
 
 
 def select_group(kpo, prompt="Groups"):
@@ -1433,6 +1515,8 @@ def client():
     port, auth = get_auth()
     mgr = BaseManager(address=('', port), authkey=auth)
     mgr.register('set_event')
+    mgr.register('get_pipe')
+    mgr.register('read_args_from_pipe')
     mgr.connect()
     return mgr
 
@@ -1456,16 +1540,16 @@ class DmenuRunner(Process):
           kpo - Keepass object
     """
 
-    def __init__(self, server):
+    def __init__(self, server, **kwargs):
         Process.__init__(self)
         self.server = server
-        self.database = get_database()
-        self.kpo = get_entries(self.database)
-        if not self.kpo:
+        self.database, self.open_databases = get_database(**kwargs)
+        if self.database:
+            self.database.kpo = get_entries(self.database)
+        if not self.database or not self.database.kpo:
             self.server.kill_flag.set()
             sys.exit()
-        self.expiring = get_expiring_entries(self.kpo.entries)
-        self.open_databases = {self.database: self.kpo}
+        self.expiring = get_expiring_entries(self.database.kpo.entries)
         self.prev_entry = None
 
     def _set_timer(self):
@@ -1481,8 +1565,12 @@ class DmenuRunner(Process):
             self.server.start_flag.wait()
             if self.server.kill_flag.is_set():
                 break
-            if not self.kpo:
+            if not self.database or not self.database.kpo:
                 pass
+            elif self.server.args_flag.is_set():
+                dargs = self.server.get_args()
+                self.menu_open_another_database(**dargs)
+                self.server.args_flag.clear()
             else:
                 self.dmenu_run()
             if self.server.cache_time_expired.is_set():
@@ -1496,6 +1584,7 @@ class DmenuRunner(Process):
 
         """
         self.server.cache_time_expired.set()
+        self.server.args_flag.clear()
         if not self.server.start_flag.is_set():
             self.server.kill_flag.set()
             self.server.start_flag.set()
@@ -1506,13 +1595,13 @@ class DmenuRunner(Process):
         If 'hide_groups' is defined in config.ini, hide those from main and
         view/type all views.
 
-        Args: self.kpo - Keepass object
+        Args: self.database.kpo - Keepass object
 
         Note: I had to reload the kpo object after every save to prevent being
         affected by the gibberish password bug in pykeepass:
         https://github.com/pschmitt/pykeepass/issues/43
 
-        Once this is fixed, the extra calls to self.kpo = get_entries... can be
+        Once this is fixed, the extra calls to self.database.kpo = get_entries... can be
         deleted
 
         """
@@ -1525,12 +1614,12 @@ class DmenuRunner(Process):
             hid_groups = CONF.get("database", "hide_groups").split("\n")
             # Validate ignored group names in config.ini
             hid_groups = [i for i in hid_groups if i in
-                          [j.name for j in self.kpo.groups]]
+                          [j.name for j in self.database.kpo.groups]]
         else:
             hid_groups = []
 
         filtered_entries = [
-            i for i in self.kpo.entries if not
+            i for i in self.database.kpo.entries if not
             any(j in "/".join(i.path[:-1]) for j in hid_groups)
         ]
         options = {
@@ -1539,7 +1628,7 @@ class DmenuRunner(Process):
             'View previous entry': self.menu_view_previous_entry,
             'Edit expiring/expired passwords ({})'.format(len(self.expiring)):
                 functools.partial(self.menu_edit_entries, self.expiring),
-            'Edit entries': functools.partial(self.menu_edit_entries, self.kpo.entries),
+            'Edit entries': functools.partial(self.menu_edit_entries, self.database.kpo.entries),
             'Add entry': self.menu_add_entry,
             'Manage groups': self.menu_manage_groups,
             'Reload database': self.menu_reload_database,
@@ -1551,7 +1640,7 @@ class DmenuRunner(Process):
         if len(self.expiring) == 0:
             del options['Edit expiring/expired passwords (0)']
 
-        sel = view_all_entries(list(options), filtered_entries)
+        sel = view_all_entries(list(options), filtered_entries, self.database.dbase)
         if not sel:
             return
         if sel in options:
@@ -1562,8 +1651,11 @@ class DmenuRunner(Process):
                 entry = filtered_entries[int(sel.split('-', 1)[0])]
             except (ValueError, TypeError):
                 return
-            type_entry(entry, self.database[3])
+            type_entry(entry, self.database.atype)
             self.prev_entry = entry
+        # Reset database autotype in between runs
+        cur_db = [i for i in self.open_databases.values() if i.is_active is True][0]
+        self.database.atype = cur_db.atype
 
     def menu_view_type_individual_entries(self, hid_groups):
         """Process menu entry - View/Type individual entries
@@ -1571,10 +1663,10 @@ class DmenuRunner(Process):
         """
         options = []
         filtered_entries = [
-            i for i in self.kpo.entries if not
+            i for i in self.database.kpo.entries if not
             any(j in "/".join(i.path[:-1]) for j in hid_groups)
         ]
-        sel = view_all_entries(options, filtered_entries)
+        sel = view_all_entries(options, filtered_entries, self.database.dbase)
         try:
             entry = filtered_entries[int(sel.split('-', 1)[0])]
         except (ValueError, TypeError):
@@ -1596,65 +1688,64 @@ class DmenuRunner(Process):
 
         """
         options = []
-        sel = view_all_entries(options, entries)
+        sel = view_all_entries(options, entries, self.database.dbase)
         try:
             entry = entries[int(sel.split('-', 1)[0])]
         except (ValueError, TypeError):
             return
         edit = True
         while edit is True:
-            edit = edit_entry(self.kpo, entry)
-        self.kpo.save()
-        self.kpo = get_entries(self.database)
+            edit = edit_entry(self.database.kpo, entry)
+        self.database.kpo.save()
+        self.database.kpo = get_entries(self.database)
         self.prev_entry = entry
 
     def menu_add_entry(self):
         """Process menu entry - Add entry
 
         """
-        entry = add_entry(self.kpo)
+        entry = add_entry(self.database.kpo)
         if entry:
-            self.kpo.save()
-            self.kpo = get_entries(self.database)
+            self.database.kpo.save()
+            self.database.kpo = get_entries(self.database)
             self.prev_entry = entry
 
     def menu_manage_groups(self):
         """Process menu entry - manage groups
 
         """
-        group = manage_groups(self.kpo)
+        group = manage_groups(self.database.kpo)
         if group:
-            self.kpo.save()
-            self.kpo = get_entries(self.database)
+            self.database.kpo.save()
+            self.database.kpo = get_entries(self.database)
 
     def menu_reload_database(self):
         """Process menu entry - Reload database
 
         """
-        self.kpo = get_entries(self.database)
-        if not self.kpo:
+        self.database.kpo = get_entries(self.database)
+        if not self.database.kpo:
             return
-        self.expiring = get_expiring_entries(self.kpo.entries)
+        self.expiring = get_expiring_entries(self.database.kpo.entries)
         self.dmenu_run()
 
-    def menu_open_another_database(self):
+    def menu_open_another_database(self, **kwargs):
         """Process menu entry - Open different database
 
+        Args: kwargs - possibly 'database', 'keyfile', 'autotype'
+
         """
-        new_db = get_database(self.open_databases.keys())
-        if new_db[0] is None or new_db[0] == self.database[0]:
-            self.dmenu_run()
+        prev_db, prev_open = self.database, deepcopy(self.open_databases)
+        self.database, self.open_databases = get_database(self.open_databases, **kwargs)
+        if self.database is None:
+            self.database, self.open_databases = prev_db, prev_open
             return
-        if new_db in self.open_databases:
-            self.kpo = self.open_databases[new_db]
-            self.database = new_db
-        else:
-            new_kpo = get_entries(new_db)
-            if new_kpo:
-                self.kpo = new_kpo
-                self.database = new_db
-                self.open_databases[new_db] = new_kpo
-        self.expiring = get_expiring_entries(self.kpo.entries)
+        if not self.database.kpo:
+            self.database.kpo = get_entries(self.database)
+            if self.database.kpo is None:
+                self.database, self.open_databases = prev_db, prev_open
+                return
+        self.expiring = get_expiring_entries(self.database.kpo.entries)
         self.dmenu_run()
 
     def menu_kill_daemon(self):
@@ -1677,12 +1768,24 @@ class Server(Process):
         self.start_flag = Event()
         self.kill_flag = Event()
         self.cache_time_expired = Event()
+        self.args_flag = Event()
         self.start_flag.set()
+        self.args = None
+        self._parent_conn, self._child_conn = Pipe(duplex=False)
 
     def run(self):
-        serv = self.server()  # pylint: disable=unused-variable
+        _ = self.server()
         self.kill_flag.wait()
 
+    def _get_pipe(self):
+        return self._child_conn
+
+    def get_args(self):
+        """ Reads aruments sent by the client to the server
+
+        """
+        return self._parent_conn.recv()
+
     def server(self):
         """Set up BaseManager server
 
@@ -1690,16 +1793,18 @@ class Server(Process):
         mgr = BaseManager(address=('127.0.0.1', self.port),
                           authkey=self.authkey)
         mgr.register('set_event', callable=self.start_flag.set)
+        mgr.register('get_pipe', callable=self._get_pipe)
+        mgr.register('read_args_from_pipe', callable=self.args_flag.set)
         mgr.start()
         return mgr
 
 
-def run():
+def run(**kwargs):
     """Main entrypoint. Start the background Manager and Dmenu runner processes.
 
     """
     server = Server()
-    dmenu = DmenuRunner(server)
+    dmenu = DmenuRunner(server, **kwargs)
     dmenu.daemon = True
     server.start()
     dmenu.start()
@@ -1709,8 +1814,6 @@ def run():
 
 
 if __name__ == '__main__':
-    global ARGS
-
     parser = argparse.ArgumentParser(
         description="Dmenu/Rofi frontend for Keepass databases")
 
@@ -1722,13 +1825,34 @@ if __name__ == '__main__':
         help="Override autotype sequence in config.ini",
     )
 
-    ARGS = parser.parse_args()
+    parser.add_argument(
+        "-d",
+        "--database",
+        type=str,
+        required=False,
+        help="File path to a database to open, skipping the database selection menu",
+    )
+
+    parser.add_argument(
+        "-k",
+        "--keyfile",
+        type=str,
+        required=False,
+        help="File path of the keyfile needed to open the database specified by --database/-d",
+    )
+
+    args = vars(parser.parse_args())
+    args = args if any(args.values()) else {}
 
     try:
         MANAGER = client()
+        conn = MANAGER.get_pipe()  # pylint: disable=no-member
+        if args:
+            conn.send(args)
+            MANAGER.read_args_from_pipe()  # pylint: disable=no-member
         MANAGER.set_event()  # pylint: disable=no-member
     except ConnectionRefusedError:
         process_config()
-        run()
+        run(**args)
 
 # vim: set et ts=4 sw=4 :
diff --git a/keepmenu.1 b/keepmenu.1
index c5fb14f..9bbb02a 100644
--- a/keepmenu.1
+++ b/keepmenu.1
@@ -177,12 +177,16 @@ enter password"\fP. Type "ESC" to exit without making changes.
 
 .SH Options
 .PP
-keepmenu [-h] [-a AUTOTYPE]
+keepmenu [-h] [-a AUTOTYPE] [-d DATABASE] [-k KEY_FILE]
 .PP
 --help, -h Output a usage message and exit.
 .PP
 -a AUTOTYPE, --autotype AUTOTYPE Override autotype sequence in
 config.ini
+.PP
+-d DATABASE, --database DATABASE File path to a database to open, skipping the database selection menu
+.PP
+-k KEYFILE, --keyfile KEY_FILE File path of the keyfile needed to open the database specified by --database/-d
 
 .SH TESTS
 \fB1.\fR To run tests: \fIpython tests/tests.py\fP


Test patch:
diff --git a/tests/tests.py b/tests/tests.py
index efbe41c..99afc01 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -72,6 +72,18 @@ def test_client_with_server(self):
         self.assertIsInstance(KM.client(), BaseManager)
         mgr.shutdown()
 
+    def test_pipe_from_client_to_server(self):
+        """Ensure client can send message to server via a pipe
+
+        """
+
+        server = KM.Server()
+        server.start()
+        conn = server._get_pipe()
+        conn.send('test')
+        self.assertEqual('test', server.get_args())
+        server.terminate()
+
 
 class TestFunctions(unittest.TestCase):
     """Test the various Keepass functions
@@ -166,6 +178,30 @@ def test_dmenu_cmd(self):
                "-p", "Password", "-nb", "#222222", "-nf", "#222222"]
         self.assertTrue(KM.dmenu_cmd(20, "Password") == res)
 
+    def test_get_databases(self):
+        """Test reading database information from config
+
+        """
+        db_name = os.path.join(self.tmpdir, "test.kdbx")
+        db_name_2 = os.path.join(self.tmpdir, "test2.kdbx")
+        copyfile("tests/keepmenu-config.ini", KM.CONF_FILE)
+        with open(KM.CONF_FILE, 'w') as conf_file:
+            KM.CONF.set('database', 'database_1', db_name)
+            KM.CONF.set('database', 'password_1', '')
+            KM.CONF.set('database', 'password_cmd_1', 'echo password')
+
+            KM.CONF.set('database', 'database_2', db_name_2)
+            KM.CONF.set('database', 'autotype_default_2', '{TOTP}{ENTER}')
+
+            KM.CONF.write(conf_file)
+
+        databases = KM.get_databases()
+
+        db1 = KM.DataBase(dbase=db_name, pword='password')
+        db2 = KM.DataBase(dbase=db_name_2, atype='{TOTP}{ENTER}')
+        self.assertEqual(db1.__dict__, databases[0].__dict__)
+        self.assertEqual(db2.__dict__, databases[1].__dict__)
+
     def test_open_database(self):
         """Test database opens properly
 
@@ -176,8 +212,8 @@ def test_open_database(self):
         with open(KM.CONF_FILE, 'w') as conf_file:
             KM.CONF.set('database', 'database_1', db_name)
             KM.CONF.write(conf_file)
-        database = KM.get_database()
-        self.assertTrue(database == (db_name, '', 'password', None))
+        database, _ = KM.get_database()
+        self.assertTrue(database == KM.DataBase(dbase=db_name, pword='password'))
         kpo = KM.get_entries(database)
         self.assertIsInstance(kpo, PyKeePass)
         # Switch from `password_1` to `password_cmd_1`
@@ -185,19 +221,20 @@ def test_open_database(self):
             KM.CONF.set('database', 'password_1', '')
             KM.CONF.set('database', 'password_cmd_1', 'echo password')
             KM.CONF.write(conf_file)
-        database = KM.get_database()
-        self.assertTrue(database == (db_name, '', 'password', None))
+        database, _ = KM.get_database()
+        self.assertTrue(database == KM.DataBase(dbase=db_name, pword='password'))
         kpo = KM.get_entries(database)
         self.assertIsInstance(kpo, PyKeePass)
         with open(KM.CONF_FILE, 'w') as conf_file:
             KM.CONF.set('database', 'autotype_default_1', '{TOTP}{ENTER}')
             KM.CONF.write(conf_file)
-        database = KM.get_database()
-        self.assertTrue(database == (db_name, '', 'password', '{TOTP}{ENTER}'))
+        database, _ = KM.get_database()
+        self.assertTrue(database == KM.DataBase(dbase=db_name,
+                                                pword='password',
+                                                atype='{TOTP}{ENTER}'))
         kpo = KM.get_entries(database)
         self.assertIsInstance(kpo, PyKeePass)
 
-
     def test_resolve_references(self):
         """Test keepass references can be resolved to values
 
@@ -208,7 +245,7 @@ def test_resolve_references(self):
         with open(KM.CONF_FILE, 'w') as conf_file:
             KM.CONF.set('database', 'database_1', db_name)
             KM.CONF.write(conf_file)
-        database = KM.get_database()
+        database, _ = KM.get_database()
         kpo = KM.get_entries(database)
         ref_entry = kpo.find_entries_by_title(title='.*REF.*', regex=True)[0]
         base_entry = kpo.find_entries_by_title(title='Test Title 1')[0]
@@ -228,7 +265,7 @@ def test_expiry(self):
         with open(KM.CONF_FILE, 'w') as conf_file:
             KM.CONF.set('database', 'database_1', db_name)
             KM.CONF.write(conf_file)
-        database = KM.get_database()
+        database, _ = KM.get_database()
         kpo = KM.get_entries(database)
         expiring_entries = KM.get_expiring_entries(kpo.entries)
         self.assertEqual(len(expiring_entries), 1)