diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..20668e8 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,44 @@ +name: Run tests + +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] + + 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 dependencies + run: | + python -m pip install --upgrade pip + pip install -r test_requirements.txt + - name: Create settings, whitelist, and log files + run: | + cp settings.py.template settings.py + cp whitelist.json.template whitelist.json + mkdir logs; touch logs/faculty-tools.log + - name: Lint with flake8 + run: flake8 + - name: Check formatting + uses: psf/black@stable + - name: Check import sorting + uses: jamescurtin/isort-action@master + - name: Lint markdown files + uses: bewuethr/mdl-action@v1 + - name: Run tests + run: coverage run -m unittest discover + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 802f960..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -matrix: - include: - - python: 3.7 - - python: 3.8 -install: -- pip install -r test_requirements.txt -- pip install coveralls -- gem install mdl -before_script: -- cp settings.py.template settings.py; cp whitelist.json.template whitelist.json -- mkdir logs; touch logs/faculty-tools.log -script: -- flake8 -- black --check . -- mdl . -- coverage run -m unittest discover -after_success: -- coveralls -notifications: - slack: - rooms: - secure: F3YANiuNHZjtbgiJC8M1JOKBKhct2EyDEkCitW4FMwrauV0G5XcWKqtqLyRzSchI1LUALn+Dnj032ElGEx7D7cJrXlqC700UjQ4wXv938GQObVnRfTVCVrsktpr5gf077dIXvwcYJYt9ZSu4uIyk+HqyNnHLX4qIL0zhFR8HytoOeXVdik35SQoLJLvorgf4EGfqU8Yo25LUJArp2AB7RceAiMg3QXmi+nDHumFFczURexaYXIDBrRYTyZgfpYP245HOUmEf/LD6G53e6FU+8hiISYr7nE0hTkNkj3U4WNga25//9VIdpjWW8VWd+G7vf8CuhzHYuWEtredoVqNnJDwKE/MLhixleA+1lAEUypAEp+0k3+zMfTk748gdl1buJ/kINjoNqjhLv6MtDH/YTw/eQKEXA+V+odoudDiHUCztHQbCaIXYmIDFnebzO9u/Gz7QJ7PfpBlmUASQru5qTFPL0tmHP/w6/zrog0n07+uwl4qo2d6qxslgtmw4+K0VxGXl1Z8STArEgD6a8KoTZ1N8XDFF0E7KXE3kGYEtLHNoV7Z9OaohB3AFwJaKPTytYyIQ+OPvzYmpyUwRGIWBfRIP7t9qemyOExbYoinw0rF7jCMm7T3gLxkwxHNOCt+Gc1kwfLvuDy8QhQR2iHspMM8NeuDQKVzK4L3FeLx7bTo= diff --git a/lti.py b/lti.py index 3b98987..920db0e 100644 --- a/lti.py +++ b/lti.py @@ -1,28 +1,28 @@ -from logging import Formatter, INFO -from logging.handlers import RotatingFileHandler import json import os import time +from logging import INFO, Formatter +from logging.handlers import RotatingFileHandler +import jinja2 +import requests from canvasapi.exceptions import CanvasException from flask import ( Flask, + Response, + redirect, render_template, - session, request, - redirect, - url_for, - Response, send_from_directory, + session, + url_for, ) from flask_sqlalchemy import SQLAlchemy -import jinja2 from pylti.flask import lti -import requests from requests.exceptions import HTTPError -from utils import filter_tool_list, slugify import settings +from utils import filter_tool_list, slugify app = Flask(__name__) app.config.from_object(settings.configClass) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7707c56 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 88 +target_version = ['py37', 'py38', 'py39'] +exclude = "src" + +[tool.isort] +profile = "black" +skip = "src" diff --git a/requirements.txt b/requirements.txt index b4f768f..9902bfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -canvasapi==0.15.0 -Flask==1.1.1 -Flask-SQLAlchemy==2.4.1 +canvasapi==3.0.0 +Flask==2.2.2 +Flask-SQLAlchemy==3.0.2 mysqlclient --e git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI +git+https://github.com/ucfcdl/pylti.git@roles#egg=PyLTI requests==2.22.0 Werkzeug>=1.0.1 # Chrome 80 SameSite fix diff --git a/settings.py.template b/settings.py.template index ce34aac..e0221f8 100644 --- a/settings.py.template +++ b/settings.py.template @@ -17,11 +17,7 @@ SHARED_SECRET = "secret" # Configuration for pylti library. Uses the above key and secret PYLTI_CONFIG = { - "consumers": { - CONSUMER_KEY: { - "secret": SHARED_SECRET - } - }, + "consumers": {CONSUMER_KEY: {"secret": SHARED_SECRET}}, # Custom configurable roles "roles": { "staff": [ diff --git a/setup.sh b/setup.sh index b586eda..786baf0 100644 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,2 @@ -source env/bin/activate export FLASK_APP=lti.py -export FLASK_ENV=development +export FLASK_DEBUG=1 diff --git a/test_requirements.txt b/test_requirements.txt index 7330fb4..b89f9d0 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -5,6 +5,7 @@ blinker coverage flake8 Flask-Testing>=0.8.0 +isort mock oauthlib requests-mock diff --git a/tests.py b/tests.py index c7c09e4..b70ed38 100644 --- a/tests.py +++ b/tests.py @@ -1,18 +1,18 @@ -from json.decoder import JSONDecodeError import logging +import time import unittest +from json.decoder import JSONDecodeError from urllib.parse import urlencode import canvasapi -import oauthlib.oauth1 import flask -from flask import url_for import flask_testing +import oauthlib.oauth1 import requests_mock +from flask import url_for +from mock import mock_open, patch from pylti.common import LTI_SESSION_KEY -import time -from mock import patch, mock_open import lti import settings import utils @@ -86,6 +86,18 @@ def generate_launch_request( new_url = signed_url[len(base_url) :] return new_url + def assert_redirects_new(self, response, location, message=None): + valid_status_codes = (301, 302, 303, 305, 307) + valid_status_code_str = ", ".join(str(code) for code in valid_status_codes) + not_redirect = "HTTP Status %s expected but got %d" % ( + valid_status_code_str, + response.status_code, + ) + self.assertTrue( + response.status_code in valid_status_codes, message or not_redirect + ) + self.assertEqual(response.location, location, message) + def test_select_theme_dirs(self, m): theme_dirs = lti.select_theme_dirs() @@ -257,7 +269,7 @@ def test_index_api_key_expired(self, m): redirect_url = ( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}" ) - self.assert_redirects( + self.assert_redirects_new( response, redirect_url.format( settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri @@ -297,7 +309,7 @@ def test_index_api_key_404(self, m): redirect_url = ( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}" ) - self.assert_redirects( + self.assert_redirects_new( response, redirect_url.format( settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri @@ -592,7 +604,7 @@ def test_oauth_login_new_user(self, m): ) ) - self.assert_redirects(response, url_for("index")) + self.assert_redirects_new(response, url_for("index")) # Check that user is created user = lti.Users.query.filter_by( @@ -701,7 +713,7 @@ def test_oauth_login_existing_user(self, m): ) ) - self.assert_redirects(response, url_for("index")) + self.assert_redirects_new(response, url_for("index")) self.assertGreater(user.expires_in, old_expire) def test_oauth_login_existing_user_db_error(self, m): @@ -905,7 +917,7 @@ def test_auth_no_user(self, m): redirect_url = ( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}" ) - self.assert_redirects( + self.assert_redirects_new( response, redirect_url.format( settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri @@ -950,7 +962,7 @@ def test_auth_no_api_key_refresh_success(self, m, mock_refresh_access_token): self.assertEqual(flask.session["api_key"], new_access_token) self.assertEqual(flask.session["expires_in"], new_expiry_date) - self.assert_redirects(response, url_for("index")) + self.assert_redirects_new(response, url_for("index")) @patch("lti.refresh_access_token") def test_auth_no_api_key_refresh_fail(self, m, mock_refresh_access_token): @@ -987,7 +999,7 @@ def test_auth_no_api_key_refresh_fail(self, m, mock_refresh_access_token): redirect_url = ( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}" ) - self.assert_redirects( + self.assert_redirects_new( response, redirect_url.format( settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri @@ -1038,7 +1050,7 @@ def test_auth_invalid_api_key_refresh_success(self, m, mock_refresh_access_token data=payload, ) - self.assertRedirects(response, url_for("index")) + self.assert_redirects_new(response, url_for("index")) @patch("lti.refresh_access_token") def test_auth_invalid_api_key_refresh_fail(self, m, mock_refresh_access_token): @@ -1085,7 +1097,7 @@ def test_auth_invalid_api_key_refresh_fail(self, m, mock_refresh_access_token): redirect_url = ( "{}login/oauth2/auth?client_id={}&response_type=code&redirect_uri={}" ) - self.assert_redirects( + self.assert_redirects_new( response, redirect_url.format( settings.BASE_URL, settings.oauth2_id, settings.oauth2_uri @@ -1123,7 +1135,7 @@ def test_auth(self, m): data=payload, ) - self.assert_redirects(response, url_for("index")) + self.assert_redirects_new(response, url_for("index")) # get_sessionless_url def test_get_sessionless_url_is_course_nav_fail(self, m): diff --git a/utils.py b/utils.py index aca6e28..b7dcc33 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,6 @@ -from collections import defaultdict import json import re +from collections import defaultdict from canvasapi import Canvas