LOC-24 Added tests for kick/ban functionality
LOC-24 Added tests for kick/ban functionality

Test 11 invites a user, has them join and then kicks them (before verifying in the database)
Test 12 has that user rejoin and then bans them (again verifying against the database)

file:a/.gitignore -> file:b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *~
 *.pyc
+localchat-testing.db
 
 

--- a/client/LocalChatClient.py
+++ b/client/LocalChatClient.py
@@ -161,7 +161,7 @@
 
         resp = self.sendRequest(request)        
         
-        if resp['status'] == "ok":
+        if "status" in resp and resp['status'] == "ok":
             return True
         
         return False
@@ -197,7 +197,7 @@
 
         resp = self.sendRequest(request)        
         
-        if resp['status'] == "ok":
+        if "status" in resp and resp['status'] == "ok":
             return True
         
         return False

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -24,6 +24,8 @@
 import random
 import string
 import gnupg
+import sys
+
 
 app = Flask(__name__)
 
@@ -43,7 +45,7 @@
         return make_response("",400)
     
     a = msghandler.processSubmission(reqjson)
-    
+
     # Check the status
     if a in [400,403,500]:
         response = make_response("",a)
@@ -55,7 +57,7 @@
 class MsgHandler(object):
 
 
-    def __init__(self,cronpass,bindpoint,purgeinterval):
+    def __init__(self,cronpass,bindpoint,purgeinterval,closethresh,testingmode):
         self.conn = False
         self.cursor = False
         # Generate a key for encryption of SYSTEM messages (LOC-13)
@@ -64,23 +66,37 @@
         self.cronpass = cronpass
         self.bindpoint = bindpoint
         self.purgeInterval = purgeinterval
+        self.roomCloseThresh = closethresh
+        self.testingMode = testingmode
+        
+        if self.testingMode:
+            print "WARNING - Messages will be written to disk"
 
 
 
     def createDB(self):
         ''' Create the in-memory database ready for use 
         '''
-        self.conn = sqlite3.connect(':memory:')
+        
+        dbpath=':memory:'
+        if self.testingMode:
+            # LOC-15 - allow DB to written to disk in test mode
+            dbpath = "%s/localchat-testing.db" % (os.getcwd(),)
+        
+        self.conn = sqlite3.connect(dbpath)
         self.cursor = self.conn.cursor()
         
-        sql = """ CREATE TABLE rooms (
+        sql = """
+        
+        DROP TABLE IF EXISTS rooms;
+        CREATE TABLE rooms (
             id INTEGER PRIMARY KEY,
             name TEXT NOT NULL UNIQUE,
             owner TEXT NOT NULL,
             lastactivity INTEGER DEFAULT 0
         );
         
-        
+        DROP TABLE IF EXISTS messages;
         CREATE TABLE messages (
             id INTEGER PRIMARY KEY AUTOINCREMENT,
             ts INTEGER NOT NULL,
@@ -90,7 +106,7 @@
             msg TEXT NOT NULL
         );
         
-        
+        DROP TABLE IF EXISTS users;
         CREATE TABLE users (
             username TEXT NOT NULL,
             room INTEGER NOT NULL,
@@ -99,14 +115,14 @@
             PRIMARY KEY (username,room)
         );
         
-        
+        DROP TABLE IF EXISTS sessions;
         CREATE TABLE sessions (
             username TEXT NOT NULL,
             sesskey TEXT NOT NULL,
             PRIMARY KEY(sesskey)
         );
         
