diff --git a/.vscode/settings.json b/.vscode/settings.json index 49867c4..4e4a85c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,8 +4,9 @@ }, "python.formatting.provider": "none", "python.testing.pytestArgs": [ - "tests" + "tests", + "-s" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, } \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da416a0 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help +.PHONY: coverage deps help lint push test doc build + +deps: ## Install dependencies + pip install -r requirements.txt + +run: ## Install dependencies + python run.py + +devdeps: ## Install development dependencies + pip install black pip-audit flake8 coverage + +lint: ## Lint and static-check + black beflask + flake8 beflask + +push: ## Push code with tags + git push && git push --tags + +test: ## Run tests + pytest + +build: ## Build package + rm -fr dist/* + rm -fr build/* + python setup.py sdist bdist_wheel + +audit: ## Audit pip + pip-audit + +version: + @echo "Current Python version .....: $(shell python --version)" + @echo "Current Benchmark version ..: $(shell python -c "from benchmark import _version; print(_version.__version__)")" + +help: ## Show help message + @IFS=$$'\n' ; \ + help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##/:/'`); \ + printf "%s\n\n" "Usage: make [task]"; \ + printf "%-20s %s\n" "task" "help" ; \ + printf "%-20s %s\n" "------" "----" ; \ + for help_line in $${help_lines[@]}; do \ + IFS=$$':' ; \ + help_split=($$help_line) ; \ + help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ + help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ + printf '\033[36m'; \ + printf "%-20s %s" $$help_command ; \ + printf '\033[0m'; \ + printf "%s\n" $$help_info; \ + done diff --git a/beflask/forms.py b/beflask/forms.py index 1ec7887..8953f15 100644 --- a/beflask/forms.py +++ b/beflask/forms.py @@ -4,7 +4,6 @@ from wtforms import ( PasswordField, BooleanField, SubmitField, - SelectField, ) from wtforms.validators import ( DataRequired, diff --git a/beflask/interactive/forms.py b/beflask/interactive/forms.py index 67b793b..5b3ab86 100644 --- a/beflask/interactive/forms.py +++ b/beflask/interactive/forms.py @@ -3,7 +3,7 @@ from wtforms import SubmitField, SelectField, TextAreaField from benchmark.Arguments import ALL_METRICS -##### NOT USED ##### +# ----- NOT USED ----- # class RankingForm(FlaskForm): score = SelectField("Score", choices=ALL_METRICS) output = TextAreaField("Output") diff --git a/beflask/main.py b/beflask/main.py index 6e4afc7..6bffa2d 100644 --- a/beflask/main.py +++ b/beflask/main.py @@ -105,4 +105,6 @@ def login(): def logout(): if current_user.is_authenticated: logout_user() + else: + flash("You are not logged in.", "danger") return redirect(url_for(current_app.config["INDEX"])) diff --git a/beflask/models.py b/beflask/models.py index 963d6dc..518a536 100644 --- a/beflask/models.py +++ b/beflask/models.py @@ -18,9 +18,6 @@ class User(UserMixin, db.Model): date_created = db.Column(db.DateTime, default=db.func.now()) last_login = db.Column(db.DateTime, default=db.func.now()) - def __repr__(self): - return "".format(self.username, self.email) - def set_password(self, password): self.password_hash = generate_password_hash(password) diff --git a/dbseed.py b/dbseed.py index c83fd4a..374f4e0 100644 --- a/dbseed.py +++ b/dbseed.py @@ -1,7 +1,7 @@ from beflask.models import Benchmark, db, User from beflask import app -app = app.create_app() +_, app = app.create_app() with app.app_context(): db.drop_all() diff --git a/dbseed_docker.py b/dbseed_docker.py index 9b62da0..3c4bdf6 100644 --- a/dbseed_docker.py +++ b/dbseed_docker.py @@ -1,7 +1,7 @@ from beflask.models import Benchmark, db, User from beflask import app -app = app.create_app() +_, app = app.create_app() with app.app_context(): db.drop_all() diff --git a/pyproject.toml b/pyproject.toml index b9f1aac..8db9769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ branch = true source = ["beflask"] [tool.pytest.ini_options] +addopts = "--cov --cov-report html --cov-report term-missing" #--cov-fail-under 95 " testpaths = ["tests"] [tool.black] diff --git a/tests/conftest.py b/tests/conftest.py index 1060fab..cf4a54d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,20 +5,28 @@ from beflask.models import Benchmark, User, db class AuthActions(object): + guest_user = "guest" + guest_password = "patata" + def __init__(self, client): self._client = client def login( - self, username="guest", password="patata", follow_redirects=False + self, + username=None, + password=None, + follow_redirects=False, ): + username = username or self.guest_user + password = password or self.guest_password return self._client.post( "/login", data={"username": username, "password": password}, follow_redirects=follow_redirects, ) - def logout(self): - return self._client.get("/logout") + def logout(self, follow_redirects=False): + return self._client.get("/logout", follow_redirects=follow_redirects) @pytest.fixture @@ -27,11 +35,11 @@ def auth(client): @pytest.fixture -def app(): +def app(admin_user, admin_password): socketio, app = application.create_app("testing") app.test_client_class = FlaskLoginClient with app.app_context(): - db_seed(db) + db_seed(db, admin_user, admin_password) return socketio, app @@ -45,13 +53,33 @@ def runner(app): return app[1].test_cli_runner() -def db_seed(db): +@pytest.fixture +def admin_user(): + return "rmontanana" + + +@pytest.fixture +def admin_password(): + return "patito" + + +@pytest.fixture +def guest_user(): + return AuthActions.guest_user + + +@pytest.fixture +def guest_password(): + return AuthActions.guest_password + + +def db_seed(db, admin_user, admin_password): db.drop_all() db.create_all() b = Benchmark( name="discretizbench", folder="/Users/rmontanana/Code/discretizbench", - description="Experiments with local discretization and Bayesian " + description="Experiments with local discretization and Bayesian" "classifiers", ) db.session.add(b) @@ -68,19 +96,19 @@ def db_seed(db): ) db.session.add(b) u = User( - username="rmontanana", + username=admin_user, email="rmontanana@gmail.com", admin=True, benchmark_id=1, ) - u.set_password("patito") + u.set_password(admin_password) u1 = User( - username="guest", + username=AuthActions.guest_user, email="guest@example.com", admin=False, benchmark_id=1, ) - u1.set_password("patata") + u1.set_password(AuthActions.guest_password) db.session.add(b) db.session.add(u) db.session.add(u1) diff --git a/tests/test_login.py b/tests/test_login.py index 51c9765..bec1005 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,35 +1,78 @@ -import pytest -from urllib.parse import urlparse -from flask import session, g +from flask import session, g, url_for -def test_login(client, auth): - assert client.get("/login").status_code == 200 +def test_login(app, client, auth, admin_user, admin_password): + with app[1].test_request_context(): + url_login = url_for("main.login") + url_index = url_for("main.index") + + assert client.get(url_login).status_code == 200 response = auth.login() - assert response.headers["Location"] == "/index" + assert response.headers["Location"] == url_index auth.logout() - response = auth.login(username="rmontanana", password="patito") - assert response.headers["Location"] == "/index" - + response = auth.login(username=admin_user, password=admin_password) + assert response.headers["Location"] == url_index with client: - client.get("/index") + client.get(url_index) assert session["_user_id"] == "1" - assert g._login_user.username == "rmontanana" + assert g._login_user.username == admin_user + # Check if an already logged in user is redirected to index + assert client.get(url_login).status_code == 302 + response = client.get(url_login) + assert response.status_code == 302 + assert response.headers["Location"] == url_index auth.logout() -def test_login_invalid(client, auth): +def test_login_invalid(auth, admin_user): response = auth.login( - username="rmontanana", password="patato", follow_redirects=True + username=admin_user, password="wrong_password", follow_redirects=True ) assert b"Invalid username or password" in response.data assert response.status_code == 200 -def test_logout(client, auth): - auth.login() +def test_access_page_not_logged(client, app, auth, guest_user, guest_password): + with app[1].test_request_context(): + url_login = url_for("main.login") + url_config = url_for("main.config") + # Check if a not logged in user is redirected to login with next param + response = client.get(url_config) + header_login = f"{url_login}?next=%2Fconfig" + assert response.headers["Location"] == header_login + assert response.status_code == 302 + # Check if a not logged in user is redirected to login + response = client.get("/config", follow_redirects=True) + assert b"Please log in to access this page." in response.data + assert response.status_code == 200 + with client: + data = {"username": guest_user, "password": guest_password} + response = client.post(header_login, data=data, follow_redirects=True) + assert response.status_code == 200 + responses = { + "score": "Deafult score if none is provided", + "platform": "Name of the platform running benchmarks", + "model": "Default model used if none is provided", + } + for key, value in responses.items(): + assert bytes(key, "utf-8") in response.data + assert bytes(value, "utf-8") in response.data + +def test_logout_logged(client, auth): + response = auth.login() with client: auth.logout() assert "user_id" not in session + assert response.headers["Location"] == url_for("main.index") + + +def test_logout_not_logged(client, auth): + with client: + response = auth.logout() + assert response.status_code == 302 + assert "user_id" not in session + assert response.headers["Location"] == url_for("main.index") + response = auth.logout(follow_redirects=True) + assert b"You are not logged in." in response.data diff --git a/tests/test_main.py b/tests/test_main.py index 52638b6..ab5270b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,12 +1,104 @@ +from flask import url_for, g from beflask import app -def test_config(): - assert not app.create_app()[1].testing - assert app.create_app("testing")[1].testing +# def test_config_init(): +# assert not app.create_app()[1].testing +# assert app.create_app("testing")[1].testing def test_index(client): response = client.get("/") # check image is in the response assert b"img/robert-lukeman-_RBcxo9AU-U-unsplash.jpg" in response.data + + +def test_index_logged(client, auth): + assert auth.login(follow_redirects=True).status_code == 200 + with client: + response = client.get("/") + assert ( + b"img/robert-lukeman-_RBcxo9AU-U-unsplash.jpg" not in response.data + ) + assert ( + b"Experiments with local discretization and Bayesian" + in response.data + ) + assert b"discretizbench" in response.data + auth.logout() + + +def test_set_benchmark_guest(client, auth, app): + assert auth.login(follow_redirects=True).status_code == 200 + with app[1].test_request_context(): + url = url_for("main.set_benchmark", benchmark_id=1) + with client: + response = client.get(url, follow_redirects=True) + assert response.status_code == 200 + assert g._login_user.benchmark_id == 1 + assert b"Benchmark already selected." in response.data + response = client.get("/set_benchmark/2", follow_redirects=True) + assert g._login_user.benchmark_id == 1 + assert b"discretizbench" in response.data + assert b"You are not an admin." in response.data + auth.logout + + +def test_set_benchmark_admin(client, auth, app, admin_user, admin_password): + assert ( + auth.login( + username=admin_user, password=admin_password, follow_redirects=True + ).status_code + == 200 + ) + with app[1].test_request_context(): + url = url_for("main.set_benchmark", benchmark_id=1) + with client: + response = client.get(url, follow_redirects=True) + assert response.status_code == 200 + assert g._login_user.benchmark_id == 1 + assert b"Benchmark already selected." in response.data + response = client.get("/set_benchmark/2", follow_redirects=True) + assert g._login_user.benchmark_id == 2 + assert b"odtebench" in response.data + assert ( + b"Experiments with STree and Ensemble classifiers" + in response.data + ) + response = client.get("/set_benchmark/31", follow_redirects=True) + assert g._login_user.benchmark_id == 2 + assert b"odtebench" in response.data + assert ( + b"Experiments with STree and Ensemble classifiers" + in response.data + ) + assert b"Benchmark not found." in response.data + auth.logout + + +def test_config(app, client, auth): + explanations = { + "score": "Deafult score if none is provided", + "platform": "Name of the platform running benchmarks", + "model": "Default model used if none is provided", + "stratified": "Wether or not to split data in a stratified way", + "source_data": "Type of datasets", + "discretize": "Discretize of not datasets before training", + "fit_features": "Wheter or not to include features names in fit", + "seeds": "Seeds used to train/test models", + "nodes": "Label for nodes in report", + "leaves": "Label for leaves in report", + "depth": "Label for depth in report", + "margin": "Margin to add to ZeroR classifier in binary classes " + "datasets", + "framework": "HTML Framework default used in be_flask command", + } + assert auth.login(follow_redirects=True).status_code == 200 + with app[1].test_request_context(): + url = url_for("main.config") + with client: + response = client.get(url) + assert response.status_code == 200 + for key, value in explanations.items(): + assert bytes(key, "utf-8") in response.data + assert bytes(value, "utf-8") in response.data