From 73c69c078b402b3b1e713c9d3fc752fdb8be27b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Gr=C3=B8n=C3=A5s?= <44143748+sondregronas@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:51:33 +0200 Subject: [PATCH] Move app inside an app factory - setup tests --- .github/workflows/CI.yml | 40 ++++++ BookingSystem/__init__.py | 16 --- BookingSystem/app.py | 271 +++++++++++++++++++------------------- BookingSystem/db.py | 2 +- tests/test_app.py | 30 +++++ 5 files changed, 207 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/CI.yml create mode 100644 tests/test_app.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..f0c5b1c --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + test: + strategy: + max-parallel: 6 + matrix: + os: [ ubuntu-latest ] + python-version: [ 3.7, 3.8, 3.9, '3.10', '3.11' ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + + - name: Run unit tests + run: | + pip install -r requirements.txt + python -m pytest tests/ --cov=FeideUtstyrbase --cov-report=xml + + - uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml \ No newline at end of file diff --git a/BookingSystem/__init__.py b/BookingSystem/__init__.py index 46539ce..557639f 100644 --- a/BookingSystem/__init__.py +++ b/BookingSystem/__init__.py @@ -2,10 +2,7 @@ from logging import Logger, StreamHandler, Formatter, FileHandler from pathlib import Path -import flask from dotenv import load_dotenv -from flask_session import Session -from werkzeug.middleware.proxy_fix import ProxyFix dotenv_path = Path('.env') if dotenv_path.exists(): @@ -20,19 +17,6 @@ KIOSK_FQDN = os.getenv('KIOSK_FQDN') API_TOKEN = os.getenv('API_TOKEN') -# Flask app setup -app = flask.Flask(__name__, template_folder='templates', static_folder='static') -app.secret_key = os.getenv('SECRET_KEY') -app.config['SESSION_TYPE'] = 'filesystem' -app.config['PERMANENT_SESSION_LIFETIME'] = 3600 -if os.getenv('DEBUG') == 'True': - app.debug = True - -# We're behind a reverse proxy, so we need to fix the scheme and host -app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) - -Session(app) - # Logger setup logger = Logger(__name__) logger.setLevel('DEBUG') diff --git a/BookingSystem/app.py b/BookingSystem/app.py index 1347bfe..3adfb06 100644 --- a/BookingSystem/app.py +++ b/BookingSystem/app.py @@ -1,7 +1,10 @@ +import os from datetime import datetime import flask from dateutil import parser +from flask_session import Session +from werkzeug.middleware.proxy_fix import ProxyFix import api import feide @@ -9,150 +12,148 @@ import inventory import mail import user -from __init__ import app, logger, KIOSK_FQDN, LABEL_SERVER +from __init__ import logger, KIOSK_FQDN, LABEL_SERVER from db import init_db, add_admin from utils import login_required -app.register_blueprint(api.api) -app.register_blueprint(feide.feide) +def create_app() -> flask.Flask: + # Flask app setup + app = flask.Flask(__name__, template_folder='templates', static_folder='static') + app.secret_key = os.getenv('SECRET_KEY') + app.config['SESSION_TYPE'] = 'filesystem' + app.config['PERMANENT_SESSION_LIFETIME'] = 3600 + if os.getenv('DEBUG') == 'True': + app.debug = True + + # We're behind a reverse proxy, so we need to fix the scheme and host + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) + + Session(app) + + app.register_blueprint(api.api) + app.register_blueprint(feide.feide) + + @app.template_filter('strftime') + def _jinja2_filter_datetime(date, fmt='%d.%m.%Y') -> str: + return parser.parse(date).strftime(fmt) + + @app.template_filter('strfunixtime') + def _jinja2_filter_strftime(date, fmt='%d.%m.%Y') -> str: + return datetime.fromtimestamp(float(date)).strftime(fmt) + + @app.errorhandler(401) + def unauthorized(e) -> flask.Response: + logger.warning(f'Unauthorized access: {flask.request.url} from {flask.request.remote_addr}') + return flask.redirect(flask.url_for('login')) + + @app.errorhandler(403) + def unauthorized(e) -> flask.Response: + flask.session.clear() + logger.warning(f'Unauthorized access: {flask.request.url} from {flask.request.remote_addr}') + return flask.redirect(flask.url_for('login')) + + @app.route('/') + @login_required() + def index() -> str: + if flask.session.get("user").is_admin: + return flask.render_template('index_admin.html', overdue_items=inventory.get_all_overdue()) + return flask.render_template('index_student.html', all_groups=groups.get_all()) + + @app.route('/login') + def login() -> str | flask.Response: + if KIOSK_FQDN and flask.request.headers.get('Host') == KIOSK_FQDN: + flask.session['method'] = 'kiosk' + r = flask.request.referrer + if r and r != flask.url_for('login'): + return flask.redirect(r) + return flask.redirect(flask.url_for('index')) + return flask.render_template('login.html') + + @app.route('/register') + @login_required() + def register() -> flask.Response: + user = flask.session.get("user") + if user.is_admin and not user.exists: + add_admin(flask.session.get("user").__dict__) -@app.template_filter('strftime') -def _jinja2_filter_datetime(date, fmt='%d.%m.%Y') -> str: - return parser.parse(date).strftime(fmt) - - -@app.template_filter('strfunixtime') -def _jinja2_filter_strftime(date, fmt='%d.%m.%Y') -> str: - return datetime.fromtimestamp(float(date)).strftime(fmt) - - -@app.errorhandler(401) -def unauthorized(e) -> flask.Response: - logger.warning(f'Unauthorized access: {flask.request.url} from {flask.request.remote_addr}') - return flask.redirect(flask.url_for('login')) - - -@app.errorhandler(403) -def unauthorized(e) -> flask.Response: - flask.session.clear() - logger.warning(f'Unauthorized access: {flask.request.url} from {flask.request.remote_addr}') - return flask.redirect(flask.url_for('login')) - - -@app.route('/') -@login_required() -def index() -> str: - if flask.session.get("user").is_admin: - return flask.render_template('index_admin.html', overdue_items=inventory.get_all_overdue()) - return flask.render_template('index_student.html', all_groups=groups.get_all()) - - -@app.route('/login') -def login() -> str | flask.Response: - if KIOSK_FQDN and flask.request.headers.get('Host') == KIOSK_FQDN: - flask.session['method'] = 'kiosk' - r = flask.request.referrer - if r and r != flask.url_for('login'): - return flask.redirect(r) return flask.redirect(flask.url_for('index')) - return flask.render_template('login.html') - - -@app.route('/register') -@login_required() -def register() -> flask.Response: - user = flask.session.get("user") - if user.is_admin and not user.exists: - add_admin(flask.session.get("user").__dict__) - - return flask.redirect(flask.url_for('index')) - - -@app.route('/logout') -def logout() -> flask.Response: - flask.session.clear() - return flask.redirect(flask.url_for('index')) - - -@app.route('/admin') -@login_required(admin_only=True) -def admin_settings() -> str: - return flask.render_template('admin_settings.html', all_groups=groups.get_all(), - all_categories=inventory.all_categories(), - all_emails=mail.get_all_emails(), - last_sent=mail.get_last_sent()) - - -@app.route('/audits') -@login_required(admin_only=True) -def audits() -> str: - """All audits in data/audits.log""" - log = open('data/audits.log', 'r').readlines() - log = [{ - 'timestamp': audit.split('|')[0].strip(), - 'event': audit.split('|')[1].split(' - ')[0].strip(), - 'message': ''.join(audit.split(' - ')[1:]).strip() - } for audit in log if audit.strip()] - return flask.render_template('audits.html', audits=log) - - -@app.route('/inventar') -@login_required(admin_only=True) -def inventar() -> str: - return flask.render_template('inventar.html', items=inventory.get_all()) - - -@app.route('/inventar/add') -@login_required(admin_only=True) -def inventar_add() -> str: - return flask.render_template('inventar_add.html', categories=inventory.all_categories()) - - -@app.route('/inventar/edit/') -@login_required(admin_only=True) -def edit_item(item_id: str) -> str: - return flask.render_template('inventar_edit.html', item=inventory.get(item_id), - categories=inventory.all_categories()) - - -@app.route('/inventar/print/') -@login_required(admin_only=True) -def print_item(item_id: str) -> str: - return flask.render_template('inventar_print.html', item=inventory.get(item_id)) - - -@app.route('/booking') -@login_required(admin_only=True) -def booking() -> str: - return flask.render_template('booking.html', - all_users=user.get_all_active_users(), - all_items=inventory.get_all()) - - -@app.route('/innlevering') -@login_required(admin_only=True) -def innlevering() -> str: - return flask.render_template('innlevering.html', - unavailable_items=inventory.get_all_unavailable()) - - -@app.route('/etikettserver') -@login_required(admin_only=True) -def labelserver() -> str: - return flask.render_template('labelserver.html', labelserver_url=LABEL_SERVER) - - -@app.route('/ansvarsavtale') -def responsibility() -> str: - return flask.render_template('responsibility.html') + @app.route('/logout') + def logout() -> flask.Response: + flask.session.clear() + return flask.redirect(flask.url_for('index')) -@app.route('/personvern') -def privacy() -> str: - return flask.render_template('privacy.html') + @app.route('/admin') + @login_required(admin_only=True) + def admin_settings() -> str: + return flask.render_template('admin_settings.html', all_groups=groups.get_all(), + all_categories=inventory.all_categories(), + all_emails=mail.get_all_emails(), + last_sent=mail.get_last_sent()) + + @app.route('/audits') + @login_required(admin_only=True) + def audits() -> str: + """All audits in data/audits.log""" + log = open('data/audits.log', 'r').readlines() + log = [{ + 'timestamp': audit.split('|')[0].strip(), + 'event': audit.split('|')[1].split(' - ')[0].strip(), + 'message': ''.join(audit.split(' - ')[1:]).strip() + } for audit in log if audit.strip()] + return flask.render_template('audits.html', audits=log) + + @app.route('/inventar') + @login_required(admin_only=True) + def inventar() -> str: + return flask.render_template('inventar.html', items=inventory.get_all()) + + @app.route('/inventar/add') + @login_required(admin_only=True) + def inventar_add() -> str: + return flask.render_template('inventar_add.html', categories=inventory.all_categories()) + + @app.route('/inventar/edit/') + @login_required(admin_only=True) + def edit_item(item_id: str) -> str: + return flask.render_template('inventar_edit.html', item=inventory.get(item_id), + categories=inventory.all_categories()) + + @app.route('/inventar/print/') + @login_required(admin_only=True) + def print_item(item_id: str) -> str: + return flask.render_template('inventar_print.html', item=inventory.get(item_id)) + + @app.route('/booking') + @login_required(admin_only=True) + def booking() -> str: + return flask.render_template('booking.html', + all_users=user.get_all_active_users(), + all_items=inventory.get_all()) + + @app.route('/innlevering') + @login_required(admin_only=True) + def innlevering() -> str: + return flask.render_template('innlevering.html', + unavailable_items=inventory.get_all_unavailable()) + + @app.route('/etikettserver') + @login_required(admin_only=True) + def labelserver() -> str: + return flask.render_template('labelserver.html', labelserver_url=LABEL_SERVER) + + @app.route('/ansvarsavtale') + def responsibility() -> str: + return flask.render_template('responsibility.html') + + @app.route('/personvern') + def privacy() -> str: + return flask.render_template('privacy.html') + + return app if __name__ == '__main__': init_db() - app.run(host='0.0.0.0') + create_app().run(host='0.0.0.0') diff --git a/BookingSystem/db.py b/BookingSystem/db.py index cc16b0f..acea69a 100644 --- a/BookingSystem/db.py +++ b/BookingSystem/db.py @@ -5,7 +5,7 @@ def read_sql_query(sql_name) -> str: - return Path(f'sql/{sql_name}').read_text() + return Path(f'{Path(__file__).parent}/sql/{sql_name}').read_text() def add_admin(data_dict: dict) -> None: diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..733be14 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,30 @@ +import sys +from pathlib import Path + +import pytest + +sys.path.append(str(Path(__file__).parent.parent) + '\\BookingSystem') + +import app + +from db import init_db + + +@pytest.fixture +def client(): + flask_app = app.create_app() + flask_app.config['TESTING'] = True + init_db() + yield flask_app.test_client() + + +def test_index_unauthorized(client): + response = client.get('/') + assert response.status_code == 302 + assert '/login' in response.headers['Location'] + + +def test_login(client): + response = client.get('/login') + assert response.status_code == 200 + assert '/login/feide' in response.data.decode('utf-8')