From 20736bd00156fad2e6e315a451c8b3e5bb0fb667 Mon Sep 17 00:00:00 2001 From: Imenbr Date: Wed, 29 May 2024 17:32:43 +0200 Subject: [PATCH 1/6] (fix) handle unknown email in login --- .coverage | Bin 0 -> 53248 bytes .coveragerc | 6 ++ .flake8 | 12 +++ .gitignore | 5 +- pyproject.toml | 2 + pytest.ini | 3 + requirements.txt | 3 + server.py | 81 +++++++++----- templates/index.html | 13 ++- templates/welcome.html | 6 +- tests/__init__.py | 0 tests/integration_tests/__init__.py | 0 tests/integration_tests/conftest.py | 48 +++++++++ tests/integration_tests/test_unknown_email.py | 24 +++++ tests/unit_tests/__init__.py | 0 tests/unit_tests/conftest.py | 8 ++ tests/unit_tests/test_unknown_email.py | 101 ++++++++++++++++++ tests/unit_tests/utils.py | 23 ++++ 18 files changed, 301 insertions(+), 34 deletions(-) create mode 100644 .coverage create mode 100644 .coveragerc create mode 100644 .flake8 create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/integration_tests/__init__.py create mode 100644 tests/integration_tests/conftest.py create mode 100644 tests/integration_tests/test_unknown_email.py create mode 100644 tests/unit_tests/__init__.py create mode 100644 tests/unit_tests/conftest.py create mode 100644 tests/unit_tests/test_unknown_email.py create mode 100644 tests/unit_tests/utils.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..e361004f7f162b57579d9fcceca1f3ab6d62707d GIT binary patch literal 53248 zcmeI)+i%-c90zdQr%RSJ!9&xCs%rEI3EHY{LN&25co+rxFfnNx6kgECoaChOi0#aF z*5(0C-H;|B@dtnq{3-kkpz%KO0(+UzB$$wfRDM5Sa@m@#+RL=&YvtnD=NzB&Ikz|$ ze{%7h9V*%Me8&pq3GIlc>)Lx#YMPd#dy($xlBJcY^aVZZL+j&Kb6WYzFGce+t&sUv zGruUFH&+Tj6yKQswczBwnf@_rP5R!J>?mJ0?Y5G(+pz0aXuF#-Y^gMKH&6|p z=x7^FbsVr9#pkuE-Jn>Z+N2~pzU^3kPkyO-%Ta*jXrbB;;|-LEvNv50As3S9i_%w3 z^v(<7y!4TGYbZBjyY&u?({-BNMoSmM8!io{b(srvIgXhe zYe;7hd5@S$@Fa4RGUs|J>mVW@nj%MecDqiE$ggyr4p$Dvhz}fk<6@4B8+u7$_gy!8 z)a9VOBh{IbbB)Hax~_e-tNN9{EC(@{qhh$;vgATg3wV7xQ#yTA*Qiyh?ci#g-fvZ{ zZsLxxbv0@z%u}t|VbEX6jWnA6<}po&_m6Kk+)G9q3HPFQ zTYAl?%W!NNR}Qu2;XYGIF)W`un)H%@l+)+h+Bmb62U%--wb8Z~wU|Ir2cfYb4c6?s zb?zzwm!a-mQ@*vS%E{h%Sa{wIiJH!8E>l{c8`pIDO`y8kU(N5#WU{5%S^Y*XYDP(I zB~RIrCY09X9>Zw5%TZR)_0xgmNrPk>V=zjleI(gK#aYQ_N^7&@ij#z^t|lvocd}F2 z(y?RuPTV7ST;ltokwWm6K^J&rBG`ZSJ670|kLR5?Q^w~HWlHZJ8<%m;LUolNGdDjs zqAcH0uX=z1Rwwb2tWV=5P$##AOL}95jZJ? z7F0eB=voGqD_)N-pVCm33%#)Axz!C7@I{I$p>jHHy2e0IiLMD#ZiP`h~^M73b4{a0z5P$##AOHafKmY;| zfB*y_aG(VQ{Z63B-~a38e;VDeKmY;|fB*y_009U<00Izz00ba#00o37=HmbV-{$N8 z-!X3=z(S%(5P$##AOHafKmY;|fB*y_0D*%kkQY;W?iKN1clT%gC4<&qH6C35_l&l? zdtI3NR4(`0AH9=*{&c6HWwb*4|NncM`Oy5^ymv4`P$~#O00Izz00bZa0SG_<0uX?} zL`+x1oL@A)K5P$##AOHafKmY;|fB*y_0D*}U;P3x&{y%Zm zjsimf0uX=z1Rwwb2tWV=5P-mh3B>>Zzt6w_e_-C9u)?Fb5P$##AOHafKmY;|fB*y_ T0D*%lAOv0YUl{zr*Z=/') -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 +}) From 4e1e43e4631a3fdafc4d703ada32ffe04d5b41c6 Mon Sep 17 00:00:00 2001 From: Imenbr Date: Fri, 31 May 2024 00:45:47 +0200 Subject: [PATCH 2/6] (fix) Handle redeeming more points than available --- .coverage | Bin 53248 -> 53248 bytes requirements.txt | 2 +- server.py | 67 +++++++++++--- templates/booking.html | 12 ++- tests/integration_tests/conftest.py | 32 +++---- ...s_should_not_use_more_than_their_points.py | 44 +++++++++ ...s_should_not_use_more_than_their_points.py | 86 ++++++++++++++++++ tests/unit_tests/utils.py | 26 +++--- 8 files changed, 227 insertions(+), 42 deletions(-) create mode 100644 tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py create mode 100644 tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py diff --git a/.coverage b/.coverage index e361004f7f162b57579d9fcceca1f3ab6d62707d..5c65d975222eea263bfde8c47648bae7c1596e07 100644 GIT binary patch delta 259 zcmZozz}&Eac>{|B4-3CK1OH|Inf&qm>YD`x#Q9mxSeO}#C(rZOoZRov&B#1?w!aR0 zQht7RW?uT_DgFgO9U2V$SNIe7HTY)%bx80}PKq~YWwB%~X9Vejsogv!zCZz}_#gxS zEB;&jXZa5T6>sGC=VD=D

B1_W%EWMs8*xTY?$L{{LT^j|s?;WBOMA1H`Ro7GY%J zZwh7Xt$W3;#0){@?sx_}}tB0~&gbpPdb;m63&I^V|92 hAUofHq`&ch{|B7c+k%1OH|Inf!^H1qFinS{VECnv<~ZayAws{oX~%)tMi|33d^p!89G zYc6IMMouy2Z~y=AXXIl7vgMe*)&H<(`2WA2nV*q`lT+zgrP=fO_jwrD7^Xai-S!42on6p{}Cwon4g~+=tf> int(club["points"]): + return "Places required exceed club's total points" + + +def take_places(places, club, competition): + try: + competition["numberOfPlaces"] = \ + int(competition["numberOfPlaces"]) - places + club["points"] = int(club["points"]) - places + return True + except Exception: + return False + + @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] + competition = get_competition_from_name(request.form["competition"]) + club = get_club_from_name(request.form["club"]) + + error_message = check_places(request.form["places"], club) + if error_message: + flash(error_message) + return redirect( + url_for("book", competition=competition["name"], club=club["name"]) + ) 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 + if take_places(placesRequired, club, competition): + flash("Great-booking complete!") + return render_template("welcome.html", club=club, + competitions=competitions) + else: + flash("Something went wrong-please try again") + return redirect( + url_for("book", competition=competition["name"], club=club["name"]) + ) @app.route("/logout") diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..5037049c1 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -1,3 +1,4 @@ + @@ -6,11 +7,20 @@

{{competition['name']}}

+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {% endwith %} Places available: {{competition['numberOfPlaces']}}
- +
diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 4cf8514f0..1c4b1477c 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -3,23 +3,21 @@ 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"}, - ] - } -) +mock_clubs_json = json.dumps({ + "clubs": [ + {"name": "Club 1", "email": "club1@example.com", "points": "10"}, + {"name": "Club 2", "email": "club2@example.com", "points": "15"} + ] +}) + +mock_competitions_json = json.dumps({ + "competitions": [ + {"name": "Competition 1", "date": "2023-03-27 10:00:00", + "numberOfPlaces": "25"}, + {"name": "Competition 2", "date": "2023-10-22 13:30:00", + "numberOfPlaces": "15"} + ] +}) def mocked_open(file, *args, **kwargs): diff --git a/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py new file mode 100644 index 000000000..985ae9115 --- /dev/null +++ b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py @@ -0,0 +1,44 @@ + + +def test_purchase_places_valid(client): + """ + Test purchasing places with valid club points and valid competition. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "5" + }) + + assert response.status_code == 200 + assert b"Great-booking complete!" in response.data + + +def test_purchase_places_insufficient_points(client): + """ + Test purchasing more places than the club's available points. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "20" # Club 1 has only 10 points + }) + + assert response.status_code == 302 # Redirect expected + response = client.get(response.headers["Location"]) # Follow the redirect + assert b"Places required exceed club's total points" in response.data + + +def test_purchase_places_zero_places(client): + """ + Test purchasing zero places, which is an invalid input. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "0" + }) + + assert response.status_code == 302 # Redirect expected + response = client.get(response.headers["Location"]) # Follow the redirect + assert b"Places required must be a positive integer" in response.data diff --git a/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py new file mode 100644 index 000000000..8387ba263 --- /dev/null +++ b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py @@ -0,0 +1,86 @@ +from unittest.mock import patch +from .utils import mock_clubs_list, mock_competitions_list + + +# Unit Tests for get_competition_from_name +@patch("server.competitions", mock_competitions_list) +def test_get_competition_from_name_valid(): + from server import get_competition_from_name + + competition = get_competition_from_name("Competition 1") + assert competition is not None + assert competition["name"] == "Competition 1" + + +@patch("server.competitions", mock_competitions_list) +def test_get_competition_from_name_invalid(): + from server import get_competition_from_name + + competition = get_competition_from_name("Invalid Competition") + assert competition is None + + +# Unit Tests for get_club_from_name ### +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_name_valid(): + from server import get_club_from_name + + club = get_club_from_name("Club 1") + assert club is not None + assert club["name"] == "Club 1" + + +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_name_invalid(): + from server import get_club_from_name + + club = get_club_from_name("Invalid Club") + assert club is None + + +# Unit Tests for check_places ### +def test_check_places_valid(): + from server import check_places + + error = check_places("5", mock_clubs_list[0]) + assert error is None + + +def test_check_places_invalid_zero(): + from server import check_places + + error = check_places("0", mock_clubs_list[0]) + assert error == "Places required must be a positive integer" + + +def test_check_places_invalid_negative(): + from server import check_places + + error = check_places("-3", mock_clubs_list[0]) + assert error == "Places required must be a positive integer" + + +def test_check_places_exceeds_points(): + from server import check_places + + error = check_places("20", mock_clubs_list[0]) + assert error == "Places required exceed club's total points" + + +# Unit Tests for take_places ### +def test_take_places_valid(): + from server import take_places + + result = take_places(5, mock_clubs_list[0], mock_competitions_list[0]) + assert result is True + assert mock_clubs_list[0]["points"] == 5 # 10 - 5 + assert mock_competitions_list[0]["numberOfPlaces"] == 20 # 25 - 5 + + +def test_take_places_invalid(): + from server import take_places + + result = take_places( + "invalid", mock_clubs_list[0], mock_competitions_list[0] + ) + assert result is False diff --git a/tests/unit_tests/utils.py b/tests/unit_tests/utils.py index 3a095deab..233e88226 100644 --- a/tests/unit_tests/utils.py +++ b/tests/unit_tests/utils.py @@ -1,23 +1,25 @@ - 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"} + {"name": "Club 1", "email": "club1@example.com", "points": "10"}, + {"name": "Club 2", "email": "club2@example.com", "points": "15"}, ] mock_competitions_list = [ - {"name": "Competition 1", "numberOfPlaces": "25"}, - {"name": "Competition 2", "numberOfPlaces": "15"} + { + "name": "Competition 1", + "date": "2023-03-27 10:00:00", + "numberOfPlaces": "25", + }, + { + "name": "Competition 2", + "date": "2023-10-22 13:30:00", + "numberOfPlaces": "15", + }, ] - # Mock data for clubs.json and competitions.json -mock_clubs_json = json.dumps({ - "clubs": mock_clubs_list -}) +mock_clubs_json = json.dumps({"clubs": mock_clubs_list}) -mock_competitions_json = json.dumps({ - "competitions": mock_competitions_list -}) +mock_competitions_json = json.dumps({"competitions": mock_competitions_list}) From 247bb57d4fcf8a3b0c8e66ca9affa1d5368df0cb Mon Sep 17 00:00:00 2001 From: Imenbr Date: Fri, 31 May 2024 00:45:47 +0200 Subject: [PATCH 3/6] (fix) Handle redeeming more points than available --- .coverage | Bin 53248 -> 53248 bytes requirements.txt | 2 +- server.py | 67 ++++- templates/booking.html | 12 +- tests/integration_tests/conftest.py | 32 ++- ...s_should_not_use_more_than_their_points.py | 44 ++++ ...s_should_not_use_more_than_their_points.py | 229 ++++++++++++++++++ tests/unit_tests/utils.py | 26 +- 8 files changed, 370 insertions(+), 42 deletions(-) create mode 100644 tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py create mode 100644 tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py diff --git a/.coverage b/.coverage index e361004f7f162b57579d9fcceca1f3ab6d62707d..5c65d975222eea263bfde8c47648bae7c1596e07 100644 GIT binary patch delta 259 zcmZozz}&Eac>{|B4-3CK1OH|Inf&qm>YD`x#Q9mxSeO}#C(rZOoZRov&B#1?w!aR0 zQht7RW?uT_DgFgO9U2V$SNIe7HTY)%bx80}PKq~YWwB%~X9Vejsogv!zCZz}_#gxS zEB;&jXZa5T6>sGC=VD=D

