diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 04a508e6f..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 2cba99d87..23264e70d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__.DS_Store +env/ +__pycache__/ +*.pyc +.DS_Store +.pytest_cache/ \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..de19c9f0b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 139affa05..8f10818fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ -click==7.1.2 -Flask==1.1.2 -itsdangerous==1.1.0 -Jinja2==2.11.2 -MarkupSafe==1.1.1 -Werkzeug==1.0.1 +click==8.1.7 +Flask==3.0.3 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +Werkzeug==3.0.4 +pytest==8.3.3 +pytest-cov==5.0.0 +locust==2.31.6 \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..a095ee333 100644 --- a/server.py +++ b/server.py @@ -20,14 +20,20 @@ def loadCompetitions(): competitions = loadCompetitions() clubs = loadClubs() +MAX_PLACES_PER_COMPETITION = 12 + @app.route('/') def index(): return render_template('index.html') @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) + matching_clubs = [c for c in clubs if c['email'] == request.form['email']] + if not matching_clubs: + flash("Sorry, that email was not found.") + return redirect(url_for('index')) + club = matching_clubs[0] + return render_template('welcome.html', club=club, competitions=competitions) @app.route('/book//') @@ -46,7 +52,22 @@ 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']) + + if placesRequired > MAX_PLACES_PER_COMPETITION: + flash("You cannot book more than 12 places per competition.") + return render_template('welcome.html', club=club, competitions=competitions) + + if placesRequired > int (competition['numberOfPlaces']): + flash("There are not enough places available in this competition") + return render_template('welcome.html', club=club, competitions=competitions) + + if placesRequired > int(club['points']): + flash("You do not have enough points to book that many places.") + return render_template('welcome.html', club=club, competitions=competitions) + + competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired + club['points'] = int(club['points']) - placesRequired flash('Great-booking complete!') return render_template('welcome.html', club=club, competitions=competitions) diff --git a/templates/index.html b/templates/index.html index 926526b7d..ecfe61c61 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,6 +5,15 @@ GUDLFT Registration + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %}

Welcome to the GUDLFT Registration Portal!

Please enter your secretary email to continue:
diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..cda79c157 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import pytest +import copy +import server + + +@pytest.fixture +def client(): + server.app.config['TESTING'] = True + with server.app.test_client() as client: + yield client + +@pytest.fixture(autouse=True) +def reset_data(): + clubs_sauvegarde = copy.deepcopy(server.clubs) + competition_sauvegarde = copy.deepcopy(server.competitions) + yield + server.clubs[:] = clubs_sauvegarde + server.competitions[:] = competition_sauvegarde \ No newline at end of file diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/test_login.py b/tests/integration/test_login.py new file mode 100644 index 000000000..c7cc014af --- /dev/null +++ b/tests/integration/test_login.py @@ -0,0 +1,19 @@ +def test_login_with_valid_email_shows_summary(client): + """Happy path : un email connu doit afficher la page de résumé.""" + response = client.post( + "/showSummary", + data={"email": "john@simplylift.co"}, + ) + assert response.status_code == 200 + assert b"john@simplylift.co" in response.data + + +def test_login_with_unknown_email_does_not_crash(client): + """Sad path (bug #1) : un email inconnu ne doit pas faire planter l'appli.""" + response = client.post( + "/showSummary", + data={"email": "inconnu@test.com"}, + follow_redirects=True, + ) + assert response.status_code == 200 + assert b"Sorry, that email was not found." in response.data \ No newline at end of file diff --git a/tests/integration/test_purchase.py b/tests/integration/test_purchase.py new file mode 100644 index 000000000..22d01d8e1 --- /dev/null +++ b/tests/integration/test_purchase.py @@ -0,0 +1,106 @@ +import server + + +def test_purchase_deducts_points_from_club(client): + """Bug #6 : les points utilises doivent etre deduits du solde du club.""" + # On choisit un club et une competition connus + club = next(c for c in server.clubs if c['name'] == "Simply Lift") + competition = next( + c for c in server.competitions if c['name'] == "Spring Festival" + ) + + points_avant = int(club['points']) + places_reservees = 3 + + client.post( + "/purchasePlaces", + data={ + "competition": competition['name'], + "club": club['name'], + "places": str(places_reservees), + }, + ) + + points_apres = int(club['points']) + assert points_apres == points_avant - places_reservees + +def test_purchase_more_than_points_is_blocked(client): + """Bug #2 : un club ne peut pas reserver plus de places qu'il n'a de points.""" + club = next(c for c in server.clubs if c['name'] == "Iron Temple") # 4 points + competition = next( + c for c in server.competitions if c['name'] == "Spring Festival" + ) + + points_avant = int(club['points']) + places_demandees = points_avant + 5 # plus que le solde : impossible + + response = client.post( + "/purchasePlaces", + data={ + "competition": competition['name'], + "club": club['name'], + "places": str(places_demandees), + }, + follow_redirects=True, + ) + + # Les points ne doivent pas avoir changé (rien n'a ete reserve) + assert int(club['points']) == points_avant + # Un message d'erreur doit apparaitre + assert b"do not have enough points" in response.data + +def test_purchase_more_than_12_places_is_blocked(client): + """Bug #3 : un club ne peut reserver plus de 12 places par competition""" + # On choisit un club et une competition connus + club = next(c for c in server.clubs if c['name'] == "Simply Lift") + competition = next( + c for c in server.competitions if c['name'] == "Spring Festival" + ) + + points_avant = int(club['points']) + places_demandees = 13 # au dessus de la limite de 12 + + response = client.post( + + "/purchasePlaces", + data={ + "competition": competition['name'], + "club": club['name'], + "places": str(places_demandees), + }, + follow_redirects=True, + ) + + # Rien ne doit avoir ete debite + assert int(club["points"]) == points_avant + # Un message erreur doit apparaitre + assert b"cannot book more than 12 places" in response.data + +def test_purchase_more_than_available_places_is_blocked(client): + """Bug #4 : un club ne peut pas reserver plus de places qu'il n'en reste""" + club = next(c for c in server.clubs if c['name'] == "Simply Lift") + competition = next( + c for c in server.competitions if c['name'] == "Spring Festival" + ) + + # Force une competition presque pleine + competition['numberOfPlaces'] = "5" + + points_avant = int(club['points']) + places_demandees = 10 # Sous la limite de 12 mais plus que les 5 restants + + response = client.post( + "/purchasePlaces", + data={ + "competition": competition['name'], + "club": club['name'], + "places": str(places_demandees), + }, + follow_redirects=True, + ) + + # Rien ne doit avoir change + assert int(club['points']) == points_avant + assert int (competition['numberOfPlaces']) == 5 + # Un message d'erreur doit apparaitre + assert b"not enough places available" in response.data \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb