diff --git a/.gitignore b/.gitignore index 2088e658a..d5be6856d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,9 @@ templates/*.py TODO \#* .#* +cfg.py +cherrypy.config +data/users.sqlite3 +predepend +build/ +*.pid \ No newline at end of file diff --git a/INSTALL b/INSTALL index 9b2705496..e5761a149 100644 --- a/INSTALL +++ b/INSTALL @@ -2,20 +2,27 @@ ## Installing Plinth -Install the python-cheetah package and pandoc: - -apt-get install python-cheetah pandoc +Install the python-cheetah package, pandoc, python-augeas, and +bjsonrpc: +apt-get install python-cheetah pandoc python-augeas python-bjsonrpc Install the python-simplejson apt-get install python-simplejson - + Unzip and untar the source into a directory. Change to the directory -containing the program. Do `make` and then run `./plinth.py` and -point your web browser at `localhost:8000`. The default username is -"admin" and the default password is "secret". +containing the program. Run: + + $ make + +To start Plinth, run: + + $ ./start.sh + +and point your web browser at `localhost:8000`. The default username is "admin" +and the default password is "secret". ## Dependencies @@ -27,6 +34,10 @@ point your web browser at `localhost:8000`. The default username is * *GNU Make* is used to build the templates and such. +* bjsonrpc - used for configuration management layer + +* python-augeas and augeas - used for configuration management + The documentation has some dependencies too. * *Markdown* is used to format and style docs. @@ -42,5 +53,3 @@ The documentation has some dependencies too. Documentation has been collected into a pdf that can be built using `make doc`. It also gets built into smaller files and other formats, including one suitable for install as a man page. - - diff --git a/Makefile b/Makefile index 3afdbcfb0..72d916965 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ #SHELL := /bin/bash MAKE=make +BUILD_DIR = build #TODO: add install target @@ -9,9 +10,22 @@ COMPRESSED_CSS := $(patsubst %.css,%.tiny.css,$(CSS)) PWD=`pwd` ## Catch-all tagets -default: cfg cherrypy.config dirs template css docs dbs +default: predepend cfg cherrypy.config dirs template css docs dbs $(BUILD_DIR)/exmachina #$(BUILD_DIR)/bjsonrpc all: default +build: + mkdir -p $(BUILD_DIR) + +predepend: + sudo sh -c "apt-get install augeas-tools python-bjsonrpc python-augeas python-simplejson pandoc python-cheetah" + touch predepend + +$(BUILD_DIR)/exmachina: build + git clone git://github.com/tomgalloway/exmachina $(BUILD_DIR)/exmachina + +$(BUILD_DIR)/bjsonrpc: build + git clone git://github.com/deavid/bjsonrpc.git $(BUILD_DIR)/bjsonrpc + dbs: data/users.sqlite3 data/users.sqlite3: data/users.sqlite3.distrib @@ -69,3 +83,5 @@ clean: @find . -name "*.bak" -exec rm {} \; @$(MAKE) -s -C doc clean @$(MAKE) -s -C templates clean + rm -rf $(BUILD_DIR) + rm -f predepend diff --git a/NOTES b/NOTES index ff46c2c7d..dbdebd53c 100644 --- a/NOTES +++ b/NOTES @@ -2,6 +2,18 @@ % % February 2012 +# Edits by bnewbold + +## 2012-07-12 "exmachina" configuration management layer + +- this new code is very ugly and in the "just make it work" style +- add exmachina code and test code +- modify plinth.py to listen for shared secret on stdin at start + (if appropriate flag is set) and try to connect to exmachina daemon +- use exmachina to read and set /etc/hostname as a demo +- update plinth init.d script to start exmachina and share keys +- update docs with new deps and run instructions + # Edits by seandiggity ## 2012-02-27 new theme based upon bootstrap diff --git a/README b/README index 6d56c72fd..d2cdd3c1b 100644 --- a/README +++ b/README @@ -50,4 +50,5 @@ interface will overwrite those changes at first opportunity. This interface is not a tool for super admins facing complex scenarios. It is for home users to do a wide variety of basic tasks. - +See comments in exmachina/exmachina.py for more details about the configuration +management process seperation scheme. diff --git a/cfg.sample.py b/cfg.sample.py index 799984a86..8521ca653 100644 --- a/cfg.sample.py +++ b/cfg.sample.py @@ -8,10 +8,12 @@ user_db = os.path.join(data_dir, "users") status_log_file = os.path.join(data_dir, "status.log") access_log_file = os.path.join(data_dir, "access.log") users_dir = os.path.join(data_dir, "users") +pidfile = os.path.join(data_dir, "pidfile.pid") product_name = "Plinth" box_name = "FreedomBox" +host = "127.0.0.1" port = 8000 ## Do not edit below this line ## diff --git a/fabfile.py b/fabfile.py index 92a52e196..4b4d25410 100644 --- a/fabfile.py +++ b/fabfile.py @@ -138,7 +138,7 @@ def apache(): @task def deps(): "Basic plinth dependencies" - sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme') + sudo('apt-get install --no-install-recommends -y python make python-cheetah pandoc python-simplejson python-pyme python-augeas python-bjsonrpc') @task def update(): diff --git a/issues/links.org b/issues/links.org new file mode 100644 index 000000000..c95b563c5 --- /dev/null +++ b/issues/links.org @@ -0,0 +1,42 @@ +# -*- mode: org; mode: auto-fill; fill-column: 80 -*- + +#+TITLE: Make Links Portable +#+OPTIONS: d:t +#+LINK_UP: ./ +#+LINK_HOME: ../ + +* Issue + + Currently, all the links in Plinth point to 127.0.0.1/(something), and that + sucks for serving Plinth on a local network, like most use cases imply. + +* Fixes [0/1] + +** TODO Links work when accessed from remote clients. + + Investigate the following: + + : grep -nHr basehref ../* + + : grep -nHr 127.0 ../* + + #+begin_ascii + fabfile.py:40: if env.host == "localhost" or env.host=="127.0.0.1": + fabfile.py:46: if env.host == "localhost" or env.host=="127.0.0.1": + fabfile.py:102: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (tor_dir, santiago_port) + modules/installed/santiago/santiago.py:48: hidden_service_config = "HiddenServiceDir %s\nHiddenServicePort 80 127.0.0.1:%d" % (self.tor_dir, santiago_port) + plinth.py:119: server.socket_host = '127.0.0.1' + #+end_ascii + + Also, why is base_href blank in [[file:~/programs/freedombox/plinth/cfg.sample.py][cfg.sample.py]]? + +* Discussion + +* Metadata + :PROPERTIES: + :Status: Incomplete + :Priority: 0 + :Owner: Nick Daly + :Description: + :Tags: + :END: diff --git a/model.py b/model.py index d3807d0b1..e0b3cd20d 100644 --- a/model.py +++ b/model.py @@ -1,14 +1,15 @@ class User(dict): - """ Every user must have keys for a username, name, password (this + """ Every user must have keys for a username, name, passphrase (this is a md5 hash of the password), groups, and an email address. They can be blank or None, but the keys must exist. """ def __init__(self, dict=None): - for key in ['username', 'name', 'password', 'email']: + for key in ['username', 'name', 'passphrase', 'email']: self[key] = '' for key in ['groups']: self[key] = [] - for key in dict: - self[key] = dict[key] + if dict: + for key in dict: + self[key] = dict[key] def __getattr__(self, attr): return None diff --git a/modules/installed/lib/user_store.py b/modules/installed/lib/user_store.py index a4042c102..236b73a8f 100644 --- a/modules/installed/lib/user_store.py +++ b/modules/installed/lib/user_store.py @@ -12,10 +12,42 @@ class UserStore(UserStoreModule, sqlite_db): self.db_file = cfg.user_db sqlite_db.__init__(self, self.db_file, autocommit=True) self.__enter__() + def close(self): - self.__exit__() - def expert(self): - return False + self.__exit__(None,None,None) + + def expert(self, username=None): + groups = self.attr(username,"groups") + if not groups: + return False + return 'expert' in groups + + def attr(self, username=None, field=None): + return self.get(username)[field] + + def get(self,username=None): + return User(sqlite_db.get(self,username)) + + def exists(self, username=None): + try: + user = self.get(username) + if not user: + return False + elif user["username"]=='': + return False + return True + except TypeError: + return False + + def remove(self,username=None): + self.__delitem__(username) + self.commit() + + def get_all(self): + return self.items() + + def set(self,username=None,user=None): + sqlite_db.__setitem__(self,username, user) class UserStoreOld(): #class UserStore(UserStoreModule): diff --git a/modules/installed/system/config.py b/modules/installed/system/config.py index c8b5190a8..b671fcc12 100644 --- a/modules/installed/system/config.py +++ b/modules/installed/system/config.py @@ -1,4 +1,4 @@ -import os, shutil, subprocess +import os, subprocess from socket import gethostname import cherrypy import simplejson as json @@ -10,6 +10,7 @@ import cfg from forms import Form from model import User from util import * +import platform class Config(PagePlugin): def __init__(self, *args, **kwargs): @@ -41,20 +42,21 @@ def valid_hostname(name): def set_hostname(hostname): "Sets machine hostname to hostname" - cfg.log.info("Writing '%s' to /etc/hostname" % hostname) - unslurp("/etc/hostname", hostname+"\n") + cfg.log.info("Writing '%s' to /etc/hostname with exmachina" % hostname) + try: - retcode = subprocess.call("/etc/init.d/hostname.sh start", shell=True) - if retcode < 0: - cfg.log.error("Hostname restart terminated by signal: return code is %s" % retcode) + cfg.exmachina.augeas.set("/files/etc/hostname/*", hostname) + cfg.exmachina.augeas.save() + # don't persist/cache change unless it was saved successfuly + sys_store = filedict_con(cfg.store_file, 'sys') + sys_store['hostname'] = hostname + if platform.linux_distribution()[0]=="Ubuntu" : + cfg.exmachina.service.start("hostname") else: - cfg.log.debug("Hostname restart returned %s" % retcode) + cfg.exmachina.initd.restart("hostname.sh") # is hostname.sh debian-only? except OSError, e: raise cherrypy.HTTPError(500, "Hostname restart failed: %s" % e) - sys_store = filedict_con(cfg.store_file, 'sys') - sys_store['hostname'] = hostname - class general(FormPlugin, PagePlugin): url = ["/sys/config"] order = 30 @@ -72,8 +74,11 @@ class general(FormPlugin, PagePlugin): def main(self, message='', **kwargs): sys_store = filedict_con(cfg.store_file, 'sys') + hostname = cfg.exmachina.augeas.get("/files/etc/hostname/*") + # this layer of persisting configuration in sys_store could/should be + # removed -BLN defaults = {'time_zone': "slurp('/etc/timezone').rstrip()", - 'hostname': "gethostname()", + 'hostname': "hostname", } for k,c in defaults.items(): if not k in kwargs: @@ -81,6 +86,8 @@ class general(FormPlugin, PagePlugin): kwargs[k] = sys_store[k] except KeyError: exec("if not '%(k)s' in kwargs: sys_store['%(k)s'] = kwargs['%(k)s'] = %(c)s" % {'k':k, 'c':c}) + # over-ride the sys_store cached value + kwargs['hostname'] = hostname ## Get the list of supported timezones and the index in that list of the current one module_file = __file__ @@ -120,7 +127,8 @@ class general(FormPlugin, PagePlugin): old_val = sys_store['hostname'] try: set_hostname(hostname) - except: + except Exception, e: + cfg.log.error(e) cfg.log.info("Trying to restore old hostname value.") set_hostname(old_val) raise @@ -128,8 +136,8 @@ class general(FormPlugin, PagePlugin): message += msg if time_zone != sys_store['time_zone']: src = os.path.join("/usr/share/zoneinfo", time_zone) - cfg.log.info("Copying %s to /etc/localtime" % src) - shutil.copy(src, "/etc/localtime") + cfg.log.info("Setting timezone to %s" % time_zone) + cfg.exmachina.misc.set_timezone(time_zone) sys_store['time_zone'] = time_zone return message or "Settings updated." diff --git a/modules/installed/system/users.py b/modules/installed/system/users.py index c4ac97771..4277f1050 100644 --- a/modules/installed/system/users.py +++ b/modules/installed/system/users.py @@ -5,22 +5,23 @@ from plugin_mount import PagePlugin, FormPlugin import cfg from forms import Form from util import * +from model import User class users(PagePlugin): order = 20 # order of running init in PagePlugins def __init__(self, *args, **kwargs): PagePlugin.__init__(self, *args, **kwargs) self.register_page("sys.users") + self.register_page("sys.users.add") + self.register_page("sys.users.edit") @cherrypy.expose @require() def index(self): - parts = self.forms('/sys/config') - parts['title']=_("Manage Users and Groups") - return self.fill_template(**parts) + return self.fill_template(title="Manage Users and Groups", sidebar_right="""Add User
Edit Users""") class add(FormPlugin, PagePlugin): - url = ["/sys/users"] + url = ["/sys/users/add"] order = 30 sidebar_left = '' @@ -40,33 +41,35 @@ class add(FormPlugin, PagePlugin): form.text_input(_("Username"), name="username", value=username) form.text_input(_("Full name"), name="name", value=name) form.text_input(_("Email"), name="email", value=email) - form.text_input(_("Password"), name="password") + form.text_input(_("Password"), name="password", type="password") form.text_input(name="md5_password", type="hidden") form.submit(label=_("Create User"), name="create") return form.render() def process_form(self, username=None, name=None, email=None, md5_password=None, **kwargs): - msg = '' + msg = Message() - if not username: msg = add_message(msg, _("Must specify a username!")) - if not md5_password: msg = add_message(msg, _("Must specify a password!")) + if not username: msg.add = _("Must specify a username!") + if not md5_password: msg.add = _("Must specify a password!") - if username in cfg.users: - msg = add_message(msg, _("User already exists!")) + if username in cfg.users.get_all(): + msg.add = _("User already exists!") else: try: - cfg.users[username]= User(dict={'username':username, 'name':name, 'email':email, 'password':md5_password}) + di = {'username':username, 'name':name, 'email':email, 'passphrase':md5_password} + new_user = User(di) + cfg.users.set(username,new_user) except: - msg = add_message(msg, _("Error storing user!")) + msg.add = _("Error storing user!") if not msg: - msg = add_message(msg, "%s saved." % username) - - main = self.make_form(username, name, email, message=msg) - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) + msg.add = _("%s saved." % username) + cfg.log(msg.text) + main = self.main(username, name, email, msg=msg.text) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) class edit(FormPlugin, PagePlugin): - url = ["/sys/users"] + url = ["/sys/users/edit"] order = 35 sidebar_left = '' @@ -77,14 +80,15 @@ class edit(FormPlugin, PagePlugin): system.

Deleting users is permanent!

""" % (cfg.product_name, cfg.box_name)) def main(self, msg=''): - users = cfg.users.keys() + users = cfg.users.get_all() add_form = Form(title=_("Edit or Delete User"), action="/sys/users/edit", message=msg) add_form.html('Delete
') - for uname in sorted(users.keys()): + for uname in users: + user = User(uname[1]) add_form.html('  %s     ' % - add_form.get_checkbox(name=uname) + + add_form.get_checkbox(name=user['username']) + '%s (%s)
' % - (uname, users[uname]['name'], uname)) + (user['username'], user['name'], user['username'])) add_form.submit(label=_("Delete User"), name="delete") return add_form.render() @@ -95,12 +99,12 @@ class edit(FormPlugin, PagePlugin): cfg.log.info("%s asked to delete %s" % (cherrypy.session.get(cfg.session_key), usernames)) if usernames: for username in usernames: - if username in cfg.users: + if cfg.users.exists(username): try: - del cfg.users[username] + cfg.users.remove(username) msg.add(_("Deleted user %s." % username)) except IOError, e: - if 'username' in cfg.users: + if cfg.users.exists(username): m = _("Error on deletion, user %s not fully deleted: %s" % (username, e)) cfg.log.error(m) msg.add(m) @@ -113,8 +117,8 @@ class edit(FormPlugin, PagePlugin): msg.add(_("User %s does not exist." % username)) else: msg.add = _("Must specify at least one valid, existing user.") - main = self.make_form(msg=msg.text) - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) + main = self.main(msg=msg.text) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=self.sidebar_right) sidebar_right = '' u = cfg.users[kwargs['username']] @@ -125,4 +129,4 @@ class edit(FormPlugin, PagePlugin): main = _("""Edit User '%s'""" % u['username']) sidebar_right = '' - return self.fill_template(title="", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right) + return self.fill_template(title="Manage Users and Groups", main=main, sidebar_left=self.sidebar_left, sidebar_right=sidebar_right) diff --git a/plinth.py b/plinth.py index 2eff29f40..3b8a1001a 100755 --- a/plinth.py +++ b/plinth.py @@ -17,6 +17,9 @@ from util import * from logger import Logger #from modules.auth import AuthController, require, member_of, name_is +from exmachina import ExMachinaClient +import socket + __version__ = "0.2.14" __author__ = "James Vasile" __copyright__ = "Copyright 2011, James Vasile" @@ -51,7 +54,7 @@ class Root(plugin_mount.PagePlugin): raise cherrypy.InternalRedirect('/router') else: raise cherrypy.InternalRedirect('/help/about') - + def load_modules(): """Import all the symlinked .py files in the modules directory and all the .py files in directories linked in the modules directory @@ -71,9 +74,20 @@ def parse_arguments(): parser = argparse.ArgumentParser(description='Plinth web interface for the FreedomBox.') parser.add_argument('--pidfile', default="", help='specify a file in which the server may write its pid') + parser.add_argument('--listen-exmachina-key', default=False, action='store_true', + help='listen for JSON-RPC shared secret key on stdin at startup') args=parser.parse_args() if args.pidfile: cfg.pidfile = args.pidfile + else: + if not cfg.pidfile: + cfg.pidfile = "plinth.pid" + if args.listen_exmachina_key: + # this is where we optionally try to read in a shared secret key to + # authenticate connections to exmachina + cfg.exmachina_secret_key = sys.stdin.readline().strip() + else: + cfg.exmachina_secret_key = None def setup(): parse_arguments() @@ -85,6 +99,13 @@ def setup(): except AttributeError: pass + try: + cfg.exmachina = ExMachinaClient( + secret_key=cfg.exmachina_secret_key or None) + except socket.error: + cfg.exmachina = None + print "couldn't connect to exmachina daemon, but continuing anyways..." + os.chdir(cfg.file_root) cherrypy.config.update({'error_page.404': error_page_404}) cherrypy.config.update({'error_page.500': error_page_500}) @@ -103,7 +124,7 @@ def setup(): server.subscribe() # Configure default server - cherrypy.config.update({'server.socket_host': '127.0.0.1', + cherrypy.config.update({'server.socket_host': cfg.host, 'server.socket_port': cfg.port, 'server.thread_pool':10, 'tools.staticdir.root': cfg.file_root, @@ -112,7 +133,7 @@ def setup(): 'tools.sessions.storage_type':"file", 'tools.sessions.timeout':90, 'tools.sessions.storage_path':"%s/data/cherrypy_sessions" % cfg.file_root, - + }) config = {'/': {'tools.staticdir.root': '%s/static' % cfg.file_root, @@ -124,11 +145,11 @@ def setup(): } cherrypy.tree.mount(cfg.html_root, '/', config=config) cherrypy.engine.signal_handler.subscribe() - + def main(): setup() - print "localhost %d" % cfg.port + print "%s %d" % (cfg.host, cfg.port) cherrypy.engine.start() cherrypy.engine.block() diff --git a/share/init.d/plinth b/share/init.d/plinth index e364fc123..90bfcec98 100755 --- a/share/init.d/plinth +++ b/share/init.d/plinth @@ -12,23 +12,53 @@ # This file is /etc/init.d/plinth DAEMON=/usr/local/bin/plinth.py +EXMACHINA_DAEMON=/usr/local/bin/exmachina.py PID_FILE=/var/run/plinth.pid - +EXMACHINA_PID_FILE=/var/run/exmachina.pid + +PLINTH_USER=www-data +PLINTH_GROUP=www-data + +test -x $DAEMON || exit 0 +test -x $EXMACHINA_DAEMON || exit 0 + +set -e + +. /lib/lsb/init-functions + start_plinth (){ if [ -f $PID_FILE ]; then - echo Already running with a pid of `cat $PID_FILE`. + echo Already running with a pid of `cat $PID_FILE`. else - $DAEMON --pidfile=$PID_FILE + if [ -f $EXMACHINA_PID_FILE ]; then + echo exmachina was already running with a pid of `cat $EXMACHINA_PID_FILE`. + kill -15 `cat $EXMACHINA_PID_FILE` + rm -rf $EXMACHINA_PID_FILE + fi + SHAREDKEY=`$EXMACHINA_DAEMON --random-key` + touch $PID_FILE + chown $PLINTH_USER:$PLINTH_GROUP $PID_FILE + echo $SHAREDKEY | $EXMACHINA_DAEMON --pidfile=$EXMACHINA_PID_FILE --group=$PLINTH_GROUP || rm $PID_FILE + sleep 0.5 + echo $SHAREDKEY | sudo -u $PLINTH_USER -g $PLINTH_GROUP $DAEMON --pidfile=$PID_FILE fi } stop_plinth () { if [ -f $PID_FILE ]; then - kill -15 `cat $PID_FILE` + kill -15 `cat $PID_FILE` || true rm -rf $PID_FILE + echo "killed plinth" else echo "No pid file at $PID_FILE suggests plinth is not running." fi + if [ -f $EXMACHINA_PID_FILE ]; then + kill -15 `cat $EXMACHINA_PID_FILE` || true + rm -rf $EXMACHINA_PID_FILE + echo "killed exmachina" + else + echo "No pid file at $EXMACHINA_PID_FILE suggests exmachina is not running." + fi } test -x $DAEMON || exit 0 @@ -45,5 +75,12 @@ case "$1" in $0 stop $0 start ;; + status) + status_of_proc -p $PID_FILE "$DAEMON" plinth && exit 0 || exit $? + ;; + *) + echo "Usage: $NAME {start|stop|restart|status}" >&2 + exit 1 + ;; esac diff --git a/start.sh b/start.sh new file mode 100755 index 000000000..4be20c1b6 --- /dev/null +++ b/start.sh @@ -0,0 +1,10 @@ +#! /bin/sh + +PYTHONPATH=build/exmachina:$PYTHONPATH + +export PYTHONPATH + +sudo killall exmachina.py +sudo build/exmachina/exmachina.py -v & +python plinth.py +sudo killall exmachina.py diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..4b6f701d4 --- /dev/null +++ b/test.sh @@ -0,0 +1,9 @@ +#! /bin/sh + +PYTHONPATH=build/exmachina:$PYTHONPATH +PYTHONPATH=modules/installed/lib:$PYTHONPATH +PYTHONPATH=vendor:$PYTHONPATH + +export PYTHONPATH + +python tests/test_user_store.py diff --git a/tests/test_user_store.py b/tests/test_user_store.py new file mode 100644 index 000000000..e6a3e8433 --- /dev/null +++ b/tests/test_user_store.py @@ -0,0 +1,86 @@ +#! /usr/bin/env python +# -*- mode: python; mode: auto-fill; fill-column: 80 -*- + +import user_store +from logger import Logger +import cfg +import unittest +import cherrypy +import plugin_mount +import os +from model import User +cfg.log = Logger() + +cherrypy.log.access_file = None + +class UserStore(unittest.TestCase): + """Test each function of user_store to confirm they work as expected""" + + def setUp(self): + cfg.user_db = os.path.join(cfg.file_root, "tests/testdata/users"); + self.userstore = plugin_mount.UserStoreModule.get_plugins()[0] + + def tearDown(self): + for user in self.userstore.get_all(): + self.userstore.remove(user[0]) + self.userstore.close() + + def test_user_does_not_exist(self): + self.assertEqual(self.userstore.exists("notausername"),False) + + def test_user_does_exist(self): + self.add_user("isausername", False) + self.assertEqual(self.userstore.exists("isausername"),True) + + def test_add_user(self): + self.assertEqual(len(self.userstore.items()),0) + self.add_user("test_user", False) + self.assertEqual(len(self.userstore.items()),1) + + def test_user_is_in_expert_group(self): + self.add_user("test_user", True) + self.assertEqual(self.userstore.expert("test_user"),True) + + def test_user_is_not_in_expert_group(self): + self.add_user("test_user", False) + self.assertEqual(self.userstore.expert("test_user"),False) + + def test_user_removal(self): + self.assertEqual(len(self.userstore.items()),0) + self.add_user("test_user", False) + self.assertEqual(len(self.userstore.items()),1) + self.userstore.remove("test_user") + self.assertEqual(len(self.userstore.items()),0) + + def test_get_user_email_attribute(self): + self.add_user("test_user", False,"test@home") + self.assertEqual(self.userstore.attr("test_user","email"),"test@home") + + def test_get_user(self): + test_user = self.add_user("test_user", False) + self.assertEqual(self.userstore.get("test_user"),test_user) + + def test_get_all_users(self): + self.add_user("test_user1", False) + self.add_user("test_user2", False) + self.assertEqual(len(self.userstore.get_all()),2) + + def add_user(self, test_username, add_to_expert_group, email=''): + test_user = self.create_user(test_username, email) + if add_to_expert_group: + test_user = self.add_user_to_expert_group(test_user) + self.userstore.set(test_username,test_user) + return test_user + + def create_user(self, username, email=''): + test_user = User() + test_user["username"] = username + test_user["email"] = email + return test_user + + def add_user_to_expert_group(self, user): + user["groups"] = ["expert"] + return user + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/testdata/users.sqlite3 b/tests/testdata/users.sqlite3 new file mode 100644 index 000000000..03782a7d7 Binary files /dev/null and b/tests/testdata/users.sqlite3 differ