From: Felix Dörre
Date: Fri, 12 Jun 2020 14:37:21 +0000 (+0200)
Subject: Merge branch 'unit-test' into 'master'
X-Git-Url: https://code.wpia.club/?p=motion.git;a=commitdiff_plain;h=e459f85797c9ace2a9367294a5be7435533d3d37;hp=5e20a497d0fd1e068bb038e6e9d468c6104f4e30
Merge branch 'unit-test' into 'master'
Add Unit test
See merge request felixdoerre/motion!12
---
diff --git a/README.md b/README.md
index 01406ef..1181181 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,24 @@ pip install -r requirements.txt
```
Then edit config.py.example into config.py with your database connection
-To debug-run:
+To debug-run linux:
```
LANG=C.UTF-8 FLASK_DEBUG=1 FLASK_APP=motion.py flask run
```
+To debug-run windows:
+```
+set LANG=C.UTF-8
+set FLASK_DEBUG=1
+set FLASK_APP=motion.py
+flask run
+```
+
+For unit testing use config values from config.py.example:
+```
+python -m unittest tests/test_motion.py
+```
+
The database schema is automatically installed when the table "schema_version" does not exist and the application is started.
# Usage
diff --git a/motion.py b/motion.py
index f6ddf13..df5db15 100644
--- a/motion.py
+++ b/motion.py
@@ -27,11 +27,20 @@ md = Markdown(app, extensions=[EscapeHtml()])
# Load config
app.config.from_pyfile('config.py')
-prefix=app.config.get("GROUP_PREFIX")
-times=app.config.get("DURATION")
+class ConfigProxy:
+ def __init__(self, name):
+ self.name = name
+ @property
+ def per_host(self):
+ dict = app.config.get(self.name)
+ if dict is None:
+ return None
+ return dict.get(request.host)
-debuguser=app.config.get("DEBUGUSER")
+prefix = ConfigProxy("GROUP_PREFIX")
+times = ConfigProxy("DURATION")
+debuguser = ConfigProxy("DEBUGUSER")
@app.before_request
def lookup_user():
@@ -39,8 +48,9 @@ def lookup_user():
env = request.environ
user = None
- if debuguser is not None:
- parts =debuguser[request.host].split("/", 1)
+ my_debuguser = debuguser.per_host
+ if my_debuguser is not None:
+ parts = my_debuguser.split("/", 1)
user = parts[0]
roles = parts[1]
@@ -78,7 +88,7 @@ def lookup_user():
if a[0] not in g.roles:
g.roles[a[0]] = []
if val == "*":
- g.roles[a[0]] = [group for group in prefix[request.host]]
+ g.roles[a[0]] = [group for group in prefix.per_host]
else:
g.roles[a[0]].append(val)
return None
@@ -161,7 +171,7 @@ def main():
prev = rs[9][0]
else:
prev = -1
- return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times[request.host], prev=prev,
+ return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
categories=get_allowed_cats("create"))
def rel_redirect(loc):
@@ -175,7 +185,7 @@ def put_motion():
if cat not in get_allowed_cats("create"):
return "Forbidden", 403
time = int(request.form.get("days", "3"));
- if time not in times[request.host]:
+ if time not in times.per_host:
return "Error, invalid length", 500
db = get_db()
with db.xact():
@@ -184,9 +194,9 @@ def put_motion():
sr = s(cat, request.host)
ident=""
if len(sr) == 0 or sr[0][0] is None:
- ident=prefix[request.host][cat]+"."+t.strftime("%Y%m%d")+".001"
+ ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+".001"
else:
- ident=prefix[request.host][cat]+"."+t.strftime("%Y%m%d")+"."+("%03d" % (int(sr[0][0].split(".")[2])+1))
+ ident=prefix.per_host[cat]+"."+t.strftime("%Y%m%d")+"."+("%03d" % (int(sr[0][0].split(".")[2])+1))
p = db.prepare("INSERT INTO motion(\"name\", \"content\", \"deadline\", \"posed_by\", \"type\", \"identifier\", \"host\") VALUES($1, $2, CURRENT_TIMESTAMP + $3 * interval '1 days', $4, $5, $6, $7)")
p(request.form.get("title", ""), request.form.get("content",""), time, g.voter, cat, ident, request.host)
return rel_redirect("/")
@@ -215,6 +225,8 @@ def show_motion(motion):
+ "LEFT JOIN voter canceler ON canceler.id = motion.canceled_by "
+ "WHERE motion.identifier=$1 AND motion.host=$3")
rv = p(motion, g.voter, request.host)
+ if len(rv) == 0:
+ return "Error, Not found", 404
votes = None
if may("audit", rv[0].get("type")) and not rv[0].get("running") and not rv[0].get("canceled"):
votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(rv[0].get("id"));
diff --git a/sql/sample_data.sql b/sql/sample_data.sql
new file mode 100644
index 0000000..fd762be
--- /dev/null
+++ b/sql/sample_data.sql
@@ -0,0 +1,24 @@
+-- sample data for scheme version 3
+INSERT INTO voter (id,email) VALUES (1, 'User A');
+INSERT INTO voter (id,email) VALUES (2, 'User B');
+INSERT INTO voter (id,email) VALUES (3, 'User C');
+ALTER SEQUENCE voter_id_seq RESTART WITH 4;
+
+INSERT INTO motion (id,identifier,name,type,host,content,posed,posed_by,deadline,canceled,cancelation_reason,canceled_by) VALUES
+ (1,'g1.20200402.001','Motion A','group1','127.0.0.1:5000','My special motion','2020-04-02 21:40:33.780364',1,'2020-04-02 21:40:33.780364',Null,Null,Null);
+INSERT INTO motion (id,identifier,name,type,host,content,posed,posed_by,deadline,canceled,cancelation_reason,canceled_by) VALUES
+ (2,'g1.20200402.002','Motion B','group1','127.0.0.1:5000','A second motion','2020-04-02 21:41:26.588442',1,'2020-04-04 21:41:26.588442',Null,Null,Null);
+INSERT INTO motion (id,identifier,name,type,host,content,posed,posed_by,deadline,canceled,cancelation_reason,canceled_by) VALUES
+ (3,'g1.20200402.003','Motion C','group1','127.0.0.1:5000','A third motion', '2020-04-02 21:47:24.969588',1,'2020-04-04 21:47:24.969588','2020-04-03 21:48:24.969588','Entered with wrong text',1);
+-- add motion with timespan from now to 1 day from now
+INSERT INTO motion (id,identifier,name,type,host,content,posed,posed_by,deadline,canceled,cancelation_reason,canceled_by) VALUES
+ (4,'g1.20200402.004','Motion D','group1','127.0.0.1:5000','A fourth motion', current_timestamp ,1,current_timestamp + interval '1' day,Null,Null,Null);
+ALTER SEQUENCE motion_id_seq RESTART WITH 5;
+
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,1,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,2,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (1,3,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,1,'yes','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,2,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (2,3,'no','2020-04-02 21:54:34.469784');
+INSERT INTO vote (motion_id,voter_id,result,entered) VALUES (3,3,'yes','2020-04-02 21:48:34.469784');
diff --git a/tests/test_motion.py b/tests/test_motion.py
new file mode 100644
index 0000000..bb4acf0
--- /dev/null
+++ b/tests/test_motion.py
@@ -0,0 +1,457 @@
+import motion
+import unittest
+import postgresql
+from unittest import TestCase
+from motion import app
+from datetime import datetime
+
+app.config.update(
+ DEBUGUSER = {},
+ GROUP_PREFIX = {'127.0.0.1:5000': {'group1': 'g1', 'group2': 'g2'}},
+ DURATION = {'127.0.0.1:5000':[3, 7, 14]},
+ SERVER_NAME = '127.0.0.1:5000'
+)
+
+app.config['TESTING'] = True
+app.config['DEBUG'] = False
+
+
+class BasicTest(TestCase):
+
+ def init_test(self):
+ self.app = app.test_client()
+ self.assertEqual(app.debug, False)
+
+ # reset database
+ self.db_clear()
+
+ # functions to manipulate motions
+ def createVote(self, user, motion, vote):
+ return self.app.post(
+ '/motion/' + motion +'/vote',
+ environ_base={'USER_ROLES': user},
+ data=dict(vote=vote)
+ )
+
+
+ def createMotion(self, user, motiontitle, motioncontent, days, category):
+ return self.app.post(
+ '/motion',
+ environ_base={'USER_ROLES': user},
+ data=dict(title=motiontitle, content=motioncontent, days=days, category=category)
+ )
+
+ def cancelMotion(self, user, motion, reason):
+ return self.app.post(
+ '/motion/' + motion +'/cancel',
+ environ_base={'USER_ROLES': user},
+ data=dict(reason=reason)
+ )
+
+ def buildResultText(self, motiontext, yes, no, abstain):
+ return ''+motiontext+'
\n \nYes '+str(yes)+'
'\
+ + '\nNo '+str(no)+'
'\
+ + '\nAbstain '+str(abstain)+''
+
+ # functions to clear database
+ def db_clear(self):
+ with postgresql.open(app.config.get("DATABASE"), user=app.config.get("USER"), password=app.config.get("PASSWORD")) as db:
+ with app.open_resource('sql/schema.sql', mode='r') as f:
+ db.execute(f.read())
+
+ def db_sampledata(self):
+ with postgresql.open(app.config.get("DATABASE"), user=app.config.get("USER"), password=app.config.get("PASSWORD")) as db:
+ with app.open_resource('sql/sample_data.sql', mode='r') as f:
+ db.execute(f.read())
+
+
+# no specific rights required
+class GeneralTests(BasicTest):
+
+ def setUp(self):
+ self.init_test()
+ global user
+ user = 'testuser/'
+ self.db_sampledata()
+
+ def tearDown(self):
+ pass
+
+ def test_main_page(self):
+ response = self.app.get('/', environ_base={'USER_ROLES': user}, follow_redirects=True)
+ self.assertEqual(response.status_code, 200)
+
+ def test_basic_results_data(self):
+ result = self.app.get('/', environ_base={'USER_ROLES': user}, follow_redirects=True)
+ testtext= '
\n
'\
+ + '\n
Motion C (Canceled)\n
group1'\
+ + '\n
'\
+ + '\n
\n
Proposed: 2020-04-02 21:47:24 (UTC) by User A
'\
+ + '\n
Canceled: 2020-04-03 21:48:24 (UTC) by User A
\n
'\
+ + '\n
\n
A third motion
'\
+ + '\n
\nYes 1
'\
+ + '\nNo 0
'\
+ + '\nAbstain 0
\n
'\
+ + '\n
Cancelation reason: Entered with wrong text
\n
\n
'
+ self.assertIn(str.encode(testtext), result.data)
+ testtext= '\n
'\
+ + '\n
Motion B (Finished)\n
group1'\
+ + '\n
'\
+ + '\n
\n
Proposed: 2020-04-02 21:41:26 (UTC) by User A
'\
+ + '\n
Votes until: 2020-04-04 21:41:26 (UTC)
\n
'\
+ + '\n
\n
A second motion
'\
+ + '\n
\nYes 1
'\
+ + '\nNo 2
'\
+ + '\nAbstain 0
\n
\n
\n
\n'
+ self.assertIn(str.encode(testtext), result.data)
+ testtext= '\n
'\
+ + '\n
Motion A (Finished)\n
group1'\
+ + '\n
'\
+ + '\n
\n
Proposed: 2020-04-02 21:40:33 (UTC) by User A
'\
+ + '\n
Votes until: 2020-04-02 21:40:33 (UTC)
\n
'\
+ + '\n
\n
My special motion
'\
+ + '\n
\nYes 2
'\
+ + '\nNo 1
'\
+ + '\nAbstain 0
\n
\n
\n
\n'
+ self.assertIn(str.encode(testtext), result.data)
+
+ # start with second motion
+ result = self.app.get('/', environ_base={'USER_ROLES': user}, query_string=dict(start=2))
+ testtext= 'id=\"motion-3\">'
+ self.assertNotIn(str.encode(testtext), result.data)
+ testtext= 'id=\"motion-2">'
+ self.assertIn(str.encode(testtext), result.data)
+ testtext= 'id=\"motion-1\">'
+ self.assertIn(str.encode(testtext), result.data)
+
+ def test_basic_results_data_details(self):
+ motion='g1.20200402.002'
+ result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
+ testtext= 'A second motion
\n \n\nBack\n