From 2ba2fcfaaf0c2dc8c84aed52b8dbf10db9c6896c Mon Sep 17 00:00:00 2001 From: elof-dev Date: Wed, 22 Oct 2025 07:26:35 +0700 Subject: [PATCH 01/11] Fix issue #1 : correct showSummary and add Flask testing setup - Updated showSummary() in server.py to handle email validation and error messages - Modified index.html to display flash messages correctly - Created tests/ folder with conftest.py and test_show_summary.py for unit testing - Added .flaskenv for local environment configuration - Updated .gitignore to include .venv and exclude tests/ folder - Updated requirements.txt to add pytest dependency --- .flaskenv | 2 ++ .gitignore | 4 +-- requirements.txt | 7 ++++++ server.py | 9 +++++-- templates/index.html | 9 +++++++ tests/unit/conftest.py | 23 ++++++++++++++++++ tests/unit/test_check_email_show_summary.py | 27 +++++++++++++++++++++ 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 .flaskenv create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_check_email_show_summary.py diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 000000000..10950ca46 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=server +FLASK_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2cba99d87..f577272a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.venv \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 139affa05..7a3b0bf04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,13 @@ click==7.1.2 +colorama==0.4.6 Flask==1.1.2 +iniconfig==2.3.0 itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 +packaging==25.0 +pluggy==1.6.0 +Pygments==2.19.2 +pytest==8.4.2 +python-dotenv==1.1.1 Werkzeug==1.0.1 diff --git a/server.py b/server.py index 4084baeac..43c1340ce 100644 --- a/server.py +++ b/server.py @@ -26,8 +26,13 @@ def index(): @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) + email = request.form.get('email', '') + email = email.strip() + club = next((c for c in clubs if c.get('email', '').strip() == email), None) + if not email or not club: + flash("Sorry, that email was not found, please try again.") + return render_template('index.html') + return render_template('welcome.html', club=club, competitions=competitions) @app.route('/book//') diff --git a/templates/index.html b/templates/index.html index 926526b7d..ae4e91eb1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,6 +7,15 @@

Welcome to the GUDLFT Registration Portal!

