From 875b6992b178d98fc9545d0c8d38a23f4cf422f1 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 13:39:29 +0100 Subject: [PATCH 01/23] chore: set up test folder structure, conftest and pytest config --- .gitignore | 13 +++++---- pytest.ini | 6 ++++ requirements.txt | 54 +++++++++++++++++++++++++++++++---- tests/__init__.py | 0 tests/conftest.py | 9 ++++++ tests/functional/__init__.py | 0 tests/integration/__init__.py | 0 tests/unit/__init__.py | 0 8 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/functional/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore index 2cba99d87..d62acba5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ -bin -include -lib +bin/ +include/ +lib/ .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__/ +*.pyc +.coverage +htmlcov/ +venv/ \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..8335bb6ec --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = --cov=server --cov-report=html \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 139affa05..820ddef85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,48 @@ -click==7.1.2 -Flask==1.1.2 -itsdangerous==1.1.0 -Jinja2==2.11.2 -MarkupSafe==1.1.1 -Werkzeug==1.0.1 +attrs==25.4.0 +bidict==0.23.1 +blinker==1.9.0 +brotli==1.2.0 +certifi==2026.2.25 +charset-normalizer==3.4.5 +click==8.3.1 +ConfigArgParse==1.7.5 +coverage==7.13.4 +Flask==3.1.3 +flask-cors==6.0.2 +Flask-Login==0.6.3 +gevent==25.9.1 +geventhttpclient==2.3.9 +greenlet==3.3.2 +h11==0.16.0 +idna==3.11 +iniconfig==2.3.0 +itsdangerous==2.2.0 +Jinja2==3.1.6 +locust==2.43.3 +MarkupSafe==3.0.3 +msgpack==1.1.2 +outcome==1.3.0.post0 +packaging==26.0 +pluggy==1.6.0 +psutil==7.2.2 +Pygments==2.19.2 +PySocks==1.7.1 +pytest==9.0.2 +pytest-cov==7.0.0 +python-engineio==4.13.1 +python-socketio==5.16.1 +pyzmq==27.1.0 +requests==2.32.5 +selenium==4.41.0 +simple-websocket==1.1.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.33.0 +trio-websocket==0.12.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +websocket-client==1.9.0 +Werkzeug==3.1.6 +wsproto==1.3.2 +zope.event==6.1 +zope.interface==8.2 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..99c53d124 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from server import app + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + with app.test_client() as client: + yield client \ 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/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb From 020c5e5ca8b3507faa8a699656b56b04fa748ed1 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 13:52:38 +0100 Subject: [PATCH 02/23] chore: configure app entry point and clean up .gitignore --- .gitignore | 2 +- server.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d62acba5e..6b74e86ad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bin/ include/ lib/ .Python -.envrc + __pycache__/ *.pyc .coverage diff --git a/server.py b/server.py index 4084baeac..8dbbe134f 100644 --- a/server.py +++ b/server.py @@ -56,4 +56,8 @@ def purchasePlaces(): @app.route('/logout') def logout(): - return redirect(url_for('index')) \ No newline at end of file + return redirect(url_for('index')) + +# Needs this to run app +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file From 9f24397470899cfa6b278d5490bad92efa196c5a Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 14:30:39 +0100 Subject: [PATCH 03/23] chore: add test fixtures and mock data for full test isolation --- tests/conftest.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 99c53d124..861841441 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,92 @@ import pytest +import server from server import app +# --- Mock data fixtures --- + +@pytest.fixture +def mock_clubs(): + """ + Returns a controlled list of mock clubs for testing. + Includes real-world clubs plus edge cases (zero points, one point) + to cover boundary conditions without touching clubs.json. + """ + return [ + {"name": "Simply Lift", "email": "john@simplylift.co", "points": "13"}, + {"name": "Iron Temple", "email": "admin@irontemple.com", "points": "4"}, + {"name": "She Lifts", "email": "kate@shelifts.co.uk", "points": "12"}, + # Edge cases + {"name": "Zero Points Club", "email": "zero@club.com", "points": "0"}, + {"name": "One Point Club", "email": "one@club.com", "points": "1"}, + ] + +@pytest.fixture +def mock_competitions(): + """ + Returns a controlled list of mock competitions for testing. + Includes future competitions, edge cases (full, almost full), + and past competitions to cover all validation scenarios + without touching competitions.json. + """ + return [ + # Future competitions — valid for booking + { + "name": "Future Festival", + "date": "2030-03-27 10:00:00", + "numberOfPlaces": "25" + }, + { + "name": "Future Classic", + "date": "2030-10-22 13:30:00", + "numberOfPlaces": "13" + }, + # Edge cases for place availability + { + "name": "Almost Full Competition", + "date": "2030-06-15 10:00:00", + "numberOfPlaces": "1" + }, + { + "name": "Full Competition", + "date": "2030-07-20 10:00:00", + "numberOfPlaces": "0" + }, + # Past competitions — visible but not bookable + { + "name": "Past Festival", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25" + }, + { + "name": "Past Classic", + "date": "2020-10-22 13:30:00", + "numberOfPlaces": "13" + }, + ] + + +# --- App fixtures --- + @pytest.fixture def client(): + """ + Provides a basic Flask test client with TESTING mode enabled. + Uses the real clubs.json and competitions.json data. + Only use this fixture for tests that specifically need real data. + """ app.config['TESTING'] = True with app.test_client() as client: - yield client \ No newline at end of file + yield client + +@pytest.fixture +def client_with_mock_data(client, mock_clubs, mock_competitions, monkeypatch): + """ + Provides a Flask test client with fully isolated mock data injected + via monkeypatch. Never reads from or writes to clubs.json or + competitions.json. Use this fixture for all unit and integration tests + to ensure test isolation and repeatable results. + """ + monkeypatch.setattr(server, 'clubs', mock_clubs) + monkeypatch.setattr(server, 'competitions', mock_competitions) + yield client \ No newline at end of file From 82aa7dd6edfaabedc61565aa4c264e56e861b1b9 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 14:38:40 +0100 Subject: [PATCH 04/23] Added coverage reports to each testing --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 8335bb6ec..a73aec021 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,4 +3,4 @@ testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* -addopts = --cov=server --cov-report=html \ No newline at end of file +addopts = --cov=server --cov-report=html --cov-report=term-missing \ No newline at end of file From db2649643ef1d5d7debae1ca6932e1775a288a69 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 14:42:18 +0100 Subject: [PATCH 05/23] chore: wrote happy and sad test cases for login --- tests/unit/test_login.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/unit/test_login.py diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py new file mode 100644 index 000000000..6b0d2f29c --- /dev/null +++ b/tests/unit/test_login.py @@ -0,0 +1,40 @@ +import pytest + +class TestLogin: + """ + Unit tests for the /showSummary route (login). + + Issue #2: Unknown email crashes the app with IndexError 500. + Branch: fix/login-unknown-email-crash + """ + + # ----------------- + # SAD PATH + # ----------------- + def test_unknown_email_returns_error_message(self, client_with_mock_data): + """ + An unknown email should return 200 with a clear error + message instead of crashing with IndexError 500. + """ + response = client_with_mock_data.post( + '/showSummary', + data={'email': 'unknown@notfound.com'} + ) + assert response.status_code == 200 + assert b"Sorry, that email wasn't found." in response.data + + # ----------------- + # HAPPY PATH + # ----------------- + + def test_valid_email_loads_summary_page(self, client_with_mock_data): + """ + A valid email should load the summary/welcome page + and display the club's name. + """ + response = client_with_mock_data.post( + '/showSummary', + data={'email': 'john@simplylift.co'} + ) + assert response.status_code == 200 + assert b"Welcome" in response.data From 0e6a2e215c1210b945de17cb28febef71c40248e Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 15:06:28 +0100 Subject: [PATCH 06/23] fix: switched from index based search for email to next() with default, used condition to set flash message, set 200 status code and stay on index.html --- server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 8dbbe134f..6a51693aa 100644 --- a/server.py +++ b/server.py @@ -26,7 +26,10 @@ def index(): @app.route('/showSummary',methods=['POST']) def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] + club = next((club for club in clubs if club['email'] == request.form['email']), None) + if not club: + flash("Sorry, that email was not found.") + return render_template('index.html'), 200 return render_template('welcome.html',club=club,competitions=competitions) From 94faeb24a3551f9930fdd45c509627187acfb3d4 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 15:07:11 +0100 Subject: [PATCH 07/23] improvement: added flash message to set failed login attempts into ui without crashing --- templates/index.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/templates/index.html b/templates/index.html index 926526b7d..23c3879ad 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,17 @@

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:
From 4f4a7e0068006454f606f0c08b345d680f1011b1 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 15:08:46 +0100 Subject: [PATCH 08/23] TDD: failed test case for an edge case (no email submission) --- tests/unit/test_login.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py index 6b0d2f29c..b1644b685 100644 --- a/tests/unit/test_login.py +++ b/tests/unit/test_login.py @@ -38,3 +38,18 @@ def test_valid_email_loads_summary_page(self, client_with_mock_data): ) assert response.status_code == 200 assert b"Welcome" in response.data + + # ----------------- + # EDGE CASE + # ----------------- + def test_empty_email_returns_error_message(self, client_with_mock_data): + """ + An empty email submission should return 200 with an + error message rather than crashing or returning a server error. + """ + response = client_with_mock_data.post( + '/showSummary', + data={'email': ''} + ) + assert response.status_code == 200 + assert b"Sorry, that email was not found." in response.data From 8b2a376c0b77048b7f3e4708d715c6f9f9ace509 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 15:10:43 +0100 Subject: [PATCH 09/23] test fix: removed contraction to escape string literals and match server.py --- tests/unit/test_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py index b1644b685..bcd645881 100644 --- a/tests/unit/test_login.py +++ b/tests/unit/test_login.py @@ -21,7 +21,7 @@ def test_unknown_email_returns_error_message(self, client_with_mock_data): data={'email': 'unknown@notfound.com'} ) assert response.status_code == 200 - assert b"Sorry, that email wasn't found." in response.data + assert b"Sorry, that email was not found." in response.data # ----------------- # HAPPY PATH From 217ffc83b97a192144805906b8eb34f506659657 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:21:57 +0100 Subject: [PATCH 10/23] refactor: rename client_with_mock_data to mock_client for brevity --- tests/conftest.py | 8 +++----- tests/unit/test_login.py | 13 +++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 861841441..c396eec89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,12 +80,10 @@ def client(): yield client @pytest.fixture -def client_with_mock_data(client, mock_clubs, mock_competitions, monkeypatch): +def mock_client(client, mock_clubs, mock_competitions, monkeypatch): """ - Provides a Flask test client with fully isolated mock data injected - via monkeypatch. Never reads from or writes to clubs.json or - competitions.json. Use this fixture for all unit and integration tests - to ensure test isolation and repeatable results. + Flask test client with fully isolated mock data injected via monkeypatch. + Use this in all unit and integration tests to ensure test isolation. """ monkeypatch.setattr(server, 'clubs', mock_clubs) monkeypatch.setattr(server, 'competitions', mock_competitions) diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py index bcd645881..4e538fc20 100644 --- a/tests/unit/test_login.py +++ b/tests/unit/test_login.py @@ -11,12 +11,12 @@ class TestLogin: # ----------------- # SAD PATH # ----------------- - def test_unknown_email_returns_error_message(self, client_with_mock_data): + def test_unknown_email_returns_error_message(self, mock_client): """ An unknown email should return 200 with a clear error message instead of crashing with IndexError 500. """ - response = client_with_mock_data.post( + response = mock_client.post( '/showSummary', data={'email': 'unknown@notfound.com'} ) @@ -27,12 +27,12 @@ def test_unknown_email_returns_error_message(self, client_with_mock_data): # HAPPY PATH # ----------------- - def test_valid_email_loads_summary_page(self, client_with_mock_data): + def test_valid_email_loads_summary_page(self, mock_client): """ A valid email should load the summary/welcome page and display the club's name. """ - response = client_with_mock_data.post( + response = mock_client.post( '/showSummary', data={'email': 'john@simplylift.co'} ) @@ -42,12 +42,13 @@ def test_valid_email_loads_summary_page(self, client_with_mock_data): # ----------------- # EDGE CASE # ----------------- - def test_empty_email_returns_error_message(self, client_with_mock_data): + + def test_empty_email_returns_error_message(self, mock_client): """ An empty email submission should return 200 with an error message rather than crashing or returning a server error. """ - response = client_with_mock_data.post( + response = mock_client.post( '/showSummary', data={'email': ''} ) From ed78f83065aa0eebcbaf9d60d4f7e0416402f5b5 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:08:32 +0100 Subject: [PATCH 11/23] chore: defined test cases to run for happy + sad paths + edge cases --- tests/unit/test_booking.py | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/unit/test_booking.py diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py new file mode 100644 index 000000000..ec0c3a1bd --- /dev/null +++ b/tests/unit/test_booking.py @@ -0,0 +1,61 @@ +import pytest + + +class TestPurchasePlaces: + """ + Unit tests for the /purchasePlaces route. + + Issue #6: Competition places are not correctly deducted after booking. + Branch: fix/competition-places-not-deducted + + Verifies that places are correctly deducted from the competition's + available total after a successful booking, and that the type + consistency of numberOfPlaces is maintained after deduction. + """ + + # ----------------- + # HAPPY PATH + # ----------------- + + def test_booking_deducts_places_from_competition(self, client_with_mock_data): + """ + After a valid booking, the competition's numberOfPlaces + should be reduced by the number of places requested. + """ + pass + + def test_booking_reflects_updated_count_in_response(self, client_with_mock_data): + """ + The updated number of places should be reflected + immediately in the response after booking. + """ + pass + + # ----------------- + # SAD PATH + # ----------------- + + # def test_unknown_competition_returns_error(self, client_with_mock_data): + # """ + # If the competition name is not found, the app should + # return an error message without crashing. + # """ + # pass + + # def test_unknown_club_returns_error(self, client_with_mock_data): + # """ + # If the club name is not found, the app should + # return an error message without crashing. + # """ + # pass + + # ----------------- + # EDGE CASES + # ----------------- + + # def test_places_value_remains_integer_after_deduction(self, client_with_mock_data): + # """ + # After deduction, numberOfPlaces should be stored + # as an integer to maintain type consistency across bookings. + # """ + # pass \ No newline at end of file From d6b0498ecf39eea14a4ac28db98129011c418d69 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:23:59 +0100 Subject: [PATCH 12/23] test: add happy path for competition place deduction - issue #6 --- tests/unit/test_booking.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index ec0c3a1bd..ef4681bf1 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -17,19 +17,25 @@ class TestPurchasePlaces: # HAPPY PATH # ----------------- - def test_booking_deducts_places_from_competition(self, client_with_mock_data): + def test_booking_deducts_places_from_competition(self, mock_client): """ After a valid booking, the competition's numberOfPlaces should be reduced by the number of places requested. """ - pass + response = mock_client.post('/purchasePlaces', data={ + 'competition': 'Future Festival', + 'club': 'Simply Lift', + 'places': '3', + }) + assert response.status_code == 200 + assert b'Great-booking complete!' in response.data - def test_booking_reflects_updated_count_in_response(self, client_with_mock_data): - """ - The updated number of places should be reflected - immediately in the response after booking. - """ - pass + # def test_booking_reflects_updated_count_in_response(self, client_with_mock_data): + # """ + # The updated number of places should be reflected + # immediately in the response after booking. + # """ + # pass # ----------------- # SAD PATH From 1b451de3847b8a544400655c1d9a002e6fbf7353 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:32:02 +0100 Subject: [PATCH 13/23] test: add happy path tests for competition place deduction - issue #6 --- tests/unit/test_booking.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index ef4681bf1..de21599a8 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -1,4 +1,5 @@ import pytest +import server class TestPurchasePlaces: @@ -30,25 +31,34 @@ def test_booking_deducts_places_from_competition(self, mock_client): assert response.status_code == 200 assert b'Great-booking complete!' in response.data - # def test_booking_reflects_updated_count_in_response(self, client_with_mock_data): - # """ - # The updated number of places should be reflected - # immediately in the response after booking. - # """ - # pass + def test_booking_reflects_updated_count_in_response(self, mock_client): + """ + Checks the in-memory state directly. + The updated number of places should be reflected + immediately in the response after booking. + """ + mock_client.post('/purchasePlaces', data={ + 'competition': 'Future Festival', + 'club': 'Simply Lift', + 'places': '3', + }) + competition = next( + (c for c in server.competitions if c['name'] == 'Future Festival'), None + ) + assert competition['numberOfPlaces'] == 22 # ----------------- # SAD PATH # ----------------- - # def test_unknown_competition_returns_error(self, client_with_mock_data): + # def test_unknown_competition_returns_error(self, mock_client): # """ # If the competition name is not found, the app should # return an error message without crashing. # """ # pass - # def test_unknown_club_returns_error(self, client_with_mock_data): + # def test_unknown_club_returns_error(self, mock_client): # """ # If the club name is not found, the app should # return an error message without crashing. @@ -59,7 +69,7 @@ def test_booking_deducts_places_from_competition(self, mock_client): # EDGE CASES # ----------------- - # def test_places_value_remains_integer_after_deduction(self, client_with_mock_data): + # def test_places_value_remains_integer_after_deduction(self, mock_client): # """ # After deduction, numberOfPlaces should be stored # as an integer to maintain type consistency across bookings. From ea6a36b43a67f2cdf9c3222c78b3320b5e967b71 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:42:11 +0100 Subject: [PATCH 14/23] test: add sad path for unknown competition - issue #6 --- server.py | 7 ++++++- tests/unit/test_booking.py | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 6a51693aa..4ea38fcee 100644 --- a/server.py +++ b/server.py @@ -46,8 +46,13 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): - competition = [c for c in competitions if c['name'] == request.form['competition']][0] + competition = next((c for c in competitions if c['name'] == request.form['competition']), None) club = [c for c in clubs if c['name'] == request.form['club']][0] + + if not competition: + flash('Something went wrong - please try again.') + return render_template('welcome.html', club=club, competitions=competitions), 200 + placesRequired = int(request.form['places']) competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired flash('Great-booking complete!') diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index de21599a8..209528b4f 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -51,12 +51,18 @@ def test_booking_reflects_updated_count_in_response(self, mock_client): # SAD PATH # ----------------- - # def test_unknown_competition_returns_error(self, mock_client): - # """ - # If the competition name is not found, the app should - # return an error message without crashing. - # """ - # pass + def test_unknown_competition_returns_error(self, mock_client): + """ + If the competition name is not found, the app should + return an error message without crashing. + """ + response = mock_client.post('/purchasePlaces', data={ + 'competition': 'Unknown Competition', + 'club': 'Simply Lift', + 'places': '3', + }) + assert response.status_code == 200 + assert b'Something went wrong' in response.data # def test_unknown_club_returns_error(self, mock_client): # """ From d262556de8ae19d6ca2c032c8f2ea259c0eb5eb2 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:46:18 +0100 Subject: [PATCH 15/23] test: add sad path for unknown club - issue #6 --- tests/unit/test_booking.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index 209528b4f..efa2b6d96 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -64,12 +64,18 @@ def test_unknown_competition_returns_error(self, mock_client): assert response.status_code == 200 assert b'Something went wrong' in response.data - # def test_unknown_club_returns_error(self, mock_client): - # """ - # If the club name is not found, the app should - # return an error message without crashing. - # """ - # pass + def test_unknown_club_returns_error(self, mock_client): + """ + If the club name is not found, the app should + return an error message without crashing. + """ + response = mock_client.post('/purchasePlaces', data={ + 'competition': 'Future Festival', + 'club': 'Unknown Club', + 'places': '3', + }) + assert response.status_code == 200 + assert b'Something went wrong' in response.data # ----------------- # EDGE CASES From 974ae2719b692fa1367f8811a19f4d97382d68ba Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:46:26 +0100 Subject: [PATCH 16/23] fix: handle unknown club with next() and guard clause - issue #6 --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 4ea38fcee..d74fdfb00 100644 --- a/server.py +++ b/server.py @@ -47,9 +47,9 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): competition = next((c for c in competitions if c['name'] == request.form['competition']), None) - club = [c for c in clubs if c['name'] == request.form['club']][0] + club = next((c for c in clubs if c['name'] == request.form['club']), None) - if not competition: + if not competition or not club: flash('Something went wrong - please try again.') return render_template('welcome.html', club=club, competitions=competitions), 200 From 0715257584f6b12363d01e671538839821c5c121 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:51:58 +0100 Subject: [PATCH 17/23] test: add characterisation test for numberOfPlaces type consistency - issue #6 --- tests/unit/test_booking.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index efa2b6d96..e207139fd 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -81,9 +81,21 @@ def test_unknown_club_returns_error(self, mock_client): # EDGE CASES # ----------------- - # def test_places_value_remains_integer_after_deduction(self, mock_client): - # """ - # After deduction, numberOfPlaces should be stored - # as an integer to maintain type consistency across bookings. - # """ - # pass \ No newline at end of file + def test_places_value_remains_integer_after_deduction(self, mock_client): + """ + After deduction, numberOfPlaces should be stored + as an integer to maintain type consistency across bookings. + """ + mock_client.post("/purchasePlaces", data={ + 'competition': 'Future Festival', + 'club': 'Simply Lift', + 'places': '3', + }) + competition = next( + (c for c in server.competitions if c['name'] == 'Future Festival'), None + ) + + # Verify type consistency — numberOfPlaces must be int after deduction, + # not a string as it was originally stored in the JSON file + assert isinstance(competition['numberOfPlaces'], int) + assert competition['numberOfPlaces'] == 22 From 7561de370f69d172addb30ac7ec34a2835bcedf9 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 16:56:07 +0100 Subject: [PATCH 18/23] refactor: PEP8 fixes in purchasePlaces - snake_case and spacing --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index d74fdfb00..28296c10c 100644 --- a/server.py +++ b/server.py @@ -53,8 +53,8 @@ def purchasePlaces(): flash('Something went wrong - please try again.') return render_template('welcome.html', club=club, competitions=competitions), 200 - placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired + places_required = int(request.form['places']) + competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - places_required flash('Great-booking complete!') return render_template('welcome.html', club=club, competitions=competitions) From e1014753571ef1ce157757f93971f750506ddebe Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 17:06:59 +0100 Subject: [PATCH 19/23] refactor: PEP8 fixes in show_summary - snake_case and spacing --- server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index 28296c10c..065c21995 100644 --- a/server.py +++ b/server.py @@ -24,13 +24,13 @@ def loadCompetitions(): def index(): return render_template('index.html') -@app.route('/showSummary',methods=['POST']) -def showSummary(): +@app.route('/showSummary', methods=['POST']) +def show_summary(): club = next((club for club in clubs if club['email'] == request.form['email']), None) if not club: flash("Sorry, that email was not found.") return render_template('index.html'), 200 - return render_template('welcome.html',club=club,competitions=competitions) + return render_template('welcome.html', club=club, competitions=competitions) @app.route('/book//') From b48a1bf648c808d78a0db5a396d9c5a58bdf667a Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 17:19:38 +0100 Subject: [PATCH 20/23] refactor: PEP8 fixes in purchasePlaces - snake_case in function name --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 065c21995..31847a329 100644 --- a/server.py +++ b/server.py @@ -44,8 +44,8 @@ def book(competition,club): return render_template('welcome.html', club=club, competitions=competitions) -@app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): +@app.route('/purchasePlaces', methods=['POST']) +def purchase_places(): competition = next((c for c in competitions if c['name'] == request.form['competition']), None) club = next((c for c in clubs if c['name'] == request.form['club']), None) From c903b317e4ab3aba91c8154921d6f05c07c6c201 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 17:43:20 +0100 Subject: [PATCH 21/23] refactor: use pytest.mark.parametrize for sad path tests - issue #2 --- tests/unit/test_login.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py index 4e538fc20..e8e860971 100644 --- a/tests/unit/test_login.py +++ b/tests/unit/test_login.py @@ -8,17 +8,20 @@ class TestLogin: Branch: fix/login-unknown-email-crash """ - # ----------------- - # SAD PATH - # ----------------- - def test_unknown_email_returns_error_message(self, mock_client): + @pytest.mark.parametrize("email", [ + "unknown@notfound.com", + "", + "notanemail", + ]) + def test_invalid_email_returns_error_message(self, mock_client, email): """ - An unknown email should return 200 with a clear error - message instead of crashing with IndexError 500. + Any email that does not match a club in the database + should return 200 with a graceful error message — not crash. + Covers unknown, empty, and malformed email inputs. """ response = mock_client.post( '/showSummary', - data={'email': 'unknown@notfound.com'} + data={'email': email} ) assert response.status_code == 200 assert b"Sorry, that email was not found." in response.data @@ -38,19 +41,3 @@ def test_valid_email_loads_summary_page(self, mock_client): ) assert response.status_code == 200 assert b"Welcome" in response.data - - # ----------------- - # EDGE CASE - # ----------------- - - def test_empty_email_returns_error_message(self, mock_client): - """ - An empty email submission should return 200 with an - error message rather than crashing or returning a server error. - """ - response = mock_client.post( - '/showSummary', - data={'email': ''} - ) - assert response.status_code == 200 - assert b"Sorry, that email was not found." in response.data From 34945eaeaaed82ddb1dcf73849f2da802a772681 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 17:45:36 +0100 Subject: [PATCH 22/23] refactor: added missing annotation for sad paths/paramaterize --- tests/unit/test_login.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/test_login.py b/tests/unit/test_login.py index e8e860971..58760fb4a 100644 --- a/tests/unit/test_login.py +++ b/tests/unit/test_login.py @@ -8,6 +8,10 @@ class TestLogin: Branch: fix/login-unknown-email-crash """ + # ----------------- + # SAD PATH + # ----------------- + @pytest.mark.parametrize("email", [ "unknown@notfound.com", "", From 637dbd976021f355af41cb612ccb0c1458379189 Mon Sep 17 00:00:00 2001 From: SiRipo92 Date: Fri, 13 Mar 2026 17:50:34 +0100 Subject: [PATCH 23/23] refactor: use pytest.mark.parametrize for sad path tests - issue #6 --- tests/unit/test_booking.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/unit/test_booking.py b/tests/unit/test_booking.py index e207139fd..39e4e9519 100644 --- a/tests/unit/test_booking.py +++ b/tests/unit/test_booking.py @@ -51,28 +51,19 @@ def test_booking_reflects_updated_count_in_response(self, mock_client): # SAD PATH # ----------------- - def test_unknown_competition_returns_error(self, mock_client): + @pytest.mark.parametrize("competition,club", [ + ("Unknown Competition", "Simply Lift"), + ("Future Festival", "Unknown Club"), + ]) + def test_invalid_booking_data_returns_error(self, mock_client, competition, club): """ - If the competition name is not found, the app should - return an error message without crashing. + If either competition or club name is not found, + the app should return an error message without crashing. """ response = mock_client.post('/purchasePlaces', data={ - 'competition': 'Unknown Competition', - 'club': 'Simply Lift', - 'places': '3', - }) - assert response.status_code == 200 - assert b'Something went wrong' in response.data - - def test_unknown_club_returns_error(self, mock_client): - """ - If the club name is not found, the app should - return an error message without crashing. - """ - response = mock_client.post('/purchasePlaces', data={ - 'competition': 'Future Festival', - 'club': 'Unknown Club', - 'places': '3', + 'competition': competition, + 'club': club, + 'places': '3' }) assert response.status_code == 200 assert b'Something went wrong' in response.data