-        
+        DROP TABLE IF EXISTS failuremsgs;
         CREATE TABLE failuremsgs (
             username TEXT NOT NULL,
             room INTEGER NOT NULL,
@@ -423,6 +439,12 @@
         # Push a message to the room to note that the user joined
         msgid = self.pushSystemMsg("User %s joined the room" % (reqjson['payload']['user']),room)
 
+
+        # If we're in testing mode, push a warning so the new user can see it
+        if self.testingMode:
+            msgid = self.pushSystemMsg("Server is in testing mode. Messages are being written to disk",room,'syswarn')
+
+
         # Check the latest message ID for that room
         self.cursor.execute("SELECT id from messages WHERE room=? and id != ? ORDER BY id DESC",(room,msgid))
         r = self.cursor.fetchone()
@@ -671,6 +693,8 @@
         # Tidy messages older than 10 minutes
         self.tidyMsgs(time.time() - self.purgeInterval);
         
+        # Auto-close any rooms beyond the threshold
+        self.autoCloseRooms()
         
         return {'status':'ok'}
         
@@ -691,6 +715,28 @@
 
         # Tidy away any failure messages
         self.cursor.execute("DELETE FROM failuremsgs  where expires < ?",(time.time(),))
+
+
+
+
+    def autoCloseRooms(self):
+        ''' Automatically close any rooms that have been sat idle for too long
+        '''
+        
+        self.cursor.execute("SELECT id,name from rooms where lastactivity < ? and lastactivity > 0",(time.time() - self.roomCloseThresh,))
+        rooms = self.cursor.fetchall()
+        
+        # Messages probably have been auto-purged, but make sure
+        for r in rooms:
+            self.cursor.execute("DELETE FROM messages where room=?",(r[0],))
+            self.cursor.execute("DELETE FROM failuremsgs where room=?",(r[0],))
+            self.cursor.execute("DELETE FROM users where room=?",(r[0],))
+            self.cursor.execute("DELETE FROM sessions where sesskey like ?", (r[1] + '-%',))
+            self.cursor.execute("DELETE FROM rooms where id=?",(r[0],))
+            self.conn.commit()
+            
+            
+        
 
 
 
@@ -812,17 +858,23 @@
 
 if __name__ == '__main__':
     
+    # LOC-15
+    testingmode = False
+    if '--testing-mode-enable' in sys.argv:
+        testingmode = True
+    
     
     # These will be handled properly later
     passw = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits).encode('utf-8') for _ in range(64))
     bindpoint = "https://127.0.0.1:8090" 
     purgeinterval = 600 # Wipe messages older than 10 mins
+    closethresh = 3600 * 6 # Auto-close rooms after 6 hours of inactivity
 
     # Create a global instance of the wrapper so that state can be retained
     #
     # We pass in the password we generated for the scheduler thread to use
     # as well as the URL it should POST to
-    msghandler = MsgHandler(passw,bindpoint,purgeinterval)
+    msghandler = MsgHandler(passw,bindpoint,purgeinterval,closethresh,testingmode)
 
 
     # Bind to PORT if defined, otherwise default to 8090.