Please enter your secretary email to continue: + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %}
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 000000000..7153001ce --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,23 @@ +import os +import sys +import pytest + + +"""Pytest configuration file to set up the testing environment. +This file adds the project root directory to sys.path to ensure that +""" + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +if ROOT_DIR not in sys.path: + sys.path.insert(0, ROOT_DIR) + +from server import app + +@pytest.fixture +def client(): + """Flask test client fixture. + Provides a test client for the Flask application defined in server.py. + """ + app.config['TESTING'] = True + with app.test_client() as client: + yield client \ No newline at end of file diff --git a/tests/unit/test_check_email_show_summary.py b/tests/unit/test_check_email_show_summary.py new file mode 100644 index 000000000..4e6428c9b --- /dev/null +++ b/tests/unit/test_check_email_show_summary.py @@ -0,0 +1,27 @@ +import pytest + + +"""Unit tests for the /showSummary route in server.py. +test1 : Valid email should return 200 and welcome message +test2 : Unknown email should return 200 and error message +test3 : Invalid email (empty or whitespace) should return 200 and error message + +""" + + +def test_show_summary_with_valid_email(client): + response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert response.status_code == 200 + assert b'Welcome' in response.data + + +def test_show_summary_with_unknown_email(client): + response = client.post('/showSummary', data={'email': 'unknown@example.com'}) + assert response.status_code == 200 + assert b"Sorry, that email was not found" in response.data + + +def test_show_summary_with_invalid_email(client): + response = client.post('/showSummary', data={'email': ' '}) + assert response.status_code == 200 + assert b"Sorry, that email was not found" in response.data \ No newline at end of file From 8135241e0c17f4bbfd4a9e90dbcfc27b8422abcb Mon Sep 17 00:00:00 2001 From: elof-dev Date: Wed, 22 Oct 2025 09:55:53 +0700 Subject: [PATCH 02/11] Fix issue #2 : correct purchasePlaces() and add unit tests - Updated purchasePlaces() to check if the club has enough points before confirming a booking - Added a new test file with 2 unit tests to verify point validation logic --- server.py | 10 ++++- .../unit/test_book_place_with_enough_point.py | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_book_place_with_enough_point.py diff --git a/server.py b/server.py index 43c1340ce..07932372b 100644 --- a/server.py +++ b/server.py @@ -51,8 +51,14 @@ 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!') + club_points = int(club['points']) + + if placesRequired > club_points: + flash("Cannot book more places than club points.") + return render_template('welcome.html', club=club, competitions=competitions) + + competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired + flash('Great - booking complete!') return render_template('welcome.html', club=club, competitions=competitions) diff --git a/tests/unit/test_book_place_with_enough_point.py b/tests/unit/test_book_place_with_enough_point.py new file mode 100644 index 000000000..efe3df3dd --- /dev/null +++ b/tests/unit/test_book_place_with_enough_point.py @@ -0,0 +1,44 @@ +import server + +""" +Unit test file for the place booking feature. +The purpose is to verify that booking places is only possible with sufficient points. + +Test 1: the club has 4 points and wants to book 4 places — booking accepted : + - status code 200 + - confirmation message + - remaining places updated correctly. +Test 2: the club has 4 points and wants to book 5 places — booking refused : + - status code 200 + - error message + - remaining places unchanged +""" + + +def test_book_places_with_enough_points(client): + server.clubs = [{"name": "Club A", "points": "4"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "5"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '4' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 1 + +def test_book_places_without_enough_points(client): + server.clubs = [{"name": "Club B", "points": "4"}] + server.competitions = [{"name": "Comp 2", "numberOfPlaces": "5"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 2', + 'club': 'Club B', + 'places': '5' + }) + + assert response.status_code == 200 + assert b"Cannot book more places than club points." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 5 \ No newline at end of file From 825904b6c1eaf7bd9503bc988cc69290ef2fda14 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Wed, 22 Oct 2025 10:31:11 +0700 Subject: [PATCH 03/11] Fix issue #4 : correct purchasePlaces() and add unit tests - Updated purchasePlaces() to check avoid clubs booking more than 12 places per competition - Added a new test file with 2 unit tests to verify point validation logic --- server.py | 8 +++- tests/unit/test_book_more_than_12_places.py | 47 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_book_more_than_12_places.py diff --git a/server.py b/server.py index 07932372b..9bd8ee4d1 100644 --- a/server.py +++ b/server.py @@ -48,6 +48,7 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): + MAX_BOOKING = 12 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']) @@ -55,7 +56,12 @@ def purchasePlaces(): if placesRequired > club_points: flash("Cannot book more places than club points.") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template('welcome.html', club=club, competitions=competitions) + + + if placesRequired > MAX_BOOKING: + flash(f"Cannot book more than {MAX_BOOKING} places for this competition.") + return render_template('welcome.html', club=club, competitions=competitions) competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired flash('Great - booking complete!') diff --git a/tests/unit/test_book_more_than_12_places.py b/tests/unit/test_book_more_than_12_places.py new file mode 100644 index 000000000..e238b9569 --- /dev/null +++ b/tests/unit/test_book_more_than_12_places.py @@ -0,0 +1,47 @@ +import server + +""" +Unit test file to check that clubs cannot book more than 12 places. + +Test 1: Club A has 13 points and competition 1 has 25 places. + - Book 12 places + - status code 200 + - message: "Great - booking complete!" + - competition places decreased + +Test 2: Club A has 13 points and competition 1 has 25 places. + - Book 13 places + - status code 200 + - message contains "Cannot book more than" + - competition places unchanged +""" + + +def test_book_12_places_allowed(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "25"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '12' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 13 + + +def test_cannot_book_more_than_12_places(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "25"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '13' + }) + + assert response.status_code == 200 + assert b"Cannot book more than" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 25 \ No newline at end of file From ceb484c4642498ce96bae701f94cf924507ba15d Mon Sep 17 00:00:00 2001 From: elof-dev Date: Wed, 22 Oct 2025 15:21:08 +0700 Subject: [PATCH 04/11] Fix issue #5 : correct book() and add unit tests - Updated book() to avoid clubs booking in past competition - Added a new test file with 2 unit tests --- server.py | 8 ++++++ tests/unit/test_book_past_competition.py | 34 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/unit/test_book_past_competition.py diff --git a/server.py b/server.py index 9bd8ee4d1..7ea6e6c3c 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,6 @@ import json from flask import Flask,render_template,request,redirect,flash,url_for +from datetime import datetime def loadClubs(): @@ -39,8 +40,15 @@ def showSummary(): 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] + + competition_date = datetime.strptime(foundCompetition['date'], "%Y-%m-%d %H:%M:%S") + if competition_date < datetime.now(): + flash("This competition has already taken place, booking is not allowed.") + return render_template('welcome.html', club=foundClub, competitions=competitions) + if foundClub and 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) diff --git a/tests/unit/test_book_past_competition.py b/tests/unit/test_book_past_competition.py new file mode 100644 index 000000000..7436ca6ff --- /dev/null +++ b/tests/unit/test_book_past_competition.py @@ -0,0 +1,34 @@ +import server +from datetime import datetime, timedelta + +""" +Unit test file to check that booking is not allowed for past competitions + +Test 1: A competition in the future -> user can access booking page + - status code 200 + - page contains "How many places?" +Test 2: A competition in the past -> booking is refused. + - status code 200 + - message contains "This competition has already taken place" +""" + +def test_can_book_future_competition(client): + future_date = (datetime.now() + timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") + server.competitions = [{"name": "Future Comp", "date": future_date, "numberOfPlaces": "10"}] + server.clubs = [{"name": "Club A", "email": "a@a.com", "points": "10"}] + + response = client.get('/book/Future Comp/Club A') + + assert response.status_code == 200 + assert b"How many places?" in response.data + + +def test_cannot_book_past_competition(client): + past_date = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") + server.competitions = [{"name": "Old Comp", "date": past_date, "numberOfPlaces": "10"}] + server.clubs = [{"name": "Club A", "email": "a@a.com", "points": "10"}] + + response = client.get('/book/Old Comp/Club A') + + assert response.status_code == 200 + assert b"This competition has already taken place" in response.data From f4b264407580cea8e12ba101b01f48ff72637566 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Wed, 22 Oct 2025 16:04:59 +0700 Subject: [PATCH 05/11] Fix issue #6 : correct purchasePlaces() and add unit tests - Added missing line in purchasePlaces() to decrease club points after a valid booking - Created test to verify: - club points decrease when booking succeeds --- server.py | 1 + .../unit/test_booking_decrease_club_points.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/unit/test_booking_decrease_club_points.py diff --git a/server.py b/server.py index 7ea6e6c3c..d5cce5c41 100644 --- a/server.py +++ b/server.py @@ -72,6 +72,7 @@ def purchasePlaces(): return render_template('welcome.html', club=club, competitions=competitions) competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired + club['points'] = club_points - placesRequired flash('Great - booking complete!') return render_template('welcome.html', club=club, competitions=competitions) diff --git a/tests/unit/test_booking_decrease_club_points.py b/tests/unit/test_booking_decrease_club_points.py new file mode 100644 index 000000000..d1c54da16 --- /dev/null +++ b/tests/unit/test_booking_decrease_club_points.py @@ -0,0 +1,27 @@ +import server + +""" +Unit test file to verify that club points are correctly updated after booking. + +Test 1: Club has 10 points, books 3 places, points should decrease by 3. + - status code 200 + - success message "Great - booking complete!" + - club points decrease by 3 + +""" + + +def test_club_points_decrease_after_booking(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '3' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.clubs[0]['points']) == 7 + From 75316cd40509c577bb15f2d797b6ab14e8875b84 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Thu, 23 Oct 2025 07:16:24 +0700 Subject: [PATCH 06/11] Fixes #7: update server.py and index.html to display board + tests - Updated index.html to include a simple grey table listing all clubs and their points - Modified server.py to pass the clubs data to the index template - Added test_display_clubs_points.py to verify that: - the page loads successfully - each club name and its points appear correctly in the HTML --- server.py | 2 +- templates/index.html | 17 +++++++++++++++ .../test_board_with_clubs_and_their_points.py | 21 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_board_with_clubs_and_their_points.py diff --git a/server.py b/server.py index d5cce5c41..e53d984e2 100644 --- a/server.py +++ b/server.py @@ -23,7 +23,7 @@ def loadCompetitions(): @app.route('/') def index(): - return render_template('index.html') + return render_template('index.html', clubs=clubs) @app.route('/showSummary',methods=['POST']) def showSummary(): diff --git a/templates/index.html b/templates/index.html index ae4e91eb1..3be9546c9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -21,5 +21,22 @@

