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
# g1.20200402.003
'\ + + '\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
# g1.20200402.002
'\ + + '\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
# g1.20200402.001
'\ + + '\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' + self.assertIn(str.encode(testtext), result.data) + + def test_vote(self): + motion='g1.20200402.004' + response = self.createVote(user, motion, 'yes') + self.assertEqual(response.status_code, 403) + self.assertIn(str.encode('Forbidden'), response.data) + + def test_no_user(self): + result = self.app.get('/', follow_redirects=True) + self.assertEqual(result.status_code, 500) + self.assertIn(str.encode('Server misconfigured'), result.data) + + def test_user_invalid(self): + result = self.app.get('/', environ_base={'USER_ROLES': '/'}, follow_redirects=True) + self.assertEqual(result.status_code, 403) + self.assertIn(str.encode('Access denied'), result.data) + + def test_basic_env(self): + result = self.app.get('/', environ_base={'USER': 'testuser', 'ROLES':''}, follow_redirects=True) + testtext= 'id=\"motion-3\">' + self.assertIn(str.encode(testtext), result.data) + + def test_basic_results_data_details_not_given(self): + motion='g1.30190402.001' + result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True) + self.assertEqual(result.status_code, 404) + self.assertIn(str.encode('Error, Not found'), result.data) + + +class VoterTests(BasicTest): + + def setUp(self): + self.init_test() + global user + user='testuser/vote:*' + 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_home_data(self): + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertNotIn("", str(result.data) ) + + def test_createMotion(self): + title='My Motion' + content='My body' + response = self.createMotion(user, title, content, '3', 'group1') + self.assertEqual(response.status_code, 302) + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertIn(str.encode(title), result.data) + self.assertIn(str.encode(content), result.data) + self.assertIn(str.encode('g1.'+datetime.today().strftime('%Y%m%d')+'.001'), result.data) + + title='My Motion1' + content='My body1' + response = self.createMotion(user, title, content, '3', 'group1') + self.assertEqual(response.status_code, 302) + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertIn(str.encode(title), result.data) + self.assertIn(str.encode(content), result.data) + self.assertIn(str.encode('g1.'+datetime.today().strftime('%Y%m%d')+'.002'), result.data) + + title='My Motion2' + content='My body2' + response = self.createMotion(user, title, content, '3', 'group2') + self.assertEqual(response.status_code, 302) + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertIn(str.encode(title), result.data) + self.assertIn(str.encode(content), result.data) + self.assertIn(str.encode('g2.'+datetime.today().strftime('%Y%m%d')+'.001'), result.data) + + title='My Motion3' + content='My body3' + user1='testuser/vote:* create:group1 cancel:*' + response = self.createMotion(user1, title, content, '3', 'group1') + self.assertEqual(response.status_code, 302) + + title='My Motion4' + content='My body4' + user1='testuser/vote:* create:group1 create:group2 cancel:*' + response = self.createMotion(user1, title, content, '3', 'group1') + self.assertEqual(response.status_code, 302) + + + def test_createMotionMarkdown(self): + title='Markdown Test' + content= 'MyMotionBody MD [text](https//domain.tld/link)' + response = self.createMotion(user, title, content, '3', 'group1') + self.assertEqual(response.status_code, 302) + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertIn(str.encode(title), result.data) + self.assertIn(b'MyMotionBody MD text', result.data) + + def test_createMotionMarkdownDirectLink(self): + title='Markdown Test Link' + content='MyMotionBody MD directcombined <a href="https//domain.tld/link">combined1</a', result.data) + + def test_createMotionWrongDayLength(self): + title='My Motion' + content='My body' + response = self.createMotion(user, title, content, '21', 'group1') + self.assertEqual(response.status_code, 500) + self.assertIn(str.encode('Error, invalid length'), response.data) + + def test_createMotionWrongGroup(self): + title='My Motion' + content='My body' + response = self.createMotion(user, title, content, '3', 'test1') + self.assertEqual(response.status_code, 403) + self.assertIn(str.encode('Forbidden'), response.data) + + user1='testuser/vote:* create:group1 cancel:*' + response = self.createMotion(user1, title, content, '3', 'group2') + self.assertEqual(response.status_code, 403) + self.assertIn(str.encode('Forbidden'), response.data) + + def test_cancelMotion(self): + self.db_sampledata() + + motion='g1.20200402.004' + reason="none" + response = self.cancelMotion(user, motion, reason) + self.assertEqual(response.status_code, 500) + self.assertIn(str.encode('Error, form requires reason'), response.data) + + reason='cancel test' + response = self.cancelMotion(user, motion, reason) + self.assertEqual(response.status_code, 302) + result = self.app.get('/', environ_base={'USER_ROLES': user}) + self.assertIn(b'Cancelation reason: ' + str.encode(reason), result.data) + + motion='g1.30190402.001' + reason="none" + response = self.cancelMotion(user, motion, reason) + self.assertEqual(response.status_code, 404) + self.assertIn(str.encode('Error, Not found'), response.data) + + +class AuditMotionTests(BasicTest): + + def setUp(self): + self.init_test() + global user + user='testuser/audit:*' + self.db_sampledata() + + def tearDown(self): + pass + + def test_see_old_vote(self): + motion='g1.20200402.002' + result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True) + testtext= '
\n
\n Motion Votes\n
'\ + + '\n
\n
User A: yes
\n
User B: no
'\ + + '\n
User C: no
\n
\n
\nBack' + self.assertIn(str.encode(testtext), result.data) + + +if __name__ == "__main__": + unittest.main()