]> WPIA git - motion.git/commitdiff
add: implement motion masking
authorINOPIAE <m.maengel@inopiae.de>
Mon, 21 Dec 2020 05:41:52 +0000 (06:41 +0100)
committerINOPIAE <m.maengel@inopiae.de>
Sat, 9 Jan 2021 05:15:38 +0000 (06:15 +0100)
Change-Id: Ia0c471b67764c2feafcf0065112d12190b7b7536

README.md
motion.py
sql/from_6.sql [new file with mode: 0644]
sql/schema.sql
tests/test_basics.py
tests/test_motion.py
tests/test_proxy.py [new file with mode: 0644]
tests/test_user_api.py

index 76444524e5882c97210c783e448348902932f3bc..3bbaeb5f91c15d8d5262a58b73eea315e19a42aa 100644 (file)
--- a/README.md
+++ b/README.md
@@ -200,4 +200,18 @@ flask create-user "email address" "host"
 
 ```
 
-The application will return a message for the success.
+The application will return a message for success.
+
+To mask motions use this command
+```
+flask motion_masking motionidentifier motionurl host"
+
+```
+
+where:
+
+* motionidentifier - the motion identifier or left part of it which should be cleaned
+* motionurl - an url to a motion that is the reason for the cleanup
+* host - host where the motions are located
+
+The application will return a message for success.
index 99daaf68d46d252feb0e4f321041b98704f1543d..51ec30bc795d9782fce9eb6a1aed42e33d9c0c4d 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -12,6 +12,7 @@ from datetime import date, time, datetime
 from flask_language import Language, current_language
 import gettext
 import click
+import re
 
 def get_db():
     db = getattr(g, '_database', None)
@@ -260,6 +261,11 @@ def init_db():
                 db.prepare("ALTER TABLE \"voter\" ALTER COLUMN \"host\" SET NOT NULL")()
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=6")()
 
+        if ver < 7:
+            with app.open_resource('sql/from_6.sql', mode='r') as f:
+                db.execute(f.read())
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=7")()
+
 init_db()
 
 
@@ -293,6 +299,12 @@ def rel_redirect(loc):
     r.autocorrect_location_header = False
     return r
 
+def write_proxy_log(userid, action, comment):
+    get_db().prepare("INSERT INTO adminlog(user_id, action, comment, action_user_id) VALUES($1, $2, $3, $4)")(userid, action, comment, g.voter)
+
+def write_masking_log(comment):
+    get_db().prepare("INSERT INTO adminlog(user_id, action, comment, action_user_id) VALUES($1, 'motionmasking', $2, $1)")(0, comment)
+
 @app.route("/motion", methods=['POST'])
 def put_motion():
     cat=request.form.get("category", "")
@@ -451,6 +463,7 @@ def add_proxy():
         if rv[0].get("c") is None or rv[0].get("c") >= max_proxy:
             return _("Error, Max proxy for '%s' reached.") % (proxy), 400
     rv = get_db().prepare("INSERT INTO proxy(voter_id, proxy_id, granted_by) VALUES ($1,$2,$3)")(voterid, proxyid, g.voter)
+    write_proxy_log(voterid, 'proxygranted', 'proxy: '+str(proxyid))
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revoke", methods=['POST'])
@@ -459,6 +472,7 @@ def revoke_proxy():
         return _('Forbidden'), 403
     id=request.form.get("id", "")
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE id=$2")(g.voter, int(id))
+    write_proxy_log(int(id), 'proxyrevoked', '')
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revokeall", methods=['POST'])
@@ -466,6 +480,7 @@ def revoke_proxy_all():
     if not may_admin("proxyadmin"):
         return _('Forbidden'), 403
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
+    write_proxy_log(g.voter, 'proxyrevokedall', '')
     return rel_redirect("/proxy")
 
 @app.route("/language/<string:language>")
@@ -485,3 +500,25 @@ def create_user(email, host):
             db.prepare("INSERT INTO voter(\"email\", \"host\") VALUES($1, $2)")(email, host)
             messagetext=_("User '%s' inserted to %s.") % (email, host)
     click.echo(messagetext)
+
+@app.cli.command("motion-masking")
+@click.argument("motion")
+@click.argument("motionreason")
+@click.argument("host")
+def motion_masking(motion, motionreason, host):
+    if re.search(r"[%_\\]", motion):
+        messagetext = _("No wildcards allowed for motion entry '%s'.") % (motion)
+        click.echo(messagetext)
+    else:
+        db = get_db()
+        with db.xact():
+            rv = db.prepare("SELECT id FROM motion WHERE identifier LIKE $1 AND host = $2")(motion+"%", host)
+            count = len(rv)
+            messagetext = _("%s record(s) affected by masking of '%s'.") % (count, motion)
+            click.echo(messagetext)
+            if len(rv) != 0:
+                rv = db.prepare("SELECT id FROM motion WHERE content LIKE $1 AND host = $2")('%'+motionreason+"%", host)
+                rv = db.prepare("UPDATE motion SET name=$3, content=$4 WHERE identifier LIKE $1 AND host = $2 RETURNING id ")(motion+"%", host, _("Motion masked"), _("Motion masked on base of motion [%s](%s) on %s") % (motionreason, motionreason, datetime.now().strftime("%Y-%m-%d")))
+                messagetext = _("%s record(s) updated by masking of '%s'.") % (len(rv), motion)
+                write_masking_log(_("%s motion(s) masked on base of motion %s with motion identifier '%s' on host %s") %(len(rv), motionreason, motion, host))
+                click.echo(messagetext)
diff --git a/sql/from_6.sql b/sql/from_6.sql
new file mode 100644 (file)
index 0000000..095d54f
--- /dev/null
@@ -0,0 +1,10 @@
+DROP TABLE IF EXISTS adminlog;
+DROP TYPE IF EXISTS "admin_log";
+CREATE TYPE "admin_log" AS ENUM ('motionmasking', 'proxygranted', 'proxyrevoked', 'proxyrevokedall');
+CREATE TABLE adminlog (id serial NOT NULL,
+                   user_id INTEGER NOT NULL,
+                   action admin_log NOT NULL,
+                   comment text NULL,
+                   action_user_id INTEGER NOT NULL,
+                   actiontime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   PRIMARY KEY(id));
index 690425bdbcf697509ac116210c89fd62181190ee..c3967a45391b9230a60f436d8508e064ef7ebdfc 100644 (file)
@@ -43,6 +43,17 @@ CREATE TABLE proxy (id serial NOT NULL,
 CREATE INDEX proxy_voter ON proxy (voter_id);
 CREATE INDEX proxy_proxy ON proxy (proxy_id);
 
+DROP TABLE IF EXISTS adminlog;
+DROP TYPE IF EXISTS "admin_log";
+CREATE TYPE "admin_log" AS ENUM ('motionmasking', 'proxygranted', 'proxyrevoked', 'proxyrevokedall');
+CREATE TABLE adminlog (id serial NOT NULL,
+                   user_id INTEGER NOT NULL,
+                   action admin_log NOT NULL,
+                   comment text NULL,
+                   action_user_id INTEGER NOT NULL,
+                   actiontime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   PRIMARY KEY(id));
+
 DROP TABLE IF EXISTS schema_version;
 CREATE TABLE schema_version (version INTEGER NOT NULL);
-INSERT INTO schema_version(version) VALUES(6);
+INSERT INTO schema_version(version) VALUES(7);
index f886c34d708a77e08b0245c5ac3988c1a6727c07..85dc7b6690bc90ddca4e264cbf27fa604907c911 100644 (file)
@@ -74,10 +74,36 @@ class BasicTest(TestCase):
             + '\nNo <span class=\"badge badge-pill badge-secondary\">'+str(no)+'</span><br>'\
             + '\nAbstain <span class=\"badge badge-pill badge-secondary\">'+str(abstain)+'</span>'
 
-
+    # functions handling or using database
     def open_DB(self):
         return postgresql.open(app.config.get("DATABASE"), user=app.config.get("USER"), password=app.config.get("PASSWORD"))
 
+    def db_select(self, sql, parameter):
+        with self.open_DB() as db:
+            rv = db.prepare(sql)(parameter)
+            return rv
+
+    def db_select2(self, sql, parameter, parameter2):
+        with self.open_DB() as db:
+            rv = db.prepare(sql)(parameter, parameter2)
+            return rv
+
+    def recordCountLog(self, parameter):
+        return self.recordCount("SELECT * FROM adminlog WHERE action=$1", parameter)
+
+    def recordCount(self, sql, parameter):
+        rv = self.db_select(sql, parameter)
+        return len(rv)
+
+    def logRecordDetailsTest(self, parameter, recordno, voterid, comment, actionuserid):
+        rv = self.db_select("SELECT * FROM adminlog WHERE action=$1 ORDER BY id", parameter)
+        self.assertEqual(voterid, rv[recordno].get("user_id"))
+        if comment:
+            self.assertEqual(comment, rv[recordno].get("comment"))
+        else:
+            self.assertEqual('', rv[recordno].get("comment"))
+        self.assertEqual(actionuserid, rv[recordno].get("action_user_id"))
+    
     # functions to clear database
     def db_clear(self):
         with self.open_DB() as db:
index 769deb131896a029710f670bad96e0dcdcd80cca..f6992968ff128e05375539cc51ba1d4f19e9ff54 100644 (file)
@@ -498,360 +498,6 @@ class AuditMotionTests(BasicTest):
             + '\n    <div>User C: no</div>\n  </div>\n</div>\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
         self.assertIn(str.encode(testtext), result.data)
 
-class ProxyManagementTests(BasicTest):
-
-    def setUp(self):
-        self.init_test()
-        global user
-        user='testuser/proxyadmin:*'
-        global userid
-        userid=4
-        self.db_sampledata()
-
-    def tearDown(self):
-        pass
-
-    def test_see_proxy(self):
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= 'div class="container">\n<form action="/proxy/add" method="POST">'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= '<select class="float form-control" name="voter">\n        '\
-            + '<option>User A</option>\n        <option>User B</option>\n        '\
-            + '<option>User C</option>\n        '\
-            + '<option>testuser</option>\n      '\
-            + '</select>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<select class="float form-control" name="proxy">\n          '\
-            + '<option>User A</option>\n          '\
-            + '<option>User B</option>\n          '\
-            + '<option>User C</option>\n          '\
-            + '<option>testuser</option>\n      '\
-            + '</select>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
-            + '</table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<a class="nav-link" href="/proxy">Proxy management</a>'
-        self.assertIn(str.encode(testtext), result.data)
-
-    def test_add_proxy(self):
-        voter=''
-        proxy=''
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
-
-        voter='User A'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy not found.'), response.data)
-
-        voter='User Z'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, voter not found.'), response.data)
-
-        voter=''
-        proxy='User B'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, voter not found.'), response.data)
-
-        voter='User B'
-        proxy='User B'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
-        
-        voter='User A'
-        proxy='User Z'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy not found.'), response.data)
-
-        voter='User A'
-        proxy='User B'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<form action="/proxy/revoke" method="POST">'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        '\
-            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
-
-        voter='User A'
-        proxy='User C'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
-
-        voter='User C'
-        proxy='User B'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        '\
-            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-
-        voter='testuser'
-        proxy='User B'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, Max proxy for \'User B\' reached.'), response.data)
-        
-        voter='testuser'
-        proxy='User A'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to: User A\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-
-        voter='User B'
-        proxy='testuser'
-        response = self.addProxy(user, voter, proxy)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User B</td>\n        <td>testuser</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="4">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to: User A\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of: User B\n'
-        self.assertIn(str.encode(testtext), result.data)
-
-        response = self.revokeProxy(user, userid)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to: User A\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-
-        response = self.revokeProxy(user, 3)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
-            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
-            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
-            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
-            + '</tr>\n    </table>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-
-        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        self.assertEqual(response.status_code, 302)
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<table>\n      '\
-            + '<thead>\n        '\
-            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n'\
-            + '</table>\n'
-        self.assertNotIn(str.encode(testtext), result.data)
-
-        proxytest="proxytest"
-        with self.open_DB() as db:
-            db.prepare("INSERT INTO voter(\"email\", \"host\") VALUES($1, $2)")(proxytest, '127.0.0.1:5001')
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-
-        response = self.addProxy(user, proxytest, 'testuser')
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, voter not found.'), response.data)
-
-        response = self.addProxy(user, 'testuser', proxytest)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy not found.'), response.data)
-
-    def test_see_proxy_host_only(self):
-        proxytest="proxytest"
-        with self.open_DB() as db:
-            db.prepare("INSERT INTO voter(\"email\", \"host\") VALUES($1, $2)")(proxytest, '127.0.0.1:5001')
-        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= 'div class="container">\n<form action="/proxy/add" method="POST">'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= 'proxy granted to:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= 'holds proxy of:'
-        self.assertNotIn(str.encode(testtext), result.data)
-        testtext= '<select class="float form-control" name="voter">\n        '\
-            + '<option>User A</option>\n        <option>User B</option>\n        '\
-            + '<option>User C</option>\n        '\
-            + '<option>testuser</option>\n      '\
-            + '</select>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<select class="float form-control" name="proxy">\n          '\
-            + '<option>User A</option>\n          '\
-            + '<option>User B</option>\n          '\
-            + '<option>User C</option>\n          '\
-            + '<option>testuser</option>\n      '\
-            + '</select>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        self.assertNotIn(str.encode(proxytest), result.data)
-
-class ProxyVoteTests(BasicTest):
-
-    def setUp(self):
-        self.init_test()
-        global user
-        user='testuser/vote:* proxyadmin:*'
-        self.db_sampledata()
-
-    def tearDown(self):
-        pass
-
-    def test_proxy_vote(self):
-        voter='testuser'
-        proxy='User B'
-        proxyid=2
-        proxyuser='User B/vote:*'
-
-        response = self.addProxy(user, proxy, voter)
-        self.assertEqual(response.status_code, 302)
-
-        motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes', proxyid)
-        self.assertEqual(response.status_code, 302)
-
-        # testuser view
-        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
-        # own vote without change
-        testtext= '<form action="/motion/g1.20200402.004/vote/4" method="POST">\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
-        self.assertIn(str.encode(testtext), result.data)
-        # proxy vote with change
-        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
-            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        
-        # User B view
-        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': proxyuser}, follow_redirects=True)
-        # own vote without change
-        testtext= '<h3>My vote</h3>\nGiven by testuser\n'\
-            + '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
-            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
-        self.assertIn(str.encode(testtext), result.data)
-        
-        # change vote
-        response = self.createVote(user, motion, 'no', proxyid)
-        self.assertEqual(response.status_code, 302)
-
-        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
-            + '<button type="submit" class="btn btn-success" name="vote" value="no" id="vote-no">No</button>\n'\
-            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
-        self.assertIn(str.encode(testtext), result.data)
-
-    def test_proxy_vote_no_proxy(self):
-        voter='testuser'
-        proxy='User B'
-        # wrong proxy id
-        proxyid=3
-
-        response = self.addProxy(user, proxy, voter)
-        self.assertEqual(response.status_code, 302)
-
-        motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes', proxyid)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy not found'), response.data)
-        
-        # non existing id
-        proxyid=10000
-
-        motion='g1.20200402.004'
-        response = self.createVote(user, motion, 'yes', proxyid)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(str.encode('Error, proxy not found'), response.data)
-
-    def test_proxy_vote_no_voter(self):
-        voter='User A'
-        proxy='User B'
-        proxyid=2
-
-        response = self.addProxy(user, proxy, voter)
-        self.assertEqual(response.status_code, 302)
-
-        user1='testuser1/'
-        motion='g1.20200402.004'
-        response = self.createVote(user1, motion, 'yes', proxyid)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn(str.encode('Forbidden'), response.data)
-
-
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
new file mode 100644 (file)
index 0000000..95bc8f2
--- /dev/null
@@ -0,0 +1,349 @@
+from datetime import datetime
+from tests.test_basics import BasicTest
+import motion
+import postgresql
+from motion import app
+
+
+class ProxyManagementTests(BasicTest):
+
+    def setUp(self):
+        self.init_test()
+        global user
+        user='testuser/proxyadmin:*'
+        global userid
+        userid = 4
+        self.db_sampledata()
+
+    def tearDown(self):
+        pass
+
+    def test_see_proxy(self):
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= 'div class="container">\n<form action="/proxy/add" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="voter">\n        '\
+            + '<option>User A</option>\n        <option>User B</option>\n        '\
+            + '<option>User C</option>\n        '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="proxy">\n          '\
+            + '<option>User A</option>\n          '\
+            + '<option>User B</option>\n          '\
+            + '<option>User C</option>\n          '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<a class="nav-link" href="/proxy">Proxy management</a>'
+        self.assertIn(str.encode(testtext), result.data)
+
+    def test_add_proxy(self):
+        voter=''
+        proxy=''
+        records=0
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+
+        voter='User A'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found.'), response.data)
+
+        voter='User Z'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter=''
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter='User B'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+
+        voter='User A'
+        voterid=1
+        proxy='User B'
+        records=1
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<form action="/proxy/revoke" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+        self.logRecordDetailsTest('proxygranted', records-1, voterid, 'proxy: 2', userid)
+
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+
+        voter='User A'
+        proxy='User C'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+
+        voter='User C'
+        voterid=3
+        proxy='User B'
+        records=2
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+        self.logRecordDetailsTest('proxygranted', records-1, voterid, 'proxy: 2', userid)
+
+        voter='testuser'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, Max proxy for \'User B\' reached.'), response.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+
+        voter='testuser'
+        voterid=4
+        proxy='User A'
+        records=3
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+        self.logRecordDetailsTest('proxygranted', records-1, voterid, 'proxy: 1', userid)
+
+        voter='User B'
+        voterid=2
+        proxy='testuser'
+        records=4
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User B</td>\n        <td>testuser</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="4">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of: User B\n'
+        self.assertIn(str.encode(testtext), result.data)
+        self.assertEqual(records, self.recordCountLog('proxygranted'))
+        self.logRecordDetailsTest('proxygranted', records-1, voterid, 'proxy: 4', userid)
+
+        recordsRevoked=0
+        self.assertEqual(recordsRevoked, self.recordCountLog('proxyrevoked'))
+        recordsRevoked=1
+        response = self.revokeProxy(user, userid)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        self.assertEqual(recordsRevoked, self.recordCountLog('proxyrevoked'))
+        self.logRecordDetailsTest('proxyrevoked', recordsRevoked-1, userid, '', userid)
+
+        recordsRevoked=2
+        proxyid=3
+        response = self.revokeProxy(user, proxyid)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        self.assertEqual(recordsRevoked, self.recordCountLog('proxyrevoked'))
+        self.logRecordDetailsTest('proxyrevoked', recordsRevoked-1, proxyid, '', userid)
+
+        recordsRevokedAll=0
+        self.assertEqual(recordsRevokedAll, self.recordCountLog('proxyrevokedall'))
+        recordsRevokedAll=1
+        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        self.assertEqual(recordsRevokedAll, self.recordCountLog('proxyrevokedall'))
+        self.logRecordDetailsTest('proxyrevokedall', recordsRevokedAll-1, userid, '', userid)
+
+class ProxyVoteTests(BasicTest):
+
+    def setUp(self):
+        self.init_test()
+        global user
+        user='testuser/vote:* proxyadmin:*'
+        self.db_sampledata()
+
+    def tearDown(self):
+        pass
+
+    def test_proxy_vote(self):
+        voter='testuser'
+        proxy='User B'
+        proxyid=2
+        proxyuser='User B/vote:*'
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 302)
+
+        # testuser view
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
+        # own vote without change
+        testtext= '<form action="/motion/g1.20200402.004/vote/4" method="POST">\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
+        self.assertIn(str.encode(testtext), result.data)
+        # proxy vote with change
+        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        
+        # User B view
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': proxyuser}, follow_redirects=True)
+        # own vote without change
+        testtext= '<h3>My vote</h3>\nGiven by testuser\n'\
+            + '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
+        self.assertIn(str.encode(testtext), result.data)
+        
+        # change vote
+        response = self.createVote(user, motion, 'no', proxyid)
+        self.assertEqual(response.status_code, 302)
+
+        result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+            + '<button type="submit" class="btn btn-success" name="vote" value="no" id="vote-no">No</button>\n'\
+            + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+    def test_proxy_vote_no_proxy(self):
+        voter='testuser'
+        proxy='User B'
+        # wrong proxy id
+        proxyid=3
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found'), response.data)
+        
+        # non existing id
+        proxyid=10000
+
+        motion='g1.20200402.004'
+        response = self.createVote(user, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found'), response.data)
+
+    def test_proxy_vote_no_voter(self):
+        voter='User A'
+        proxy='User B'
+        proxyid=2
+
+        response = self.addProxy(user, proxy, voter)
+        self.assertEqual(response.status_code, 302)
+
+        user1='testuser1/'
+        motion='g1.20200402.004'
+        response = self.createVote(user1, motion, 'yes', proxyid)
+        self.assertEqual(response.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), response.data)
\ No newline at end of file
index f1819f8d36e6a8cfee5b3f6ad0ed9d18c7c2b370..521c1f20a2f03ccfb8da95ca13061df7d9948761 100644 (file)
@@ -5,14 +5,10 @@ import postgresql
 
 from click.testing import CliRunner
 from motion import create_user
+from motion import motion_masking
 from motion import app
 
 
-def db_select2(self, sql, parameter, parameter2):
-    with self.open_DB() as db:
-        rv = db.prepare(sql)(parameter, parameter2)
-        return rv
-
 class GeneralTests(BasicTest):
 
     def setUp(self):
@@ -20,8 +16,6 @@ class GeneralTests(BasicTest):
 
     def tearDown(self):
         pass
-    
-
 
     def test_create_user(self):
         user = 'John Doe'
@@ -31,7 +25,7 @@ class GeneralTests(BasicTest):
         assert result.exit_code == 0
         self.assertIn("User 'John Doe' inserted to %s." % host, result.output)
 
-        rv = db_select2(self,"SELECT email FROM voter WHERE lower(email)=lower($1) AND host=$2", user, host)
+        rv = self.db_select2("SELECT email FROM voter WHERE lower(email)=lower($1) AND host=$2", user, host)
         self.assertIn(user, rv[0].get("email"))
 
         result = runner.invoke(create_user, (user, host))
@@ -45,9 +39,127 @@ class GeneralTests(BasicTest):
         assert result.exit_code == 0
         self.assertIn("User 'John Doe' inserted to 127.0.0.1:5001.", result.output)
 
-        rv = db_select2(self,"SELECT email FROM voter WHERE lower(email)=lower($1) AND host=$2", user, host)
+        rv = self.db_select2("SELECT email FROM voter WHERE lower(email)=lower($1) AND host=$2", user, host)
         self.assertIn(user, rv[0].get("email"))
 
         result = runner.invoke(create_user, (user, host))
         assert result.exit_code == 0
         self.assertIn("User 'John Doe' already exists on 127.0.0.1:5001.", result.output)
+
+    def test_motion_masking(self):
+        self.db_sampledata()
+        records=0
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        # test motion not exists
+        motion = 'g1.20200402.999'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("0 record(s) affected by masking of 'g1.20200402.999'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        # test motion with wilcards
+        motion = 'g1.20200402.00%'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.20200402.00%'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.2020040%.001'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.2020040%.001'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.20200402.00_'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.20200402.00_'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.2020040_.001'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.2020040_.001'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.2020040_.%'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.2020040_.%'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.20200402.0\\1'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.20200402.0\\1'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        motion = 'g1.2020040\.001'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= app.config.get("DEFAULT_HOST")
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("No wildcards allowed for motion entry 'g1.2020040\\.001'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+
+        # test masking single motion
+        sql = "SELECT id FROM motion WHERE content LIKE $1"
+        motion = 'g1.20200402.001'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("1 record(s) affected by masking of 'g1.20200402.001'.", result.output)
+        self.assertIn("1 record(s) updated by masking of 'g1.20200402.001'.", result.output)
+        records=1
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+        self.logRecordDetailsTest('motionmasking', records-1, 0, 
+                          "1 motion(s) masked on base of motion http://motiontest.wpia.club/motion/xxx with motion identifier 'g1.20200402.001' on host 127.0.0.1:5000", 0)
+        self.assertEqual(1, self.recordCount(sql, '%' + motionreason +'%'))
+
+        # test masking muliple motions
+        motion = 'g1.20200402'
+        motionreason='http://motiontest.wpia.club/motion/1xxx'
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("4 record(s) affected by masking of 'g1.20200402'.", result.output)
+        self.assertIn("4 record(s) updated by masking of 'g1.20200402'.", result.output)
+        records=2
+        self.assertEqual(records, self.recordCountLog('motionmasking'))
+        self.logRecordDetailsTest('motionmasking', records-1, 0, 
+                          "4 motion(s) masked on base of motion http://motiontest.wpia.club/motion/1xxx with motion identifier 'g1.20200402' on host 127.0.0.1:5000", 0)
+        self.assertEqual(4, self.recordCount(sql, '%' + motionreason +'%'))
+
+        # test different host
+        motion = 'g1.20200402.001'
+        motionreason='http://motiontest.wpia.club/motion/xxx'
+        host= '127.0.0.1:5001'
+        runner = app.test_cli_runner()
+        result = runner.invoke(motion_masking, (motion, motionreason, host))
+        assert result.exit_code == 0
+        self.assertIn("0 record(s) affected by masking of 'g1.20200402.001'.", result.output)
+        self.assertEqual(records, self.recordCountLog('motionmasking'))