B1_W%EWMs8*xTY?$L{{LT^j|s?;WBOMA1H`Ro7GY%J zZwh7Xt$W3;#0){@?sx_}}tB0~&gbpPdb;m63&I^V|92 hAUofHq`&ch{|B7c+k%1OH|Inf!^H1qFinS{VECnv<~ZayAws{oX~%)tMi|33d^p!89G zYc6IMMouy2Z~y=AXXIl7vgMe*)&H<(`2WA2nV*q`lT+zgrP=fO_jwrD7^Xai-S!42on6p{}Cwon4g~+=tf> int(club["points"]): + return "Places required exceed club's total points" + + +def take_places(places, club, competition): + try: + competition["numberOfPlaces"] = \ + int(competition["numberOfPlaces"]) - places + club["points"] = int(club["points"]) - places + return True + except Exception: + return False + + @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] + competition = get_competition_from_name(request.form["competition"]) + club = get_club_from_name(request.form["club"]) + + error_message = check_places(request.form["places"], club) + if error_message: + flash(error_message) + return redirect( + url_for("book", competition=competition["name"], club=club["name"]) + ) 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 + if take_places(placesRequired, club, competition): + flash("Great-booking complete!") + return render_template("welcome.html", club=club, + competitions=competitions) + else: + flash("Something went wrong-please try again") + return redirect( + url_for("book", competition=competition["name"], club=club["name"]) + ) @app.route("/logout") diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..5037049c1 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -1,3 +1,4 @@ + @@ -6,11 +7,20 @@

{{competition['name']}}

+ {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {% endwith %} Places available: {{competition['numberOfPlaces']}}
- +
diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 4cf8514f0..1c4b1477c 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -3,23 +3,21 @@ 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"}, - ] - } -) +mock_clubs_json = json.dumps({ + "clubs": [ + {"name": "Club 1", "email": "club1@example.com", "points": "10"}, + {"name": "Club 2", "email": "club2@example.com", "points": "15"} + ] +}) + +mock_competitions_json = json.dumps({ + "competitions": [ + {"name": "Competition 1", "date": "2023-03-27 10:00:00", + "numberOfPlaces": "25"}, + {"name": "Competition 2", "date": "2023-10-22 13:30:00", + "numberOfPlaces": "15"} + ] +}) def mocked_open(file, *args, **kwargs): diff --git a/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py new file mode 100644 index 000000000..985ae9115 --- /dev/null +++ b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py @@ -0,0 +1,44 @@ + + +def test_purchase_places_valid(client): + """ + Test purchasing places with valid club points and valid competition. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "5" + }) + + assert response.status_code == 200 + assert b"Great-booking complete!" in response.data + + +def test_purchase_places_insufficient_points(client): + """ + Test purchasing more places than the club's available points. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "20" # Club 1 has only 10 points + }) + + assert response.status_code == 302 # Redirect expected + response = client.get(response.headers["Location"]) # Follow the redirect + assert b"Places required exceed club's total points" in response.data + + +def test_purchase_places_zero_places(client): + """ + Test purchasing zero places, which is an invalid input. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "0" + }) + + assert response.status_code == 302 # Redirect expected + response = client.get(response.headers["Location"]) # Follow the redirect + assert b"Places required must be a positive integer" in response.data diff --git a/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py new file mode 100644 index 000000000..5170fd669 --- /dev/null +++ b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py @@ -0,0 +1,229 @@ +from unittest.mock import patch +from .utils import mock_clubs_list, mock_competitions_list + + +# Unit Tests for get_competition_from_name +@patch("server.competitions", mock_competitions_list) +def test_get_competition_from_name_valid(): + from server import get_competition_from_name + + competition = get_competition_from_name("Competition 1") + assert competition is not None + assert competition["name"] == "Competition 1" + + +@patch("server.competitions", mock_competitions_list) +def test_get_competition_from_name_invalid(): + from server import get_competition_from_name + + competition = get_competition_from_name("Invalid Competition") + assert competition is None + + +# Unit Tests for get_club_from_name ### +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_name_valid(): + from server import get_club_from_name + + club = get_club_from_name("Club 1") + assert club is not None + assert club["name"] == "Club 1" + + +@patch("server.clubs", mock_clubs_list) +def test_get_club_from_name_invalid(): + from server import get_club_from_name + + club = get_club_from_name("Invalid Club") + assert club is None + + +# Unit Tests for check_places ### +def test_check_places_valid(): + from server import check_places + + error = check_places("5", mock_clubs_list[0]) + assert error is None + + +def test_check_places_invalid_zero(): + from server import check_places + + error = check_places("0", mock_clubs_list[0]) + assert error == "Places required must be a positive integer" + + +def test_check_places_invalid_negative(): + from server import check_places + + error = check_places("-3", mock_clubs_list[0]) + assert error == "Places required must be a positive integer" + + +def test_check_places_exceeds_points(): + from server import check_places + + error = check_places("20", mock_clubs_list[0]) + assert error == "Places required exceed club's total points" + + +# Unit Tests for take_places ### +def test_take_places_valid(): + from server import take_places + + result = take_places(5, mock_clubs_list[0], mock_competitions_list[0]) + assert result is True + assert mock_clubs_list[0]["points"] == 5 # 10 - 5 + assert mock_competitions_list[0]["numberOfPlaces"] == 20 # 25 - 5 + + +def test_take_places_invalid(): + from server import take_places + + result = take_places( + "invalid", mock_clubs_list[0], mock_competitions_list[0] + ) + assert result is False + + +# Unit test for valid booking +@patch("server.competitions", mock_competitions_list) +@patch("server.render_template") +@patch("server.take_places") +@patch("server.check_places") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +@patch("server.flash") +def test_purchase_places_valid( + mock_flash, + mock_get_competition, + mock_get_club, + mock_check_places, + mock_take_places, + mock_render_template, + client, +): + # Mock the functions to return valid data + mock_get_competition.return_value = mock_competitions_list[0] + mock_get_club.return_value = mock_clubs_list[0] + mock_check_places.return_value = None # No error message + mock_take_places.return_value = True # Simulate successful booking + + # Simulate POST request to the route + response = client.post( + "/purchasePlaces", + data={"competition": "Competition 1", "club": "Club 1", "places": "5"}, + ) + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_check_places.assert_called_once_with("5", mock_clubs_list[0]) + mock_take_places.assert_called_once_with( + 5, mock_clubs_list[0], mock_competitions_list[0] + ) + mock_flash.assert_called_once_with("Great-booking complete!") + mock_render_template.assert_called_once_with( + "welcome.html", + club=mock_clubs_list[0], + competitions=mock_competitions_list, + ) + + # Check response status code + assert response.status_code == 200 + + +# Unit test for booking with error in places +@patch("server.redirect") +@patch("server.url_for") +@patch("server.flash") +@patch("server.check_places") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_purchase_places_invalid_places( + mock_get_competition, + mock_get_club, + mock_check_places, + mock_flash, + mock_url_for, + mock_redirect, + client, +): + # Mock the functions to simulate an error in places + mock_get_competition.return_value = mock_competitions_list[0] + mock_get_club.return_value = mock_clubs_list[0] + mock_check_places.return_value = ( + "Places required exceed club's total points" + ) + + # Mock the redirect and url_for to return a redirect response + mock_url_for.return_value = "/book" + + # Simulate POST request to the route + client.post( + "/purchasePlaces", + data={"competition": "Competition 1", "club": "Club 1", "places": "20"}, + ) + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_check_places.assert_called_once_with("20", mock_clubs_list[0]) + mock_flash.assert_called_once_with( + "Places required exceed club's total points" + ) + mock_url_for.assert_called_once_with( + "book", + competition=mock_competitions_list[0]["name"], + club=mock_clubs_list[0]["name"], + ) + mock_redirect.assert_called_once_with("/book") + + +# Unit test for failed booking due to take_places failure +@patch("server.redirect") +@patch("server.url_for") +@patch("server.flash") +@patch("server.take_places") +@patch("server.check_places") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_purchase_places_failed_take_places( + mock_get_competition, + mock_get_club, + mock_check_places, + mock_take_places, + mock_flash, + mock_url_for, + mock_redirect, + client, +): + # Mock the functions to simulate an error in take_places + mock_get_competition.return_value = mock_competitions_list[0] + mock_get_club.return_value = mock_clubs_list[0] + mock_check_places.return_value = None # No error message + mock_take_places.return_value = False # Simulate failure in taking places + + # Mock the redirect and url_for to return a redirect response + mock_url_for.return_value = "/book" + + # Simulate POST request to the route + client.post( + "/purchasePlaces", + data={"competition": "Competition 1", "club": "Club 1", "places": "5"}, + ) + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_check_places.assert_called_once_with("5", mock_clubs_list[0]) + mock_take_places.assert_called_once_with( + 5, mock_clubs_list[0], mock_competitions_list[0] + ) + mock_flash.assert_called_once_with("Something went wrong-please try again") + mock_url_for.assert_called_once_with( + "book", + competition=mock_competitions_list[0]["name"], + club=mock_clubs_list[0]["name"], + ) + mock_redirect.assert_called_once_with("/book") diff --git a/tests/unit_tests/utils.py b/tests/unit_tests/utils.py index 3a095deab..233e88226 100644 --- a/tests/unit_tests/utils.py +++ b/tests/unit_tests/utils.py @@ -1,23 +1,25 @@ - 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"} + {"name": "Club 1", "email": "club1@example.com", "points": "10"}, + {"name": "Club 2", "email": "club2@example.com", "points": "15"}, ] mock_competitions_list = [ - {"name": "Competition 1", "numberOfPlaces": "25"}, - {"name": "Competition 2", "numberOfPlaces": "15"} + { + "name": "Competition 1", + "date": "2023-03-27 10:00:00", + "numberOfPlaces": "25", + }, + { + "name": "Competition 2", + "date": "2023-10-22 13:30:00", + "numberOfPlaces": "15", + }, ] - # Mock data for clubs.json and competitions.json -mock_clubs_json = json.dumps({ - "clubs": mock_clubs_list -}) +mock_clubs_json = json.dumps({"clubs": mock_clubs_list}) -mock_competitions_json = json.dumps({ - "competitions": mock_competitions_list -}) +mock_competitions_json = json.dumps({"competitions": mock_competitions_list}) From be366bec1a434736fbc4b561103adfa9e6e08db2 Mon Sep 17 00:00:00 2001 From: Imenbr Date: Thu, 5 Sep 2024 11:40:45 +0200 Subject: [PATCH 4/6] (fix) Handle booking more 12 places --- server.py | 16 ++++-- templates/booking.html | 2 +- .../test_clubs_should_not_use_more_than_12.py | 15 +++++ ...s_should_not_use_more_than_their_points.py | 2 +- .../test_clubs_should_not_use_more_than_12.py | 57 +++++++++++++++++++ ...s_should_not_use_more_than_their_points.py | 2 +- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 tests/integration_tests/test_clubs_should_not_use_more_than_12.py create mode 100644 tests/unit_tests/test_clubs_should_not_use_more_than_12.py diff --git a/server.py b/server.py index 3f7669913..188eec454 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,10 @@ import json from flask import Flask, render_template, request, redirect, flash, url_for +app = Flask(__name__) +app.secret_key = "something_special" +app.config["COMPETITIONS_FILE"] = "competitions.json" + def loadClubs(): with open("clubs.json") as c: @@ -9,14 +13,11 @@ def loadClubs(): def loadCompetitions(): - with open("competitions.json") as comps: + with open(app.config["COMPETITIONS_FILE"], "r") as comps: listOfCompetitions = json.load(comps)["competitions"] return listOfCompetitions -app = Flask(__name__) -app.secret_key = "something_special" - competitions = loadCompetitions() clubs = loadClubs() @@ -86,6 +87,9 @@ def get_club_from_name(name): def check_places(places, club): if not places or int(places) < 1: return "Places required must be a positive integer" + if int(places) > 12: + return ("Places required must be a positive integer " + "that does not exceed 12") if int(places) > int(club["points"]): return "Places required exceed club's total points" @@ -127,3 +131,7 @@ def purchasePlaces(): @app.route("/logout") def logout(): return redirect(url_for("index")) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/templates/booking.html b/templates/booking.html index 5037049c1..b0f4938ff 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -20,7 +20,7 @@

