diff --git a/.gitignore b/.gitignore index 2886e55f4ebd761f44add8cccf47fa9ea63f9f85..299c19dad7c879992bab33d3dac6f0eb2fa87bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ nginx.log nginx.pid nginx.conf uwsgi.sock +.coverage +htmlcov/ diff --git a/README.md b/README.md index 5089659ed206165e1cf778fc05623763021ee1c3..b8d5b45bbcb196c3adafa39fc3f0071f2beec3d3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ Hinweis: diese Variante startet eine lokale Testversion der Website, es sind nic Alternativ, insbesondere zum Testen der Zugriffsbeschränkungen: Siehe `nginx.example.conf`. +### Unittests +Tests können mittels `./tests.py` ausgeführt werden. + +Coverage Tests können mittels `rm .coverage; python -m coverage run tests.py; python -m coverage html` ausgeführt werden. Dies erstellt einen Ordner `htmlcov` in dem HTML Output liegt. + ### Zum Mitmachen: 1. Repo für den eigenen User forken, dafür den "Fork-Button" auf der Website verwenden 2. Sicherstellen, dass der Upstream richtig konfiguriert ist: @@ -39,6 +44,7 @@ Optional (wird für einzelne Features benötigt): * python-ldap (Login mit Fachschaftsaccount) * python-icalendar (SoGo-Kalenderimport für Sitzungsankündigungen) * python-mysql-connector (wenn MySQL als Datenbank verwendet werden soll) +* python-coverage (Für Coverage Tests benötigt) Kurzform unter Ubuntu: `sudo apt install python3 python3-flask sqlite python3-requests python3-lxml python3-ldap3 python3-icalendar python3-mysql.connector` diff --git a/chapters.py b/chapters.py index 119d121a510bbc909433f286fb2566eb77d5a0d5..4e1dfa7c8a16bbdc3fa79a5acf62eb67242a8ba7 100644 --- a/chapters.py +++ b/chapters.py @@ -11,7 +11,10 @@ def suggest_chapter(lectureid): time= timedelta(hours=x.hour,minutes=x.minute,seconds=x.second).total_seconds() time = int(time) except ValueError: - flash('Falsches Zeitformat, "%H:%M:%S" wird erwartet. Z.B. "01:39:42" für eine Kapitel bei Stunde 1, Minute 39, Sekunde 42') + if 'ref' in request.values: + flash('Falsches Zeitformat, "%H:%M:%S" wird erwartet. Z.B. "01:39:42" für eine Kapitel bei Stunde 1, Minute 39, Sekunde 42') + return redirect(request.values['ref']) + return 'Wrong time format, "%H:%M:%S" is expected', 400 submitter = None if not ismod(): diff --git a/config.py.example b/config.py.example index fdb02c92b54e19caf93afbd6028a3156713f2fcc..cf7d0ba3b8551363673e6ccccd1221a1b2ab6371 100644 --- a/config.py.example +++ b/config.py.example @@ -30,3 +30,4 @@ LDAP_GROUPS = ['fachschaft'] ERROR_PAGE = 'static/500.html' RWTH_IP_RANGES = ['134.130.0.0/16', '137.226.0.0/16', '134.61.0.0/16', '192.35.229.0/24', '2a00:8a60::/32'] FSMPI_IP_RANGES = ['137.226.35.192/29', '137.226.75.0/27', '137.226.127.32/27', '137.226.231.192/26', '134.130.102.0/26' ] +DISABLE_SCHEDULER = False diff --git a/db.py b/db.py index 04f7fe83ff4ff4540f7d138c819fbce7bc6a9a0f..f64e6a36b6db5dabb604c0bbdaaebf3c20133fdb 100644 --- a/db.py +++ b/db.py @@ -23,8 +23,10 @@ if config['DB_ENGINE'] == 'sqlite': db = sqlite3.connect(config['SQLITE_DB']) cur = db.cursor() if config['SQLITE_INIT_SCHEMA']: + print('Init db schema') cur.executescript(open(config['DB_SCHEMA']).read()) if config['SQLITE_INIT_DATA'] and created: + print('Init db data') cur.executescript(open(config['DB_DATA']).read()) db.commit() db.close() diff --git a/legacy.py b/legacy.py index 0fcfab26c10643448645ac47e5e544caa3901add..14beb0fd934aa6c812ed38505cb5fdf9d0bd8c6c 100644 --- a/legacy.py +++ b/legacy.py @@ -10,6 +10,8 @@ def legacy_index(): if not courses: return "Not found", 404 return redirect(url_for('lecture', course=courses[0]['handle'], id=request.args['lectureid']),code=302) + if request.args['view'] == 'faq': + return redirect(url_for('faq'),code=302) return None @app.route('/site/') diff --git a/scheduler.py b/scheduler.py index 5bd30e949d32daa6e6e46b804b1b3c4ab7e0c66c..92275055bcde6677fc90520baafcee9b6f17ef5f 100644 --- a/scheduler.py +++ b/scheduler.py @@ -27,5 +27,5 @@ def sched_func(delay, priority=0, firstdelay=None, args=[], kargs={}): print("Scheduler: registered {} (frequency 1/{}s, start delay: {}s)".format(func.__name__, delay, firstdelay)) return func return wrapper - -threading.Thread(target=run_scheduler, daemon=True).start() +if not app.config['DISABLE_SCHEDULER']: + threading.Thread(target=run_scheduler, daemon=True).start() diff --git a/server.py b/server.py index d99860cd6ca9e07382ea18c645a36ae6d64a01b3..8ece609dd1667136ed89aacbfa86ace040b85048 100644 --- a/server.py +++ b/server.py @@ -25,6 +25,17 @@ if sys.argv[0].endswith('run.py'): config['SQLITE_INIT_DATA'] = True config['DEBUG'] = True config.from_pyfile('config.py', silent=True) +if sys.argv[0].endswith('tests.py'): + print('running in test mode') + import tempfile + # ensure we always use a clean sqlite db for tests + config['DB_ENGINE'] = 'sqlite' + config['SQLITE_DB'] = tempfile.mktemp(prefix='flasktestingtmp') + print('DB File: {}'.format(config['SQLITE_DB'])) + config['SQLITE_INIT_DATA'] = True + config['SQLITE_INIT_SCHEMA'] = True + config['DEBUG'] = True + config['DISABLE_SCHEDULER'] = True if config['DEBUG']: app.jinja_env.auto_reload = True @@ -338,7 +349,7 @@ def login(): user = userinfo.get('uid') if not check_mod(user, groups): flash('Login fehlgeschlagen!') - return render_template('login.html') + return make_response(render_template('login.html'), 403) session['user'] = userinfo dbuser = query('SELECT * FROM users WHERE name = ?', user) if not dbuser: diff --git a/tests.py b/tests.py new file mode 100755 index 0000000000000000000000000000000000000000..298f20b2eaa4c9060865726f4cbdf1894c576fa5 --- /dev/null +++ b/tests.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +import os +import unittest +import server + +import flask +from flask import url_for + +class VideoTestCase(unittest.TestCase): + @classmethod + def tearDownClass(cls): + os.unlink(server.app.config['SQLITE_DB']) + + def setUp(self): + server.app.testing = True + self.app = server.app.test_client() + + def login(self, c): + with c.session_transaction() as sess: + sess['user'] = {'name': 'videoag', '_csrf_token': 'asd', 'dbid': 72} + sess['_csrf_token'] = 'asd' + + def test_index(self): + with self.app as c: + r = c.get('/') + assert r.status_code == 200 + + self.login(c) + r = c.get('/') + assert r.status_code == 200 + + def test_courses(self): + with self.app as c: + r = c.get('/courses') + assert r.status_code == 200 + + self.login(c) + r = c.get('/courses') + assert r.status_code == 200 + + def test_course(self): + with self.app as c: + # normal + r = c.get('/15ws-afi') + assert r.status_code == 200 + + # not listed + r = c.get('/15ws-bio') + assert r.status_code == 200 + + # not visible + r = c.get('/15ws-einfprog') + assert r.status_code == 404 + + self.login(c) + r = c.get('/15ws-afi') + assert r.status_code == 200 + r = c.get('/15ws-einfprog') + assert r.status_code == 200 + + def test_timetable(self): + with self.app as c: + r = c.get('/internal/timetable') + assert r.status_code == 302 + + self.login(c) + r = c.get('internal/timetable') + assert r.status_code == 200 + r = c.get('internal/timetable?date=2016-W05') + assert r.status_code == 200 + assert 'AfI' in r.data.decode() + assert 'Progra' in r.data.decode() + assert 'Bio' in r.data.decode() + + def test_faq(self): + r = self.app.get('/faq') + assert r.status_code == 200 + + def test_ical(self): + with self.app as c: + r = c.get('/internal/ical/all') + assert r.status_code == 401 + + self.login(c) + r = c.get('/internal/ical/all') + assert r.status_code == 200 + assert 'Progra' in r.data.decode() + assert 'Vorlesung' in r.data.decode() + + def test_sitemap(self): + r = self.app.get('/sitemap.xml') + assert r.status_code == 200 + + def test_chapters(self): + with self.app as c: + # wrong time format + r = c.post('/internal/newchapter/7011', data={'text':'testchapter A', 'time': 1234}) + assert r.status_code == 400 + # should be inserted as id 15 + r = c.post('/internal/newchapter/7011', data={'text':'testchapter B', 'time': '00:10:00'}) + assert r.status_code == 200 + # not yet set visible + r = c.get('/internal/chapters/7011') + assert r.status_code == 404 + # other lecture + r = c.get('/internal/chapters/7012') + assert r.status_code == 404 + + self.login(c) + r = c.post('/internal/edit', data={"chapters.15.visible":1,"_csrf_token":"asd"}) + assert r.status_code == 200 + r = c.get('/internal/chapters/7011') + assert 'testchapter B' in r.data.decode() and not 'testchapter A' in r.data.decode() + + def test_search(self): + r = self.app.get('/search?q=Malo') + assert r.status_code == 200 + assert 'Mathematische Logik II' in r.data.decode() and '4.1 Der Sequenzenkalkül' in r.data.decode() + r = self.app.get('/search?q=Afi+Stens') + assert r.status_code == 200 + assert 'Analysis für Informatiker' in r.data.decode() and 'Höhere Mathematik I' in r.data.decode() + + def test_login(self): + # test login page + r = self.app.get('/internal/login') + assert r.status_code == 200 + # test successfull login + with self.app as c: + r = c.post('/internal/login', data={'user': 'videoag', 'password': 'videoag', 'ref': '/'}) + assert flask.session['user'] + assert r.status_code == 302 + assert '<a href="/">' in r.data.decode() + # test unsuccessfull login + with self.app as c: + r = c.post('/internal/login', data={'user': 'videoag', 'password': 'asd', 'ref': '/'}) + assert flask.session['user'] + assert r.status_code == 403 + assert 'Login fehlgeschlagen' in r.data.decode() + + def test_logout(self): + with self.app as c: + with c.session_transaction() as sess: + sess['user'] = {'foo': 'bar'} + r = c.get('/internal/logout', data={'ref': '/'}) + assert not flask.session.get('user') + assert r.status_code == 302 + assert '<a href="/">' in r.data.decode() + + def test_edit(self): + with self.app as c: + # test auth + r = c.get('/internal/new/courses', data={'title': 'Neue Vera14352345nstaltung totalyrandomcrap', 'responsible': 'foo', 'handle': '2r5sQ46z4w3DFCRT3<F4>DG', '_csrf_token': 'asd'}) + assert r.status_code != 200 + + r = c.get('/internal/changelog') + assert r.status_code == 302 + + # all other tests are done logged in + self.login(c) + + # add course + r = c.get('/internal/new/courses', data={'title': 'Neue Veranstaltung totalyrandomcrap', 'responsible': 'foo', 'handle': '2r5sQDFCRT3DG', '_csrf_token': 'asd'}) + assert r.status_code == 200 + r = self.app.get('/courses') + assert r.status_code == 200 + assert 'Neue Veranstaltung totalyrandomcrap' in r.data.decode() and '2r5sQDFCRT3DG' in r.data.decode() + + # rename lecture + r = c.get('internal/edit', data={"lectures.7353.title":"Testtitle","_csrf_token":"asd"}) + assert r.status_code == 200 + + # test if the changelog is written + r = c.get('/internal/changelog') + assert r.status_code == 200 + assert 'Testtitle' in r.data.decode() and 'lectures.7353.title' in r.data.decode() + + + def test_legacyurl(self): + with self.app as c: + r = self.app.get('/site/feed.php?newcourses') + assert r.status_code == 302 + assert url_for('courses_feed') in r.data.decode() + + r = self.app.get('/?view=faq') + assert r.status_code == 302 + assert url_for('faq') in r.data.decode() + + r = self.app.get('/site/feed.php?all') + assert r.status_code == 302 + assert url_for('feed') in r.data.decode() + + r = self.app.get('/?course=16ws-progra') + assert r.status_code == 302 + assert url_for('course', handle='16ws-progra') in r.data.decode() + + r = self.app.get('/?view=player&lectureid=7319') + assert r.status_code == 302 + assert url_for('lecture', id='7319', course='16ws-progra') in r.data.decode() + + r = self.app.get('/site/feed.php?16ws-afi') + assert r.status_code == 302 + assert url_for('feed', handle='16ws-afi') in r.data.decode() + + r = self.app.get('/site/feed.php?lecture=7319') + assert r.status_code == 302 + assert False # missing + + r = self.app.get('/site/feed.php?vid=6088') + assert r.status_code == 302 + assert False # missing + +if __name__ == '__main__': + unittest.main() +