diff --git a/.coverage b/.coverage new file mode 100644 index 000000000..e361004f7 Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..16fd435e9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +omit = + lib/ + bin/ + */__init__.py + tests/* diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..905ee7c71 --- /dev/null +++ b/.flake8 @@ -0,0 +1,12 @@ +[flake8] +max-line-length = 80 +exclude = + bin/activate.py, + lib, + packages, + migrations, + build, + dist, + *.pyc, + __pycache__, + bin/activate_this.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2cba99d87..824b7e2da 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +pyvenv.cfg +.DS_Store diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..627a23c90 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 80 \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..9627e15b7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] + +testpaths = tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 139affa05..7017fbc8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,6 @@ itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 Werkzeug==1.0.1 +pytest +black +flake8 \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..7719a27b8 100644 --- a/server.py +++ b/server.py @@ -1,59 +1,84 @@ import json -from flask import Flask,render_template,request,redirect,flash,url_for +from flask import Flask, render_template, request, redirect, flash, url_for def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs + with open("clubs.json") as c: + listOfClubs = json.load(c)["clubs"] + return listOfClubs def loadCompetitions(): - with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions + with open("competitions.json") as comps: + listOfCompetitions = json.load(comps)["competitions"] + return listOfCompetitions app = Flask(__name__) -app.secret_key = 'something_special' +app.secret_key = "something_special" competitions = loadCompetitions() clubs = loadClubs() -@app.route('/') + +@app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") + + +def get_club_from_email(email): + try: + club = [ + club for club in clubs if club["email"] == email + ][0] + return club + except IndexError: + return None -@app.route('/showSummary',methods=['POST']) + +@app.route("/showSummary", methods=["POST"]) def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) + club = get_club_from_email(request.form["email"]) + if club: + return render_template("welcome.html", club=club, + competitions=competitions) + else: + flash("Sorry, that email wasn't found.") + return redirect(url_for("index")) -@app.route('/book//') -def book(competition,club): - foundClub = [c for c in clubs if c['name'] == club][0] - foundCompetition = [c for c in competitions if c['name'] == competition][0] +@app.route("/book//") +def book(competition, club): + foundClub = [c for c in clubs if c["name"] == club][0] + foundCompetition = [c for c in competitions if c["name"] == competition][0] if foundClub and foundCompetition: - return render_template('booking.html',club=foundClub,competition=foundCompetition) + return render_template( + "booking.html", club=foundClub, competition=foundCompetition + ) else: flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template( + "welcome.html", club=club, competitions=competitions + ) -@app.route('/purchasePlaces',methods=['POST']) +@app.route("/purchasePlaces", methods=["POST"]) def purchasePlaces(): - competition = [c for c in competitions if c['name'] == request.form['competition']][0] - club = [c for c in clubs if c['name'] == request.form['club']][0] - placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired - flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) + competition = [ + c for c in competitions if c["name"] == request.form["competition"] + ][0] + club = [c for c in clubs if c["name"] == request.form["club"]][0] + placesRequired = int(request.form["places"]) + competition["numberOfPlaces"] = ( + int(competition["numberOfPlaces"]) - placesRequired + ) + flash("Great-booking complete!") + return render_template("welcome.html", club=club, competitions=competitions) # TODO: Add route for points display -@app.route('/logout') +@app.route("/logout") def logout(): - return redirect(url_for('index')) \ No newline at end of file + return redirect(url_for("index")) diff --git a/templates/index.html b/templates/index.html index 926526b7d..0d9e52c9f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,3 +1,4 @@ + @@ -6,10 +7,20 @@

Welcome to the GUDLFT Registration Portal!

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + Please enter your secretary email to continue:
- +
diff --git a/templates/welcome.html b/templates/welcome.html index ff6b261a2..db7c1e47e 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -7,7 +7,7 @@

Welcome, {{club['email']}}

Logout - {% with messages = get_flashed_messages()%} + {% with messages = get_flashed_messages() %} {% if messages %}
    {% for message in messages %} @@ -15,6 +15,7 @@

    Welcome, {{club['email']}}

    Logout {% endfor %}
{% endif%} + Points available: {{club['points']}}

Competitions:

    @@ -30,7 +31,6 @@

    Competitions:


    {% endfor %}
- {%endwith%} - + {% endwith %} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py new file mode 100644 index 000000000..4cf8514f0 --- /dev/null +++ b/tests/integration_tests/conftest.py @@ -0,0 +1,48 @@ +import json +import pytest +from unittest.mock import mock_open, patch + +# Mock data for clubs.json and competitions.json +mock_clubs_json = json.dumps( + { + "clubs": [ + {"name": "Club 1", "email": "club1@example.com"}, + {"name": "Club 2", "email": "club2@example.com"}, + ] + } +) + +mock_competitions_json = json.dumps( + { + "competitions": [ + {"name": "Competition 1", "numberOfPlaces": "25"}, + {"name": "Competition 2", "numberOfPlaces": "15"}, + ] + } +) + + +def mocked_open(file, *args, **kwargs): + """ + Mock open function to return mock data for + clubs.json and competitions.json + """ + if file == "clubs.json": + return mock_open(read_data=mock_clubs_json)() + elif file == "competitions.json": + return mock_open(read_data=mock_competitions_json)() + else: + raise FileNotFoundError(f"File {file} not found") + + +# Patch open before importing the app to ensure clubs +# and competitions are loaded with mock data +with patch("builtins.open", side_effect=mocked_open): + from server import app # Import app after patching + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client diff --git a/tests/integration_tests/test_unknown_email.py b/tests/integration_tests/test_unknown_email.py new file mode 100644 index 000000000..f77e75899 --- /dev/null +++ b/tests/integration_tests/test_unknown_email.py @@ -0,0 +1,24 @@ + +# Integration test for valid email +def test_showSummary_valid_email_integration(client): + # Simulate POST request with a valid email + response = client.post("/showSummary", data={"email": "club1@example.com"}) + + # Check if the welcome page is rendered with the correct club data + assert response.status_code == 200 + assert b"Welcome" in response.data + + +# Integration test for invalid email +def test_showSummary_invalid_email_integration(client): + # Simulate POST request with an invalid email + response = client.post( + "/showSummary", data={"email": "invalid@example.com"} + ) + + # Validate that the response redirects to the index page + assert response.status_code == 302 + response = client.get(response.headers["Location"]) # Follow the redirect + + # Check if the flash message appears in the redirected page + assert b"Sorry, that email wasn't found." in response.data diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py new file mode 100644 index 000000000..1c3ff9b09 --- /dev/null +++ b/tests/unit_tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +from server import app + + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client diff --git a/tests/unit_tests/test_unknown_email.py b/tests/unit_tests/test_unknown_email.py new file mode 100644 index 000000000..d0bd5446e --- /dev/null +++ b/tests/unit_tests/test_unknown_email.py @@ -0,0 +1,101 @@ +import json +from unittest.mock import mock_open, patch + +from server import get_club_from_email, loadClubs, loadCompetitions +from .utils import ( + mock_clubs_list, + mock_competitions_list, + mock_clubs_json, + mock_competitions_json, +) + + +# Test for loadClubs function +@patch("builtins.open", new_callable=mock_open, read_data=mock_clubs_json) +@patch("server.json.load") +def test_loadClubs(mock_json_load, mock_file): + # Mock json.load to return the parsed data + mock_json_load.return_value = json.loads(mock_clubs_json) + + # Call loadClubs and check if it returns the correct data + clubs = loadClubs() + assert len(clubs) == 2 + assert clubs[0]["name"] == "Club 1" + assert clubs[1]["email"] == "club2@example.com" + + +# Test for loadCompetitions function +@patch( + "builtins.open", new_callable=mock_open, read_data=mock_competitions_json +) +@patch("server.json.load") +def test_loadCompetitions(mock_json_load, mock_file): + # Mock json.load to return the parsed data + mock_json_load.return_value = json.loads(mock_competitions_json) + + # Call loadCompetitions and check if it returns the correct data + competitions = loadCompetitions() + assert len(competitions) == 2 + assert competitions[0]["name"] == "Competition 1" + assert competitions[1]["numberOfPlaces"] == "15" + + +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_email_valid(): + club = get_club_from_email("club1@example.com") + assert club is not None + assert club["name"] == "Club 1" + + +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_email_invalid(): + club = get_club_from_email("invalid@example.com") + assert club is None + + +# Unit test for showSummary with valid email +@patch( + "server.competitions", mock_competitions_list +) # Directly patch the value of competitions +@patch("server.get_club_from_email") +@patch("server.render_template") +def test_showSummary_valid_email(mock_render_template, mock_get_club, client): + # Mock get_club_from_email to return a valid club + mock_get_club.return_value = mock_clubs_list[0] + + # Simulate POST request with a valid email + response = client.post("/showSummary", data={"email": "club1@example.com"}) + + # Check if render_template was called with the correct arguments + mock_render_template.assert_called_once_with( + "welcome.html", + club=mock_clubs_list[0], + competitions=mock_competitions_list, + ) + assert response.status_code == 200 + + +# Unit test for showSummary with invalid email +@patch("server.get_club_from_email") +@patch("server.flash") +@patch("server.redirect") +@patch("server.url_for") +def test_showSummary_invalid_email( + mock_url_for, mock_redirect, mock_flash, mock_get_club, client +): + # Mock get_club_from_email to return None (simulating an invalid email) + mock_get_club.return_value = None + + # Mock url_for to return a URL for the index page + mock_url_for.return_value = "/" + + # Simulate POST request with an invalid email + client.post( + "/showSummary", data={"email": "invalid@example.com"} + ) + + # Check if flash was called with the correct message + mock_flash.assert_called_once_with("Sorry, that email wasn't found.") + + # Check if redirect was called with the correct arguments + mock_redirect.assert_called_once_with("/") diff --git a/tests/unit_tests/utils.py b/tests/unit_tests/utils.py new file mode 100644 index 000000000..3a095deab --- /dev/null +++ b/tests/unit_tests/utils.py @@ -0,0 +1,23 @@ + +import json + + +# Mock data for clubs and competitions +mock_clubs_list = [ + {"name": "Club 1", "email": "club1@example.com"}, + {"name": "Club 2", "email": "club2@example.com"} +] + +mock_competitions_list = [ + {"name": "Competition 1", "numberOfPlaces": "25"}, + {"name": "Competition 2", "numberOfPlaces": "15"} +] + +# Mock data for clubs.json and competitions.json +mock_clubs_json = json.dumps({ + "clubs": mock_clubs_list +}) + +mock_competitions_json = json.dumps({ + "competitions": mock_competitions_list +})