{{competition['name']}}

- +
diff --git a/tests/integration_tests/test_clubs_should_not_use_more_than_12.py b/tests/integration_tests/test_clubs_should_not_use_more_than_12.py new file mode 100644 index 000000000..4780a5440 --- /dev/null +++ b/tests/integration_tests/test_clubs_should_not_use_more_than_12.py @@ -0,0 +1,15 @@ + +def test_purchase_places_exceeds_12(client): + """ + Test purchasing more places than 12. + """ + response = client.post("/purchasePlaces", data={ + "competition": "Competition 1", + "club": "Club 1", + "places": "13" + }) + + assert response.status_code == 302 # Redirect expected + response = client.get(response.headers["Location"]) # Follow the redirect + assert b"Places required must be a positive integer" + b" that does not exceed 12" in response.data diff --git a/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py index 985ae9115..c762890a3 100644 --- a/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py +++ b/tests/integration_tests/test_clubs_should_not_use_more_than_their_points.py @@ -21,7 +21,7 @@ def test_purchase_places_insufficient_points(client): response = client.post("/purchasePlaces", data={ "competition": "Competition 1", "club": "Club 1", - "places": "20" # Club 1 has only 10 points + "places": "11" # Club 1 has only 10 points }) assert response.status_code == 302 # Redirect expected diff --git a/tests/unit_tests/test_clubs_should_not_use_more_than_12.py b/tests/unit_tests/test_clubs_should_not_use_more_than_12.py new file mode 100644 index 000000000..a7c4bfff4 --- /dev/null +++ b/tests/unit_tests/test_clubs_should_not_use_more_than_12.py @@ -0,0 +1,57 @@ +from unittest.mock import patch +from .utils import mock_clubs_list, mock_competitions_list + + +def test_check_places_invalid_more_than_12(): + from server import check_places + + error = check_places("13", mock_clubs_list[0]) + assert error == ("Places required must be a positive" + " integer that does not exceed 12") + + +# Unit test for booking with error in places +@patch("server.redirect") +@patch("server.url_for") +@patch("server.flash") +@patch("server.check_places") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_purchase_places_invalid_places( + mock_get_competition, + mock_get_club, + mock_check_places, + mock_flash, + mock_url_for, + mock_redirect, + client, +): + # Mock the functions to simulate an error in places + mock_get_competition.return_value = mock_competitions_list[0] + mock_get_club.return_value = mock_clubs_list[0] + mock_check_places.return_value = ( + "Places required must be a positive integer that does not exceed 12" + ) + + # Mock the redirect and url_for to return a redirect response + mock_url_for.return_value = "/book" + + # Simulate POST request to the route + client.post( + "/purchasePlaces", + data={"competition": "Competition 1", "club": "Club 1", "places": "20"}, + ) + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_check_places.assert_called_once_with("20", mock_clubs_list[0]) + mock_flash.assert_called_once_with( + "Places required must be a positive integer that does not exceed 12" + ) + mock_url_for.assert_called_once_with( + "book", + competition=mock_competitions_list[0]["name"], + club=mock_clubs_list[0]["name"], + ) + mock_redirect.assert_called_once_with("/book") diff --git a/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py index 5170fd669..0b30e47b0 100644 --- a/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py +++ b/tests/unit_tests/test_clubs_should_not_use_more_than_their_points.py @@ -63,7 +63,7 @@ def test_check_places_invalid_negative(): def test_check_places_exceeds_points(): from server import check_places - error = check_places("20", mock_clubs_list[0]) + error = check_places("11", mock_clubs_list[0]) assert error == "Places required exceed club's total points" From 84288a19f18ef22112cfc204837dfa03a790ebd0 Mon Sep 17 00:00:00 2001 From: Imenbr Date: Thu, 5 Sep 2024 12:45:37 +0200 Subject: [PATCH 5/6] (fix) handle booking places for a past competition --- competitions.json | 5 + server.py | 60 ++++-- ...est_booking-places-in-past-competitions.py | 48 +++++ ...est_booking-places-in-past-competitions.py | 189 ++++++++++++++++++ 4 files changed, 280 insertions(+), 22 deletions(-) create mode 100644 tests/integration_tests/test_booking-places-in-past-competitions.py create mode 100644 tests/unit_tests/test_booking-places-in-past-competitions.py diff --git a/competitions.json b/competitions.json index 039fc61bd..a40f93442 100644 --- a/competitions.json +++ b/competitions.json @@ -9,6 +9,11 @@ "name": "Fall Classic", "date": "2020-10-22 13:30:00", "numberOfPlaces": "13" + }, + { + "name": "New", + "date": "2024-10-22 13:30:00", + "numberOfPlaces": "13" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 188eec454..8b5b76e37 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,5 @@ import json +from datetime import datetime from flask import Flask, render_template, request, redirect, flash, url_for app = Flask(__name__) @@ -29,9 +30,7 @@ def index(): def get_club_from_email(email): try: - club = [ - club for club in clubs if club["email"] == email - ][0] + club = [club for club in clubs if club["email"] == email][0] return club except IndexError: return None @@ -41,33 +40,48 @@ def get_club_from_email(email): def showSummary(): club = get_club_from_email(request.form["email"]) if club: - return render_template("welcome.html", club=club, - competitions=competitions) + return render_template( + "welcome.html", club=club, competitions=competitions + ) else: flash("Sorry, that email wasn't found.") return redirect(url_for("index")) +def validate_competition_date(competition): + competition_date = datetime.strptime( + competition["date"], "%Y-%m-%d %H:%M:%S" + ) + if competition_date < datetime.now(): + return "This competition is already over. You cannot book a place." + + @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 - ) - else: + foundClub = get_club_from_name(club) + foundCompetition = get_competition_from_name(competition) + if not foundClub or not foundCompetition: flash("Something went wrong-please try again") return render_template( "welcome.html", club=club, competitions=competitions ) + error_message = validate_competition_date(foundCompetition) + if error_message: + flash(error_message) + return render_template( + "welcome.html", club=foundClub, competitions=competitions + ) + return render_template( + "booking.html", club=foundClub, competition=foundCompetition + ) def get_competition_from_name(name): try: competition = [ - competition for competition in - competitions if competition["name"] == name + competition + for competition in competitions + if competition["name"] == name ][0] return competition except IndexError: @@ -76,9 +90,7 @@ def get_competition_from_name(name): def get_club_from_name(name): try: - club = [ - club for club in clubs if club["name"] == name - ][0] + club = [club for club in clubs if club["name"] == name][0] return club except IndexError: return None @@ -88,16 +100,19 @@ def check_places(places, club): if not places or int(places) < 1: return "Places required must be a positive integer" if int(places) > 12: - return ("Places required must be a positive integer " - "that does not exceed 12") + return ( + "Places required must be a positive integer " + "that does not exceed 12" + ) if int(places) > int(club["points"]): return "Places required exceed club's total points" def take_places(places, club, competition): try: - competition["numberOfPlaces"] = \ + competition["numberOfPlaces"] = ( int(competition["numberOfPlaces"]) - places + ) club["points"] = int(club["points"]) - places return True except Exception: @@ -119,8 +134,9 @@ def purchasePlaces(): if take_places(placesRequired, club, competition): flash("Great-booking complete!") - return render_template("welcome.html", club=club, - competitions=competitions) + return render_template( + "welcome.html", club=club, competitions=competitions + ) else: flash("Something went wrong-please try again") return redirect( diff --git a/tests/integration_tests/test_booking-places-in-past-competitions.py b/tests/integration_tests/test_booking-places-in-past-competitions.py new file mode 100644 index 000000000..93ec674d4 --- /dev/null +++ b/tests/integration_tests/test_booking-places-in-past-competitions.py @@ -0,0 +1,48 @@ +def test_book_valid_competition(client): + """ + Test booking a valid competition with a valid club + (future competition date). + """ + response = client.get("/book/Competition%201/Club%201") + + # Ensure the correct template is rendered (booking.html) + assert response.status_code == 200 + assert ( + b"Competition 1" in response.data + ) # Assuming the booking page has the word "Booking" + + +def test_book_past_competition(client): + """ + Test booking a competition with a past date. + """ + response = client.get("/book/Competition%202/Club%201") + + # Ensure the user is shown a message that the competition is in the past + assert response.status_code == 200 + assert ( + b"This competition is already over. You cannot book a place." + in response.data + ) + + +def test_book_invalid_competition(client): + """ + Test trying to book with an invalid competition name. + """ + response = client.get("/book/Invalid%20Competition/Club%201") + + # Ensure the correct message is shown when competition is invalid + assert response.status_code == 200 + assert b"Something went wrong-please try again" in response.data + + +def test_book_invalid_club(client): + """ + Test trying to book with an invalid club name. + """ + response = client.get("/book/Competition%201/Invalid%20Club") + + # Ensure the correct message is shown when club is invalid + assert response.status_code == 200 + assert b"Something went wrong-please try again" in response.data diff --git a/tests/unit_tests/test_booking-places-in-past-competitions.py b/tests/unit_tests/test_booking-places-in-past-competitions.py new file mode 100644 index 000000000..c31115ae9 --- /dev/null +++ b/tests/unit_tests/test_booking-places-in-past-competitions.py @@ -0,0 +1,189 @@ +from unittest.mock import patch +from datetime import datetime + +from server import validate_competition_date +from .utils import mock_competitions_list, mock_clubs_list + +mock_competition_past = { + "name": "Competition 2", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "15", +} +mock_future_competition = { + "name": "Future Competition", + "date": "2024-03-27 10:00:00", +} + + +# Test for a valid future competition date +@patch("server.datetime") +def test_validate_competition_date_future(mock_datetime): + # Mock datetime.now() to return a date before the competition date + mock_datetime.now.return_value = datetime(2023, 3, 27) + mock_datetime.strptime.side_effect = datetime.strptime + + # Call the function with a future competition + result = validate_competition_date(mock_future_competition) + + # Assert that no error message is returned + assert result is None + + +# Test for a past competition date +@patch("server.datetime") +def test_validate_competition_date_past(mock_datetime): + # Mock datetime.now() to return a date after the competition date + mock_datetime.now.return_value = datetime(2023, 3, 27) + mock_datetime.strptime.side_effect = datetime.strptime + + # Call the function with a past competition + result = validate_competition_date(mock_competition_past) + + # Assert that the correct error message is returned + assert ( + result == "This competition is already over. You cannot book a place." + ) + + +# Test for valid club and valid competition (future date) +@patch("server.render_template") +@patch("server.validate_competition_date") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_book_valid_competition( + mock_get_competition, + mock_get_club, + mock_validate_date, + mock_render_template, + client, +): + # Mock valid club and competition + mock_get_competition.return_value = mock_competitions_list[0] + mock_get_club.return_value = mock_clubs_list[0] + mock_validate_date.return_value = None # No validation error + + # Simulate GET request to the route + response = client.get("/book/Competition%201/Club%201") + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_validate_date.assert_called_once_with(mock_competitions_list[0]) + mock_render_template.assert_called_once_with( + "booking.html", + club=mock_clubs_list[0], + competition=mock_competitions_list[0], + ) + + # Check the response status code + assert response.status_code == 200 + + +# Test for valid club and past competition (competition already over) +@patch("server.competitions", mock_competitions_list) +@patch("server.render_template") +@patch("server.flash") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +@patch("server.validate_competition_date") +def test_book_past_competition( + mock_validate_date, + mock_get_competition, + mock_get_club, + mock_flash, + mock_render_template, + client, +): + # Mock valid club but past competition + mock_get_competition.return_value = mock_competition_past + mock_get_club.return_value = mock_clubs_list[0] + mock_validate_date.return_value = ( + "This competition is already over. You cannot book a place." + ) + + # Simulate GET request to the route + response = client.get("/book/Competition%202/Club%201") + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 2") + mock_get_club.assert_called_once_with("Club 1") + mock_validate_date.assert_called_once_with(mock_competition_past) + mock_flash.assert_called_once_with( + "This competition is already over. You cannot book a place." + ) + mock_render_template.assert_called_once_with( + "welcome.html", + club=mock_clubs_list[0], + competitions=mock_competitions_list, + ) + + # Check the response status code + assert response.status_code == 200 + + +# Test for missing club or competition +@patch("server.competitions", mock_competitions_list) +@patch("server.render_template") +@patch("server.flash") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_book_missing_club_or_competition( + mock_get_competition, + mock_get_club, + mock_flash, + mock_render_template, + client, +): + # Mock a case where the competition is missing + mock_get_competition.return_value = None # Competition not found + mock_get_club.return_value = mock_clubs_list[0] # Club found + + # Simulate GET request to the route + response = client.get("/book/Competition%201/Club%201") + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Club 1") + mock_flash.assert_called_once_with("Something went wrong-please try again") + mock_render_template.assert_called_once_with( + "welcome.html", + club=mock_clubs_list[0]["name"], + competitions=mock_competitions_list, + ) + + # Check the response status code + assert response.status_code == 200 + + +# Test for invalid club +@patch("server.competitions", mock_competitions_list) +@patch("server.render_template") +@patch("server.flash") +@patch("server.get_club_from_name") +@patch("server.get_competition_from_name") +def test_book_invalid_club( + mock_get_competition, + mock_get_club, + mock_flash, + mock_render_template, + client, +): + # Mock a case where the club is invalid + mock_get_competition.return_value = mock_competitions_list[ + 0 + ] # Competition found + mock_get_club.return_value = None # Club not found + + # Simulate GET request to the route + response = client.get("/book/Competition%201/Invalid%20Club") + + # Assert that the necessary functions are called with the correct parameters + mock_get_competition.assert_called_once_with("Competition 1") + mock_get_club.assert_called_once_with("Invalid Club") + mock_flash.assert_called_once_with("Something went wrong-please try again") + mock_render_template.assert_called_once_with( + "welcome.html", club="Invalid Club", competitions=mock_competitions_list + ) + + # Check the response status code + assert response.status_code == 200 From 1a9b6bd23662689cc5a8e6cf31f81b7a2323204b Mon Sep 17 00:00:00 2001 From: Imenbr Date: Thu, 5 Sep 2024 20:08:43 +0200 Subject: [PATCH 6/6] (fix) handle point updates are not reflected --- .coverage | Bin 53248 -> 53248 bytes server.py | 25 +++++- tests/integration_tests/conftest.py | 75 +++++++++++++----- ...s_should_not_use_more_than_their_points.py | 32 ++++---- .../test_point_updates_are_not_reflected.py | 20 +++++ tests/unit_tests/conftest.py | 8 ++ ...s_should_not_use_more_than_their_points.py | 12 ++- .../test_point_updates_are_not_reflected.py | 43 ++++++++++ 8 files changed, 173 insertions(+), 42 deletions(-) create mode 100644 tests/integration_tests/test_point_updates_are_not_reflected.py create mode 100644 tests/unit_tests/test_point_updates_are_not_reflected.py diff --git a/.coverage b/.coverage index 5c65d975222eea263bfde8c47648bae7c1596e07..e8f487177bf2ca638102f9eb95e7a4491650697e 100644 GIT binary patch delta 216 zcmZozz}&Eac>{|B7c+k%1OH|Inf!^H1qFinS{VECnv<~ZayAws{oWf!NC6nsNe)pdKbSN z7c&bZrx^3M|Nr+h@-YF~a!lXqfBXl6dS)p`7EVr!ca`%j?d^*{znA|1+1UQ`y#GJn zpEG3QVqjok=6}n;|C|2{|68D0_xU%!oi7fu;3G)z8~;b3;A4J%W}tI{|B4-3CK1OH|Inf&qm>YD`x#Q9mxSeO}#C(rZOoZRov&B#1?w!aR0 zQht7RW?uT_DgFgO9U2V$SNIe7HTY)%bx80}PKq~YWwB%~X9Vejsogv!zCZz}_#gxS zEB;&jXZa5T6>sGC=VD=D

B1_W%EWMs8*xTY?$L{{LT^j|s?;WBOMA1H`Ro7GY%J zZwh7Xt$W3;#0){@?sx_}}tB0~&gbpPdb;m63&I^V|92 hAUofHq`&ch