--- /dev/null
+++ b/tests/run_tests.py
@@ -1,1 +1,812 @@
-
+#!/usr/bin/env python
+#
+# apt-get install:
+#   python-psutil
+#
+import subprocess
+import psutil
+import os
+import sys
+import time
+import sqlite3
+import traceback
+import json
+
+
+try:
+    from subprocess import DEVNULL # py3k
+except ImportError:
+    import os
+    DEVNULL = open(os.devnull, 'wb')
+
+
+DB_FILE = "%s/localchat-testing.db" % (os.getcwd(),)
+PARENT_DIR = "%s/.." % (os.path.dirname(os.path.abspath(__file__)),)
+sys.path.append('%s/client/' % (PARENT_DIR,))
+
+import LocalChatClient
+STORAGE = {}
+
+
+def restartServer(proc1):
+    ''' Start the server component
+    
+    If already running (i.e. proc1 != False) kill the existing instance
+    '''
+    
+    # If we've already got a process, kill it
+    if proc1:
+        kill(proc1.pid)
+    
+    try:
+        serverloc = "%s/server/LocalChat.py" % (PARENT_DIR,)
+        proc1 = subprocess.Popen([serverloc,'--testing-mode-enable'],stderr=subprocess.STDOUT,stdout=DEVNULL)
+    except Exception as e:
+        print "Failed to start server"
+        print e
+        return False
+    
+    # Give it time to start up
+    time.sleep(5)
+    return proc1
+
+
+def getClientInstance():
+    ''' Get an instance of the client class
+    '''
+    return LocalChatClient.msgHandler()
+
+
+def exit(proc1,code=0):
+    ''' Tidy up and exit
+    '''
+    
+    if proc1:
+        kill(proc1.pid)
+        
+    sys.exit(code)
+
+
+
+def kill(proc_pid):
+    ''' Kill the process and it's children
+    
+    From https://stackoverflow.com/a/25134985
+    
+    We use this because proc1.kill() doesn't seem to kill of the child thread of the server, even if shell=True
+    '''
+    process = psutil.Process(proc_pid)
+    for proc in process.children(recursive=True):
+        proc.kill()
+    process.kill()
+
+
+
+def make_table(columns, data):
+    """Create an ASCII table and return it as a string.
+
+    Pass a list of strings to use as columns in the table and a list of
+    dicts. The strings in 'columns' will be used as the keys to the dicts in
+    'data.'
+
+
+    https://snippets.bentasker.co.uk/page-1705192300-Make-ASCII-Table-Python.html
+    """
+    # Calculate how wide each cell needs to be
+    cell_widths = {}
+    for c in columns:
+        lens = []
+        values = [lens.append(len(str(d.get(c, "")))) for d in data]
+        lens.append(len(c))
+        lens.sort()
+        cell_widths[c] = max(lens)
+
+    # Used for formatting rows of data
+    row_template = "|" + " {} |" * len(columns)
+
+    # CONSTRUCT THE TABLE
+
+    # The top row with the column titles
+    justified_column_heads = [c.ljust(cell_widths[c]) for c in columns]
+    header = row_template.format(*justified_column_heads)
+    # The second row contains separators
+    sep = "|" + "-" * (len(header) - 2) + "|"
+    end = "-" * len(header)
+    # Rows of data
+    rows = []
+
+    for d in data:
+        fields = [str(d.get(c, "")).ljust(cell_widths[c]) for c in columns]
+        row = row_template.format(*fields)
+        rows.append(row)
+    rows.append(end)
+    return "\n".join([header, sep] + rows)
+
+
+
+def opendb():
+    ''' We'll do this and then close after every query to make sure we don't
+    inadvertantly lock the DB and force tests to fail.
+    
+    '''
+    CONN = sqlite3.connect(DB_FILE)
+    CURSOR = CONN.cursor()
+
+    return [CONN,CURSOR]
+    
+
+def run_tests():
+    
+    # Get an instance of the client
+    msg = getClientInstance();
+    
+    test_results = []
+    tests = ['test_one','test_two','test_three','test_four',
+             'test_five','test_six','test_seven','test_eight',
+             'test_nine','test_ten','test_eleven','test_twelve']
+    x = 1
+    for test in tests:
+        print "Running %s " % (test,)
+        stat,isFatal = globals()[test](msg)
+        stat['No'] = x
+        test_results.append(stat)
+        if isFatal and stat['Result'] == 'FAIL':
+            break
+
+        x = x + 1
+
+    return test_results
+
+
+
+
+def test_one(msg):
+    ''' Create a room and verify that it gets created
+    '''
+    
+    result = {'Test' : 'Create a Room','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    # Test 1 - create a room and check it's recorded in the DB
+    n = msg.createRoom('TestRoom1','testadmin')
+    
+    if not n:
+        result['Notes'] = 'Empty Response'
+        return [result,isFatal]
+    
+    # The client should have given us two passwords
+    if len(n) < 2:
+        result['Notes'] = 'Response too small'
+        return [result,isFatal]
+    
+    # Seperate out the return value
+    roompass = n[0]
+    userpass = n[1] # user specific password    
+
+    STORAGE['room'] = {"name":"TestRoom1",
+                       "RoomPass":roompass,
+                       "UserPass":userpass,
+                       'User':'testadmin'
+                       }
+    
+    
+    CONN,CURSOR = opendb()
+    
+    # Check the DB to ensure the room was actually created
+    CURSOR.execute("SELECT * from rooms where name=?",('TestRoom1',))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'Room not in DB'
+        return [result,isFatal]
+    
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+
+
+def test_two(msg):
+    ''' Try joining the previously created room with invalid credentials
+    '''
+    
+    result = {'Test' : 'Join the room with invalid creds','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+    n = msg.joinRoom(STORAGE['room']['User'],STORAGE['room']['name'],
+                     "%s:%s" % (STORAGE['room']['RoomPass'],'BlatantlyWrong'))
+    
+    if n:
+        result['Notes'] = 'Allowed to join with invalid pass'
+        return [result,isFatal]
+    
+    
+    # Now try with an invalid username
+    result = {'Test' : 'Join the room with invalid creds','Result' : 'FAIL', 'Notes': '' }
+    n = msg.joinRoom('AlsoWrong',STORAGE['room']['name'],"%s:%s" % (STORAGE['room']['RoomPass'],'BlatantlyWrong'))
+    
+    if n:
+        result['Notes'] = 'Invalid user Allowed to join'
+        return [result,isFatal]
+        
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+
+
+def test_three(msg):
+    ''' Join the previously created room
+    '''
+    
+    result = {'Test' : 'Join the room','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    n = msg.joinRoom(STORAGE['room']['User'],STORAGE['room']['name'],
+                     "%s:%s" % (STORAGE['room']['RoomPass'],STORAGE['room']['UserPass'])
+                     )
+    
+    if not n:
+        result['Notes'] = 'Could not join'
+        return [result,isFatal]
+    
+    CONN,CURSOR = opendb()
+    
+    # Check the DB to ensure we're now active
+    CURSOR.execute("SELECT * from users where username=? and active=1",(STORAGE['room']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'Not Active in DB'
+        return [result,isFatal]
+    
+    # Check we've got a session token
+    if not msg.sesskey:
+        result['Notes'] = 'No Session Key'
+        return [result,isFatal]
+        
+    # Check the client has recorded what it needs to
+    if not msg.room:
+        result['Notes'] = 'Client forgot room'
+        return [result,isFatal]
+
+    if not msg.user:
+        result['Notes'] = 'Client forgot user'
+        return [result,isFatal]
+
+    if not msg.roompass:
+        result['Notes'] = 'Client forgot roompass'
+        return [result,isFatal]
+
+    if not msg.syskey:
+        result['Notes'] = 'No SYSTEM key'
+        return [result,isFatal]
+    
+    
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+def test_four(msg):
+    ''' When we joined, SYSTEM will have pushed a message. Ensure it's encrypted
+    '''
+    
+    result = {'Test' : 'SYSTEM uses E2E','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+
+
+    CONN,CURSOR = opendb()
+
+    CURSOR.execute("SELECT msg FROM messages where user='SYSTEM' ORDER BY ts DESC")
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'No System Message'
+        return [result,isFatal]
+    
+    
+    try:
+        json.loads(r[0])
+        result['Notes'] = 'System Message not E2E encrypted'
+        return [result,isFatal]       
+    except:
+        # This is a good thing in this case!
+        
+        # Now try and decrypt the message
+        m = msg.decrypt(r[0],'SYSTEM')
+        if not m:
+            result['Notes'] = 'Could not decrypt'
+            return [result,isFatal]     
+    
+        # Now check we got valid json
+        try:
+            j = json.loads(m)
+            # Finally
+            if "text" not in j or "verb" not in j:
+                result['Notes'] = 'Not valid msg payload'
+                return [result,isFatal]              
+            
+            # Otherwise, we're good
+            result['Result'] = 'Pass'
+            return [result,isFatal]
+            
+        except:
+            result['Notes'] = 'Not valid JSON'
+            return [result,isFatal]              
+    
+
+def test_five(msg):
+    ''' Send a message and ensure it's encrypted in the DB
+    '''
+    
+    result = {'Test' : 'Ensure payloads are encrypted','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+    
+    msg.sendMsg('Hello World')
+    
+    CONN,CURSOR = opendb()
+    
+    CURSOR.execute("SELECT msg FROM messages where user=? ORDER BY ts DESC",(STORAGE['room']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'Message not recorded'
+        return [result,isFatal]
+    
+    
+    try:
+        json.loads(r[0])
+        result['Notes'] = 'Message not E2E encrypted'
+        return [result,isFatal]       
+    except:
+        # This is a good thing in this case!
+        
+        # Now try and decrypt the message
+        m = msg.decrypt(r[0],STORAGE['room']['User'])
+        if not m:
+            result['Notes'] = 'Could not decrypt'
+            return [result,isFatal]     
+    
+        # Now check we got valid json
+        try:
+            j = json.loads(m)
+            # Finally
+            if "text" not in j or "verb" not in j:
+                result['Notes'] = 'Not valid msg payload'
+                return [result,isFatal]              
+            
+            # Otherwise, we're good
+            result['Result'] = 'Pass'
+            return [result,isFatal]
+            
+        except:
+            result['Notes'] = 'Not valid JSON'
+            return [result,isFatal]              
+    
+
+
+def test_six(msg):
+    ''' Invite a user
+    '''
+    result = {'Test' : 'Invite a user','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    
+    n = msg.inviteUser('testuser')
+    if not n:
+        result['Notes'] = 'Could not invite testuser'
+        return [result,isFatal]
+    
+    if len(n) < 4:
+        result['Notes'] = 'Client returned too short response'
+        return [result,isFatal]
+        
+    # Otherwise, we've got details for a new user to be able to join
+    #
+    # Store them for a later test
+    
+    STORAGE['testuser'] = {
+        'room':n[0],
+        'pass':"%s:%s" % (n[1],n[2]),
+        'User':n[3]
+        }
+    
+    result['Result'] = "Pass"
+    return [result,isFatal]
+
+
+
+def test_seven(msg):
+    ''' Have the previously invited user join from a new client instance
+    '''
+    result = {'Test' : 'Join as Invited User','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    
+    usermsg = getClientInstance();
+    n = usermsg.joinRoom(STORAGE['testuser']['User'],STORAGE['testuser']['room'],STORAGE['testuser']['pass'])
+    
+    if not n:
+        result['Notes'] = 'Could not join'
+        return [result,isFatal]
+    
+    STORAGE['testuser']['clientInstance'] = usermsg
+    
+    CONN,CURSOR = opendb()
+    
+    # Check the DB to ensure we're now active
+    CURSOR.execute("SELECT * from users where username=? and active=1",(STORAGE['testuser']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'Not Active in DB'
+        return [result,isFatal]
+    
+    # Check we've got a session token
+    if not msg.sesskey:
+        result['Notes'] = 'No Session Key'
+        return [result,isFatal]
+        
+    # Check the client has recorded what it needs to
+    if not msg.room:
+        result['Notes'] = 'Client forgot room'
+        return [result,isFatal]
+
+    if not msg.user:
+        result['Notes'] = 'Client forgot user'
+        return [result,isFatal]
+
+    if not msg.roompass:
+        result['Notes'] = 'Client forgot roompass'
+        return [result,isFatal]
+
+    if not msg.syskey:
+        result['Notes'] = 'No SYSTEM key'
+        return [result,isFatal]
+    
+    
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+def test_eight(msg):
+    ''' Have the testuser send a message and ensure that the adminuser receives it
+    
+    We first call pollMsg to flush the message queue
+    '''
+    
+    result = {'Test' : 'Send a Message','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+        
+    # Poll for messages to clear the queue
+    f = msg.pollForMessage()
+    
+    testpayload = 'The admin is a ....'
+    # Now send a message as test user
+    STORAGE['testuser']['clientInstance'].sendMsg(testpayload)
+    
+    # Now call pollMsg as admin to ensure we receive the message
+    received = msg.pollForMessage()
+
+    # Poll as testuser to clear the queue ready for future tests
+    f = STORAGE['testuser']['clientInstance'].pollForMessage()
+    
+    if len(received) < 1:
+        result['Notes'] = 'No messages received'
+        return [result,isFatal]
+    
+    if len(received[0]) < 2:
+        result['Notes'] = 'Malformed message returned'
+        return [result,isFatal]
+        
+    # Now check the payload
+    #
+    # We need to trim out the timestamp etc
+    r = received[0][1].split('>')
+    m = r[1].lstrip()
+    if testpayload != m:
+        result['Notes'] = 'Incorrect message received: (%s)v(%s)' % (testpayload,m)
+        return [result,isFatal]
+        
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+
+def test_nine(msg):
+    ''' Try to maliciously invite SYSTEM (as a precursor to logging in as them)
+    
+    Essentially a regression test for LOC-5
+    
+    '''
+    
+    result = {'Test' : 'LOC-5 Try to invite SYSTEM','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+        
+    # Poll for messages to clear the queue
+    f = msg.pollForMessage()
+    
+    # Now have the test user try to invite SYSTEM
+    n = STORAGE['testuser']['clientInstance'].inviteUser('SYSTEM')
+    if n:
+        result['Notes'] = 'Allowed to invite SYSTEM'
+        return [result,isFatal]
+    
+    # Now call pollMsg as admin to ensure we received the warning message
+    received = msg.pollForMessage()
+
+    # Poll as testuser to clear the queue ready for future tests
+    f = STORAGE['testuser']['clientInstance'].pollForMessage()
+    
+    if len(received) < 1:
+        result['Notes'] = 'No warning message received'
+        return [result,isFatal]
+    
+    if len(received[0]) < 2:
+        result['Notes'] = 'Malformed message returned'
+        return [result,isFatal]
+        
+    # Now check the payload
+    #
+    # We need to trim out the timestamp etc
+    r = received[0][1].split('>')
+    m = r[1].lstrip()
+    
+    expected = "ALERT: User %s tried to invite SYSTEM" % (STORAGE['testuser']['User'],)
+
+    if expected != m:
+        result['Notes'] = 'Unexpected message received: %s' % (m)
+        return [result,isFatal]
+        
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+
+
+def test_ten(msg):
+    ''' Send a direct message and ensure it's received
+    
+    '''
+    
+    result = {'Test' : 'Send a Direct Message','Result' : 'FAIL', 'Notes': '' }
+    isFatal = False
+    
+    # Poll for messages to clear the queue
+    f = msg.pollForMessage()
+    f = STORAGE['testuser']['clientInstance'].pollForMessage()
+    
+    testpayload = 'Hi Hi'
+    
+    # Now send a direct message from test user to admin
+    n = STORAGE['testuser']['clientInstance'].sendDirectMsg(testpayload,'testadmin')
+    if not n:
+        result['Notes'] = 'Could not send direct message'
+        return [result,isFatal]
+    
+    # Poll as the admin user to ensure it was received
+    received = msg.pollForMessage()
+    
+    # Poll as testuser to clear the queue ready for future tests
+    f = STORAGE['testuser']['clientInstance'].pollForMessage()
+    
+    if len(received) < 1:
+        result['Notes'] = 'No messages received'
+        return [result,isFatal]
+    
+    if len(received[0]) < 2:
+        result['Notes'] = 'Malformed message returned'
+        return [result,isFatal]
+        
+    # Now check the payload
+    #
+    # We need to trim out the timestamp etc
+    r = received[0][1].split('>')
+    m = r[1].lstrip()
+    if testpayload != m:
+        result['Notes'] = 'Incorrect message received: (%s)v(%s)' % (testpayload,m)
+        return [result,isFatal]
+        
+    if "DM" not in r[0]:
+        result['Notes'] = 'Envelope not marked as DM: %s' % (r[0],)
+        return [result,isFatal]
+        
+    result['Result'] = 'Pass'
+    return [result,isFatal]
+    
+
+
+def test_eleven(msg):
+    ''' Invite a new user and then have admin kick them to ensure they're 
+    actually kicked out of the room
+    
+    '''
+    result = {'Test' : 'Invite and kick a user','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    
+    n = msg.inviteUser('testuser2')
+    if not n:
+        result['Notes'] = 'Could not invite testuser2'
+        return [result,isFatal]
+    
+    if len(n) < 4:
+        result['Notes'] = 'Client returned too short response'
+        return [result,isFatal]
+        
+    # Otherwise, we've got details for a new user to be able to join
+    #
+    # Store them for a later test
+    
+    STORAGE['testuser2'] = {
+        'room':n[0],
+        'pass':"%s:%s" % (n[1],n[2]),
+        'User':n[3]
+        }
+    
+    # Create a new instance so we can join as testuser2
+    usermsg = getClientInstance();
+    n = usermsg.joinRoom(STORAGE['testuser2']['User'],STORAGE['testuser2']['room'],STORAGE['testuser2']['pass'])
+    
+    if not n:
+        result['Notes'] = 'User could not join'
+        return [result,isFatal]
+    
+    STORAGE['testuser2']['clientInstance'] = usermsg
+    
+    # Now have the admin kick (but not ban) them
+    msg.kickUser('testuser2',False)
+    
+    
+    # Check that a failure message was written
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT msg FROM failuremsgs where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'User not notified'
+        return [result,isFatal]  
+        
+    # Poll to receive the failure message and verify it's deleted
+    msgs = STORAGE['testuser2']['clientInstance'].pollForMessage()
+    
+    if len(msgs) < 1:
+        result['Notes'] = "User didn't receive notification"
+        return [result,isFatal]        
+    
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT msg FROM failuremsgs where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if r:
+        result['Notes'] = 'Notification not purged on poll'
+        return [result,isFatal]  
+    
+    # Now check that their session was suspended
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT username FROM users where username=? and active=1",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if r:
+        result['Notes'] = 'User still considered active in room'
+        return [result,isFatal]         
+    
+    # Check if their session still exists
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT username FROM sessions where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()    
+    if r:
+        result['Notes'] = 'User still has active session'
+        return [result,isFatal]        
+    
+    # Otherwise, looks good
+    result['Result'] = "Pass"
+    return [result,isFatal]
+
+
+def test_twelve(msg):
+    ''' Rejoin as the previously invited user, and then ban them
+    
+    
+    '''
+    result = {'Test' : 'Ban a user','Result' : 'FAIL', 'Notes': '' }
+    isFatal = True
+    
+    # Flush the failedmessage queue
+    f = STORAGE['testuser2']['clientInstance'].pollForMessage()
+    
+    # Re-join as testuser2
+    n = STORAGE['testuser2']['clientInstance'].joinRoom(STORAGE['testuser2']['User'],
+                                                        STORAGE['testuser2']['room'],STORAGE['testuser2']['pass'])
+    
+    if not n:
+        result['Notes'] = 'TestUser2 could not join'
+        return [result,isFatal]
+    
+    # Now have the admin ban them
+    msg.kickUser('testuser2',True)
+    
+    
+    # Check that a failure message was written
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT msg FROM failuremsgs where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if not r:
+        result['Notes'] = 'User not notified'
+        return [result,isFatal]  
+    
+    # Poll to receive the failure message and verify it's deleted
+    msgs = STORAGE['testuser2']['clientInstance'].pollForMessage()
+    
+    if len(msgs) < 1:
+        result['Notes'] = "User didn't receive notification"
+        return [result,isFatal]        
+    
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT msg FROM failuremsgs where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if r:
+        result['Notes'] = 'Notification not purged on poll'
+        return [result,isFatal]  
+   
+    # Now check that their session was suspended
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT username FROM users where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()
+    
+    if r:
+        result['Notes'] = 'User still exists for room'
+        return [result,isFatal]         
+    
+    # Check if their session still exists
+    CONN,CURSOR = opendb()
+    CURSOR.execute("SELECT username FROM sessions where username=?",(STORAGE['testuser2']['User'],))
+    r = CURSOR.fetchone()
+    CONN.close()    
+    if r:
+        result['Notes'] = 'User still has active session'
+        return [result,isFatal]        
+    
+    # We don't need this any more
+    del STORAGE['testuser2']
+    
+    # Otherwise, looks good
+    result['Result'] = "Pass"
+    return [result,isFatal]
+
+
+    
+
+
+if __name__ == '__main__':
+    # Start the server
+    proc1 = restartServer(False)
+
+    if not proc1:
+        # Server start failed.
+        # abort, abort, abort
+        exit(proc1,1)
+
+    
+    try:
+        # I don't like generic catchall exceptions
+        # but, we want to make sure we kill the background
+        # process if there is one.
+        results = run_tests()
+    except Exception as e:
+        print traceback.format_exc()
+        print e
+        exit(proc1,1)
+    
+    cols = ['No','Test','Result','Notes']
+    print make_table(cols,results)
+    
+    exit(proc1,0)
+    
+