Welcome to the GUDLFT Registration Portal!

+

Clubs Points Summary

+ + + + + + + + + {% for club in clubs %} + + + + + {% endfor %} + +
ClubPoints
{{ club['name'] }}{{ club['points'] }}
\ No newline at end of file diff --git a/tests/unit/test_board_with_clubs_and_their_points.py b/tests/unit/test_board_with_clubs_and_their_points.py new file mode 100644 index 000000000..eb832fae4 --- /dev/null +++ b/tests/unit/test_board_with_clubs_and_their_points.py @@ -0,0 +1,21 @@ +import server + +""" +Unit test file to check if the index page displays the clubs and their points correctly +Test 1: The page loads successfully (status code 200) +Test 2: Each club name and its points appear in the HTML +""" + +def test_index_page_loads(client): + response = client.get('/') + assert response.status_code == 200 + assert b"Clubs Points Summary" in response.data + + +def test_index_page_displays_clubs_points(client): + response = client.get('/') + data = response.data.decode() + + for club in server.clubs: + assert club["name"] in data + assert club["points"] in data From 48abe0bca13607329db0766a0b545b473508afe2 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Thu, 23 Oct 2025 09:24:42 +0700 Subject: [PATCH 07/11] Fix/invalid bookings (empty, zero, non-numeric or negative values) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated purchasePlaces() to handle invalid inputs: • Added a check for empty input • Added a try/except block to catch non-numeric values • Added a condition to reject zero or negative values - Created test_invalid_number_of_places_booking.py: • Test 1: valid booking (success) • Test 2: zero places (error) • Test 3: negative places (error) • Test 4: empty input (error) • Test 5: non-numeric input (error) --- server.py | 20 ++++- .../test_invalid_number_of_places_booking.py | 90 +++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_invalid_number_of_places_booking.py diff --git a/server.py b/server.py index e53d984e2..adb583048 100644 --- a/server.py +++ b/server.py @@ -59,9 +59,27 @@ def purchasePlaces(): MAX_BOOKING = 12 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']) + + places = request.form.get('places') + if not places: + flash("Please enter a number of places.") + return render_template('welcome.html', club=club, competitions=competitions) + + # même si on a vérifié dans le formulaire sur le HTML avec un input de type "number", + # on doit quand même gérer le cas où un utilisateur malveillant envoie une valeur non numérique + # en modifiant via les outils de développement du navigateur. + try: + placesRequired = int(places) + except ValueError: + flash("Invalid number of places.") + return render_template('welcome.html', club=club, competitions=competitions) + club_points = int(club['points']) + if placesRequired <= 0: + flash("Invalid number of places.") + return render_template('welcome.html', club=club, competitions=competitions) + if placesRequired > club_points: flash("Cannot book more places than club points.") return render_template('welcome.html', club=club, competitions=competitions) diff --git a/tests/unit/test_invalid_number_of_places_booking.py b/tests/unit/test_invalid_number_of_places_booking.py new file mode 100644 index 000000000..f817113ff --- /dev/null +++ b/tests/unit/test_invalid_number_of_places_booking.py @@ -0,0 +1,90 @@ +import server + +""" +Unit test file to verify that the booking system correctly handles invalid number of places inputs + +Test 1: booking with valid number of places -> success, points and places updated +Test 2 :booking with 0 places -> error message, number of places and points unchanged +Test 3: booking with negative places -> error message, number of places and points unchanged +Test 4: booking without providing number of places -> error message, number of places and points unchanged +Test 5: booking with non-numeric input -> error message, number of places and points unchanged +""" + +def test_booking_valid_number_of_places(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '3' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.clubs[0]['points']) == 7 + assert int(server.competitions[0]['numberOfPlaces']) == 17 + + +def test_booking_zero_places(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '0' + }) + + assert response.status_code == 200 + assert b"Invalid number of places." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 20 + assert int(server.clubs[0]['points']) == 10 + + +def test_booking_negative_places(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '-5' + }) + + assert response.status_code == 200 + assert b"Invalid number of places." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 20 + assert int(server.clubs[0]['points']) == 10 + + +def test_booking_without_places_value(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '' + }) + + assert response.status_code == 200 + assert b"Please enter a number of places." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 20 + assert int(server.clubs[0]['points']) == 10 + + +def test_booking_with_non_numeric_input(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': 'abc' + }) + + assert response.status_code == 200 + assert b"Invalid number of places." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 20 + assert int(server.clubs[0]['points']) == 10 From ce5f1e8c8b68bf78a4d6810408e4e6173bcf16e7 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Fri, 24 Oct 2025 07:41:11 +0700 Subject: [PATCH 08/11] fix/prevent booking more places than available in competition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added validation in purchasePlaces() to prevent users from booking more places than the competition has available - Added test_booking_more_than_available.py to verify: • booking above available places shows proper error message • competition and club data remain unchanged --- server.py | 3 +++ .../test_book_more_than_places_available.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/unit/test_book_more_than_places_available.py diff --git a/server.py b/server.py index adb583048..a1e6c664c 100644 --- a/server.py +++ b/server.py @@ -84,6 +84,9 @@ def purchasePlaces(): flash("Cannot book more places than club points.") return render_template('welcome.html', club=club, competitions=competitions) + if placesRequired > int(competition['numberOfPlaces']): + flash("Cannot book more places than available.") + return render_template('welcome.html', club=club, competitions=competitions) if placesRequired > MAX_BOOKING: flash(f"Cannot book more than {MAX_BOOKING} places for this competition.") diff --git a/tests/unit/test_book_more_than_places_available.py b/tests/unit/test_book_more_than_places_available.py new file mode 100644 index 000000000..8c0e5cea6 --- /dev/null +++ b/tests/unit/test_book_more_than_places_available.py @@ -0,0 +1,20 @@ +import server + +""" +Unit test to verify that users cannot book more places than available. +""" + +def test_booking_more_places_than_available(client): + server.clubs = [{"name": "Club A", "points": "20"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "5"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '10' + }) + + assert response.status_code == 200 + assert b"Cannot book more places than available." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 5 + assert int(server.clubs[0]['points']) == 20 From 07485f6eac21310ccc708230ffb1797a250ae4f9 Mon Sep 17 00:00:00 2001 From: elof-dev Date: Fri, 24 Oct 2025 09:06:55 +0700 Subject: [PATCH 09/11] fix/cumulative-booking-limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update server.py to add cumulative validation to purchasePlaces() - Update existing test file with: • cumulative booking rejected (6 + 7) • cumulative booking accepted (6 + 5) --- server.py | 9 ++- tests/unit/test_book_more_than_12_places.py | 62 ++++++++++++++++----- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/server.py b/server.py index a1e6c664c..59336fb20 100644 --- a/server.py +++ b/server.py @@ -88,10 +88,13 @@ def purchasePlaces(): flash("Cannot book more places than available.") return render_template('welcome.html', club=club, competitions=competitions) - if placesRequired > MAX_BOOKING: - flash(f"Cannot book more than {MAX_BOOKING} places for this competition.") - return render_template('welcome.html', club=club, competitions=competitions) + already_booked = int(competition.get('booked_by', {}).get(club['name'], 0)) + if placesRequired > MAX_BOOKING or already_booked + placesRequired > MAX_BOOKING: + flash(f"Cannot book more than {MAX_BOOKING} places per club for this competition.") + return render_template('welcome.html', club=club, competitions=competitions) + + competition.setdefault('booked_by', {})[club['name']] = already_booked + placesRequired competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired club['points'] = club_points - placesRequired flash('Great - booking complete!') diff --git a/tests/unit/test_book_more_than_12_places.py b/tests/unit/test_book_more_than_12_places.py index e238b9569..061b505b0 100644 --- a/tests/unit/test_book_more_than_12_places.py +++ b/tests/unit/test_book_more_than_12_places.py @@ -1,19 +1,12 @@ import server """ -Unit test file to check that clubs cannot book more than 12 places. - -Test 1: Club A has 13 points and competition 1 has 25 places. - - Book 12 places - - status code 200 - - message: "Great - booking complete!" - - competition places decreased - -Test 2: Club A has 13 points and competition 1 has 25 places. - - Book 13 places - - status code 200 - - message contains "Cannot book more than" - - competition places unchanged +Unit test file to check that clubs cannot book more than 12 places in total. + +Test 1: Club A books exactly 12 places -> success +Test 2: Club A tries to book 13 places -> rejected +Test 3: Club A has already booked 6 places, tries to book 7 more -> rejected (cumulative > 12) +Test 4: Club A has already booked 6 places, tries to book 5 more -> accepted (cumulative = 11) """ @@ -44,4 +37,45 @@ def test_cannot_book_more_than_12_places(client): assert response.status_code == 200 assert b"Cannot book more than" in response.data - assert int(server.competitions[0]['numberOfPlaces']) == 25 \ No newline at end of file + assert int(server.competitions[0]['numberOfPlaces']) == 25 + + +def test_cannot_exceed_12_places_cumulatively(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{ + "name": "Comp 1", + "numberOfPlaces": "25", + "booked_by": {"Club A": 6} + }] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '7' + }) + + assert response.status_code == 200 + assert b"Cannot book more than" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 25 + assert int(server.clubs[0]['points']) == 13 + + +def test_booking_allowed_when_cumulative_under_limit(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{ + "name": "Comp 1", + "numberOfPlaces": "25", + "booked_by": {"Club A": 6} + }] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '5' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 20 + assert int(server.clubs[0]['points']) == 8 + assert server.competitions[0]['booked_by']['Club A'] == 11 From 116d864029a45e7f298c64c01abcd1ec74f0fe0d Mon Sep 17 00:00:00 2001 From: elof-dev Date: Tue, 28 Oct 2025 11:18:26 +0700 Subject: [PATCH 10/11] fix/Secure club booking using session authentication - Updated showSummary() and purchasePlaces() to use the club from the session rather than form data, preventing users from booking on behalf of other clubs. - Removed the club field from the booking.html form - Updated conftest.py to add the session logic - Updated test_book_place_with_enough_point, just to change the name of the club in the test (club A, because it's club A in the confest session) - Created test_session_secure_club_auth to ensure bookings are made for the logged-in club only. --- server.py | 17 ++++++++-- templates/booking.html | 1 - tests/unit/conftest.py | 7 ++--- .../unit/test_book_place_with_enough_point.py | 4 +-- tests/unit/test_session_secure_club_auth.py | 31 +++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 tests/unit/test_session_secure_club_auth.py diff --git a/server.py b/server.py index 59336fb20..acf0c6141 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ import json from flask import Flask,render_template,request,redirect,flash,url_for from datetime import datetime +from flask import session def loadClubs(): @@ -27,12 +28,13 @@ def index(): @app.route('/showSummary',methods=['POST']) def showSummary(): - email = request.form.get('email', '') - email = email.strip() + email = request.form.get('email', '').strip() club = next((c for c in clubs if c.get('email', '').strip() == email), None) if not email or not club: flash("Sorry, that email was not found, please try again.") return render_template('index.html') + # + session['club_name'] = club['name'] return render_template('welcome.html', club=club, competitions=competitions) @@ -58,7 +60,16 @@ def book(competition,club): def purchasePlaces(): MAX_BOOKING = 12 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] + club_name = session.get('club_name') + if not club_name: + flash("You must be logged in to book places.") + return render_template('index.html', clubs=clubs) + + club = next((c for c in clubs if c['name'] == club_name), None) + if not club: + flash("Club not found in session.") + return render_template('index.html', clubs=clubs) + places = request.form.get('places') if not places: diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..291d31f98 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -8,7 +8,6 @@

{{competition['name']}}

Places available: {{competition['numberOfPlaces']}}
- diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 7153001ce..1f642aacb 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -15,9 +15,8 @@ @pytest.fixture def client(): - """Flask test client fixture. - Provides a test client for the Flask application defined in server.py. - """ app.config['TESTING'] = True with app.test_client() as client: - yield client \ No newline at end of file + with client.session_transaction() as session: + session['club_name'] = "Club A" + yield client diff --git a/tests/unit/test_book_place_with_enough_point.py b/tests/unit/test_book_place_with_enough_point.py index efe3df3dd..842723a4a 100644 --- a/tests/unit/test_book_place_with_enough_point.py +++ b/tests/unit/test_book_place_with_enough_point.py @@ -30,12 +30,12 @@ def test_book_places_with_enough_points(client): assert int(server.competitions[0]['numberOfPlaces']) == 1 def test_book_places_without_enough_points(client): - server.clubs = [{"name": "Club B", "points": "4"}] + server.clubs = [{"name": "Club A", "points": "4"}] server.competitions = [{"name": "Comp 2", "numberOfPlaces": "5"}] response = client.post('/purchasePlaces', data={ 'competition': 'Comp 2', - 'club': 'Club B', + 'club': 'Club A', 'places': '5' }) diff --git a/tests/unit/test_session_secure_club_auth.py b/tests/unit/test_session_secure_club_auth.py new file mode 100644 index 000000000..c792f375b --- /dev/null +++ b/tests/unit/test_session_secure_club_auth.py @@ -0,0 +1,31 @@ +import server + +""" +Unit test to chechk that the booking process uses the club from the session +and not the club sent in the form data (to prevent cheating) +Test1 : simulate club A logged in and trying to book places by sending club B in the form +result : booking should be made for club A and not club B +""" + +def test_booking_uses_session_club(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + with client.session_transaction() as session: + session['club_name'] = "Club A" + + response = client.post('/purchasePlaces', data={ + "competition": "Comp 1", + "club": "Club B", + "places": "2" + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + # Vérif que c'est bien le club A qui a perdu 2 points et pas le club B + assert int(server.competitions[0]['numberOfPlaces']) == 18 + assert int(server.clubs[0]['points']) == 8 + + + + From efda52ef11b135dec770ea6c8143f2f45d638c7c Mon Sep 17 00:00:00 2001 From: elof-dev Date: Tue, 28 Oct 2025 17:04:07 +0700 Subject: [PATCH 11/11] Fix/handle invalid or missing json files - Refactored club and competition loading to use a shared function with error handling for missing files, malformed JSON, and missing keys - Added unit tests to verify the behavior and error logging when invalid or missing JSON files --- server.py | 22 +++++++++---- tests/unit/test_invalid_json_files.py | 47 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_invalid_json_files.py diff --git a/server.py b/server.py index acf0c6141..ef7d62b16 100644 --- a/server.py +++ b/server.py @@ -2,18 +2,26 @@ from flask import Flask,render_template,request,redirect,flash,url_for from datetime import datetime from flask import session +import logging -def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs +def load_json_data(filename: str, key: str): + try: + with open(filename) as f: + data = json.load(f) + if key not in data: + logging.warning(f"Key '{key}' not found in {filename}") + return [] + return data[key] + except (FileNotFoundError, json.JSONDecodeError) as e: + logging.warning(f"Unable to load {filename}: {e}") + return [] +def loadClubs(): + return load_json_data("clubs.json", "clubs") def loadCompetitions(): - with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions + return load_json_data("competitions.json", "competitions") app = Flask(__name__) diff --git a/tests/unit/test_invalid_json_files.py b/tests/unit/test_invalid_json_files.py new file mode 100644 index 000000000..63fae556f --- /dev/null +++ b/tests/unit/test_invalid_json_files.py @@ -0,0 +1,47 @@ +import json +import logging +import pytest +from server import load_json_data + +"""Unit tests to check handling of invalid or missing JSON files for clubs and competitions +test1 : File not found -> warning logged, empty list returned +test2 : File found but invalid JSON structure -> warning logged, empty list returned +test3 : File found but key clubs is missing -> warning logged, empty list returned +test4 : Valid file with correct structure -> data returned correctly +""" + +@pytest.fixture +def tmp_file(tmp_path): + return tmp_path / "clubs.json" + + + +def test_file_not_found(caplog): + caplog.set_level(logging.WARNING) + result = load_json_data("not_existing.json", "clubs") + assert result == [] + assert "Unable to load not_existing.json" in caplog.text + + +def test_file_found_but_wrong_json_structure(tmp_file, caplog): + caplog.set_level(logging.WARNING) + tmp_file.write_text("{ clubs: }") + result = load_json_data(tmp_file, "clubs") + assert result == [] + assert f"Unable to load {tmp_file}" in caplog.text + + +def test_missing_clubs_key(tmp_file, caplog): + caplog.set_level(logging.WARNING) + tmp_file.write_text(json.dumps({"not_clubs": []})) + result = load_json_data(tmp_file, "clubs") + assert result == [] + assert f"Key 'clubs' not found in {tmp_file}" in caplog.text + + + +def test_valid_file(tmp_file): + clubs = [{"name": "Test Club", "email": "club@test.com", "points": 10}] + tmp_file.write_text(json.dumps({"clubs": clubs})) + result = load_json_data(tmp_file, "clubs") + assert result == clubs