diff --git a/.gitignore b/.gitignore index 2cba99d87..23535d79f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.idea/ +.coverage \ No newline at end of file diff --git a/clubs.json b/clubs.json index 1d7ad1ffe..dcffab73a 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,28 @@ -{"clubs":[ - { - "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" - } -]} \ No newline at end of file +{ + "clubs": [ + { + "name": "Simply Lift", + "email": "john@simplylift.co.uk", + "password": "pbkdf2:sha256:150000$hRdMG3j8$75089df7d666a0a804b2ab30832ee5ead0af0f9da1a16792aebb227f26736140", + "points": "13" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "pbkdf2:sha256:150000$vRos0Pry$191eef653bbe787936e8329844c04c082ea74f91ed2da129ed0258a9cf4f0b62", + "points": "4" + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": "pbkdf2:sha256:150000$NSg2p4vF$e817bb5a870bf4fea7e0687a9afe4b620a6ba2402d6190450d13407b00e39848", + "points": "5" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": "pbkdf2:sha256:150000$8LvMXlh1$84c53ce7ad7b0adb694806647bc1fd7f2c748b5e3a41d5ad76f515c148e86252", + "points": "12" + } + ] +} \ No newline at end of file diff --git a/competitions.json b/competitions.json index 039fc61bd..377789cb3 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,24 @@ { "competitions": [ - { - "name": "Spring Festival", - "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" - }, { "name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "number_of_places": "13" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "20" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..877cb69d8 100644 --- a/server.py +++ b/server.py @@ -1,58 +1,292 @@ import json -from flask import Flask,render_template,request,redirect,flash,url_for +from datetime import datetime +from flask import Flask, render_template, request, redirect, flash, url_for, session +from werkzeug.security import generate_password_hash, check_password_hash -def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs +def load_clubs(): + with open('clubs.json') as c: + list_of_clubs = json.load(c)['clubs'] + return list_of_clubs -def loadCompetitions(): +def load_competitions(): with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions - + list_of_competitions = json.load(comps)['competitions'] + return list_of_competitions app = Flask(__name__) app.secret_key = 'something_special' -competitions = loadCompetitions() -clubs = loadClubs() +competitions = load_competitions() +clubs = load_clubs() + +CLUB_POINTS = 15 + +def update_club_booked_places(club, places, competition_name): + clubs.remove(club) + + club.setdefault("booked_places", {}) + current = int(club["booked_places"].get(competition_name, 0)) + club["booked_places"][competition_name] = str(current + places) + + club["points"] = str(int(club["points"]) - places) + + clubs.append(club) + save_clubs() + +def save_clubs(): + with open('clubs.json', 'w') as c: + list_of_clubs = {"clubs": clubs} + json.dump(list_of_clubs, c, indent=4) + +def update_competition_available_places(competition, places): + competitions.remove(competition) + + competition['number_of_places'] = str(int(competition['number_of_places']) - places) + + competitions.append(competition) + save_competitions() + +def save_competitions(): + with open('competitions.json', 'w') as comps: + list_of_competitions = {"competitions": competitions} + json.dump(list_of_competitions, comps, indent=4) + +def add_club(name, email, password, points): + clubs.append({"name": name, "email": email, "password": password, "points": points}) + save_clubs() + +def update_club_password(club, password): + hashed_password = generate_password_hash(password) + club["password"] = hashed_password + save_clubs() + return club @app.route('/') def index(): return render_template('index.html') -@app.route('/showSummary',methods=['POST']) -def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) +@app.route('/signUp') +def sign_up(): + return render_template('sign_up.html') +@app.route('/profile/', methods=['GET']) +def profile(club): + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) + + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Club not found"), 404 + + return render_template(template_name_or_list='profile.html', club=the_club) + + flash("Sorry, you are not allow to see that profile.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + +@app.route('/profile', methods=['POST']) +def profile_post(): + club_name = request.form['name'] + club_email = request.form['email'] + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + club_exists = next((c for c in clubs if c['email'] == club_email or c['name'] == club_name), None) + if club_exists is None: + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('sign_up')) + + hashed_password = generate_password_hash(club_password) + add_club(club_name, club_email, hashed_password, str(CLUB_POINTS)) + + the_club = next((c for c in clubs if c['email'] == club_email), None) + + if the_club is None: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='sign_up.html') + + flash("Great! You have successfully signed up.") + return render_template(template_name_or_list='profile.html', club=the_club) -@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: - flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) + flash("Sorry, the club already exists.") + return render_template(template_name_or_list='sign_up.html') + +@app.route('/changePassword/', methods=['GET', 'POST']) +def change_password(club): + if "club" in session and session['club'] == club: + if request.method == 'GET': + the_club = next((c for c in clubs if c['name'] == club), None) + + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + return render_template(template_name_or_list='change_password.html', club=the_club) + else: + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('change_password')) + + the_club = next((c for c in clubs if c['name'] == club), None) + + if check_password_hash(the_club['password'], club_password): + flash('Sorry, you have to type a new different password.') + return render_template(template_name_or_list='change_password.html', club=the_club) + + the_club = update_club_password(the_club, club_password) + if the_club: + flash("Great! You have successfully changed your password.") + return render_template(template_name_or_list='profile.html', club=the_club) + + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='index.html') + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/showSummary/', methods=['GET']) +def show_summary(club): + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + +@app.route('/showSummary', methods=['POST']) +def show_summary_post(): + the_club = next((c for c in clubs if c['email'] == request.form['email']), None) + + if the_club is None: + flash("Sorry, that email was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + if not check_password_hash(the_club['password'], request.form['password']): + flash("Sorry, the password is incorrect.") + return render_template(template_name_or_list="index.html",) + + session["club"] = the_club["name"] + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + +@app.route('/book//') +def book(competition, club): + if "club" in session and session['club'] == club: + found_club = [c for c in clubs if c['name'] == club][0] + found_competition = [c for c in competitions if c['name'] == competition][0] + + now = datetime.now() + + competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') + + error_message = "" + error_tag = "" + + the_competition = next((a_competition for a_competition in competitions + if a_competition['name'] == competition), None) + competition_places = int(the_competition['number_of_places']) + + if now > competition_date: + error_message = "Sorry, this competition is outdated. Booking not possible." + error_tag = "Outdated" + + elif competition_places == 0: + error_message = "Sorry, this competition is sold out. Booking not possible." + error_tag = "Sold out" + + if error_message and error_tag: + flash(error_message) + + the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions, + error=error_tag), 403 + + if found_club and found_competition: + return render_template(template_name_or_list='booking.html', + club=found_club, + competition=found_competition) + else: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 @app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): +def purchase_places(): 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) + places_required = int(request.form['places']) + + cumulative_places = places_required + int(club["booked_places"][competition["name"]]) \ + if "booked_places" in club else places_required + + error_message = "" + error_tag = "" + + if places_required < 0: + error_message = "Sorry, you should type a positive number." + error_tag = "Negative number" + + elif cumulative_places > 12: + error_message = "Sorry, you are not allow to purchase more than 12 places for this competition." + error_tag = "Over 12 places" + + elif places_required > int(competition['number_of_places']): + error_message = "Sorry, there are not enough places available for this competition." + error_tag = "Not enough places" + + elif places_required > int(club['points']): + error_message = "Sorry, you do not have enough points to purchase." + error_tag = "Not enough points" + + if error_message and error_tag: + flash(error_message) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error=error_tag), 403 + + update_club_booked_places(club=club, + places=places_required, + competition_name=competition["name"]) + update_competition_available_places(competition=competition, places=places_required) -# TODO: Add route for points display + flash(f"Great! Booking of {places_required} places for " + f"{competition['name']} competition complete!") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) +@app.route('/pointsBoard') +def points_board(): + clubs_for_board=[] + for club in clubs: + if clubs.index(club) %2 == 0: + club["color"] = "#cccccc" + else: + club["color"] = "#aaaaaa" + clubs_for_board.append(club) + print(clubs_for_board) + return render_template(template_name_or_list='points_board.html', clubs=clubs_for_board) @app.route('/logout') def logout(): diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 000000000..cb24cfe32 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,37 @@ +.board{ + margin-left: 50px; + display: block; + border:1px black solid; + border-radius: 10px; + width: fit-content; +} + +.board table{ + width:400px; +} + +.board table tr{ + width: 100%; +} + +.board table thead div{ + background-color: #000000; + color: white; + font-weight: bold; +} + +.board table td{ + color: black; +} + +.board table td p{ + text-align: center; +} + +.board .cell{ + border-radius: 10px; + align-items: center; + display: flex; + justify-content: center; + padding: 5px 10px; +} \ No newline at end of file diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..a9b12c600 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -6,7 +6,7 @@

{{competition['name']}}

- Places available: {{competition['numberOfPlaces']}} + Places available: {{competition['number_of_places']}}
diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 000000000..7fbd77c33 --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,35 @@ + + + + + GUDLFT Registration + + +

Welcome to the GUDLFT - Change password page !

+ + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your new password:

+ + Name : {{club['name']}}
+ Email : {{club['email']}} +

+ + + + + + + + +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 926526b7d..b00fd6206 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,11 +6,31 @@

Welcome to the GUDLFT Registration Portal!

- Please enter your secretary email to continue: + + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your secretary email and your password to continue or sign up +
+
+ + + +
+
+
+ To see the clubs points board : click here \ No newline at end of file diff --git a/templates/points_board.html b/templates/points_board.html new file mode 100644 index 000000000..e3f4df107 --- /dev/null +++ b/templates/points_board.html @@ -0,0 +1,31 @@ + + + + + GUDLFT Registration + + + +

Welcome to the GUDLFT clubs points board!

+
+

⯈ Here is the board for all the clubs and their points.

+
+ + + + + + {% for club in clubs %} + + + + + {% endfor %} +
Club
Points
{{ club.name }}
{{ club.points }}
+
+
+
+ To continue into the application, please log in or sign up +
+ + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 000000000..9b05a3332 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,32 @@ + + + + + Summary | GUDLFT Profile + + +

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

Logout + + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} +

Profile:

+
    +
  • Name : {{club['name']}}
  • +
  • Email : {{club['email']}}
  • +
  • password : {{club['password']}} - Change password
  • +
    +
  • Points available: {{club['points']}}
  • +
+
+ Go to home page + + {%endwith%} + + + \ No newline at end of file diff --git a/templates/sign_up.html b/templates/sign_up.html new file mode 100644 index 000000000..49c6be945 --- /dev/null +++ b/templates/sign_up.html @@ -0,0 +1,37 @@ + + + + + GUDLFT Sign up + + +

Welcome to the GUDLFT sign up page!

+ + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your secretary email and your password: +
+ + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html index ff6b261a2..355e23b3d 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -5,7 +5,7 @@ Summary | GUDLFT Registration -

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

Logout +

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

Logout

{% with messages = get_flashed_messages()%} {% if messages %} @@ -15,6 +15,9 @@

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

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

Competitions:

    @@ -22,8 +25,8 @@

    Competitions:

  • {{comp['name']}}
    Date: {{comp['date']}}
    - Number of Places: {{comp['numberOfPlaces']}} - {%if comp['numberOfPlaces']|int >0%} + Number of Places: {{comp['number_of_places']}} + {%if comp['number_of_places']|int >0%} Book Places {%endif%}
  • 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..4792d2456 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,174 @@ +import pytest + +from random import randint +from werkzeug.security import generate_password_hash +from server import app + +@pytest.fixture +def client(): + my_app = app + with my_app.test_client() as client: + yield client + +@pytest.fixture +def get_clubs(): + the_clubs = [ + { + "name":"Simply Lift", + "email":"john@simplylift.co", + "password": generate_password_hash("tp1_Tmn28"), + "points":"13" + }, + { + "name":"Iron Temple", + "email": "admin@irontemple.com", + "password": generate_password_hash("tp2_Tmn29"), + "points":"4" + }, + { "name":"She Lifts", + "email": "kate@shelifts.co.uk", + "password": generate_password_hash("tp3_Tmn30"), + "points":"12", + "booked_places": { + "Spring Festival": "7" + } + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": generate_password_hash("tp4_Tmn40"), + "points": "5" + } + ] + return the_clubs + +@pytest.fixture +def get_competitions(): + the_competitions = [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "number_of_places": "13" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "25" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + } + ] + return the_competitions + +@pytest.fixture +def get_credentials(): + data = {"email": "kate@shelifts.co.uk", "password": "tp3_Tmn30"} + return data + +@pytest.fixture +def get_credentials_2(): + data = {"email": "admin@irontemple.com", "password": "tp2_Tmn29"} + return data + +@pytest.fixture +def get_credentials_3(): + data = {"email": "admin@powerlift.com", "password": "tp4_Tmn40"} + return data + +@pytest.fixture +def get_unexisting_credentials(): + data = {"email": "nicolas.marie@unexisting.com", "password": "er45_shet"} + return data + +@pytest.fixture +def get_existing_competition_and_club(): + data = {"competition": "Spring Festival", "club": "She Lifts"} + return data + +@pytest.fixture +def get_existing_competition_and_club_2(): + data = {"competition": "Spring Festival", "club": "Iron Temple"} + return data + +@pytest.fixture +def get_existing_competition_and_club_3(): + data = {"competition": "Fall Classic", "club": "Iron Temple"} + return data + +@pytest.fixture +def get_existing_competition_and_club_4(): + data = {"competition": "Summer Stronger", "club": "Power Lift"} + return data + +@pytest.fixture +def get_consistent_purchasing_data(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = randint(1, 5) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def get_new_club(): + club = { + "name": "New Club", + "email": "new@newclub.com", + "password": generate_password_hash("tp6_Tmn60"), + "points": "12" + } + return club + +@pytest.fixture +def get_inconsistent_purchasing_data(): + competition = "Spring Festival" + club_name = "Iron Temple" + club_points = 4 + places_to_book = randint(club_points+1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_over_12_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 13 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_13_cumulative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 6 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_with_negative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = -2 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_places_more_than_available(): + competition = "Winter Power" + club_name = "Power Lift" + places_to_book = 5 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data 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/test_app.py b/tests/integration_tests/test_app.py new file mode 100644 index 000000000..f9cd1ead1 --- /dev/null +++ b/tests/integration_tests/test_app.py @@ -0,0 +1,243 @@ +import pytest +import server + +from bs4 import BeautifulSoup +from flask import url_for + + +class TestIntegrationApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + @staticmethod + def test_summary_logout_redirect_returns_welcome(client, get_credentials): + client.post('/showSummary', data=get_credentials) + + logout_response = client.get('/logout') + soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") + url = soup.find_all('a')[0].get('href') + redirect_response = client.get(url, follow_redirects=True) + + assert redirect_response.status_code == 200 + data = redirect_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_booking_return_festival_page_booking(client, + get_credentials, + get_existing_competition_and_club): + + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + data = client_response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data + + @staticmethod + def test_good_purchasing_places_returns_summary_page(client, + get_credentials, + get_consistent_purchasing_data): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = get_consistent_purchasing_data + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + the_competition =[competition for competition in server.competitions + if competition["name"] == purchasing_data['competition']][0] + + client.get(url_for(endpoint='book', + competition=the_competition['name'], + club=the_club['name'])) + + club_points = the_club['points'] + competition_places = the_competition['number_of_places'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + new_points = int(club_points) - int(purchasing_data['places']) + new_competition_places = int(competition_places) - int(purchasing_data['places']) + + soup = BeautifulSoup(data, features="html.parser") + all_li_str = [str(li) for li in soup.find_all('li')] + the_club_name_utf8 = "%20".join(the_club['name'].split()) + the_competition_name_utf8 = "%20".join(the_competition['name'].split()) + li = (f'
  • \n' + f' {the_competition["name"]}
    \n' + f' Date: 2026-07-27 10:00:00\n' + f' Number of Places: {new_competition_places}\n \n' + f' Book Places\n' + f'
  • ') + + assert client_response.status_code == 200 + assert (f"Great! Booking of {purchasing_data['places']} places for " + f"{purchasing_data['competition']} competition complete!") in data + assert f"Welcome, {the_club["email"]} " in data + assert li in all_li_str + assert f"Points available: {new_points}" in data + + @staticmethod + def test_purchasing_places_not_enough_points_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data): + + client.post('/showSummary', data=get_credentials_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + the_club = [club for club in server.clubs + if club["name"] == get_existing_competition_and_club_2['club']][0] + + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {the_club['points']}" in data + + @staticmethod + def test_purchasing_places_over_12_places_returns_sorry(client, + get_credentials, + purchasing_over_12_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_over_12_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, + get_credentials, + purchasing_13_cumulative_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_13_cumulative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_negative_number_returns_sorry(client, + get_credentials, + purchasing_with_negative_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_with_negative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_past_competitions_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_3): + + client.post('/showSummary', data=get_credentials_2) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert "Sorry, this competition is outdated. Booking not possible." in data + + @staticmethod + def test_purchasing_places_over_available_returns_sorry(client, + get_credentials, + purchasing_places_more_than_available): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_places_more_than_available + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, there are not enough places available for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_sold_out_status_code_error(client, + get_credentials_3, + get_existing_competition_and_club_4): + + client.post('/showSummary', data=get_credentials_3) + print(get_credentials_3) + print(get_existing_competition_and_club_4) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert "Sorry, this competition is sold out. Booking not possible." in 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/test_app.py b/tests/unit_tests/test_app.py new file mode 100644 index 000000000..4de37c381 --- /dev/null +++ b/tests/unit_tests/test_app.py @@ -0,0 +1,182 @@ +import pytest +import server + +from werkzeug.security import check_password_hash +from flask import url_for, session + + +class TestUnitApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + mocker.patch('server.save_clubs') + + @staticmethod + def test_index_status_code_ok(client): + client_response = client.get('/') + assert client_response.status_code == 200 + + @staticmethod + def test_index_return_welcome(client): + client_response = client.get('/') + data = client_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_index_mail_authentication_ok(get_credentials, client): + client_response = client.post('/showSummary', data=get_credentials) + assert client_response.status_code == 200 + + @staticmethod + def test_index_mail_authentication_returns_summary(client, get_credentials): + client_response = client.post('/showSummary', data=get_credentials) + data = client_response.data.decode('utf-8') + + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 12" in data + + @staticmethod + def test_index_mail_authentication_fail(client, get_unexisting_credentials): + client_response = client.post('/showSummary', data=get_unexisting_credentials) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data + + @staticmethod + def test_summary_logout_redirect_status_code_ok(client, get_credentials): + client.post('/showSummary', data=get_credentials) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + + @staticmethod + def test_booking_status_code_ok(client, + get_credentials, + get_existing_competition_and_club): + + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + assert client_response.status_code == 200 + + @staticmethod + def test_signup_status_code_ok(client): + client_response = client.get('/signUp') + assert client_response.status_code == 200 + + @staticmethod + def test_signup_returns_welcome(client): + client_response = client.get('/signUp') + data = client_response.data.decode('utf-8') + assert "Welcome to the GUDLFT sign up page!" in data + assert ("Club name:" in data) + assert ("Email:" in data) + assert ("Password:" in data) + assert ("Confirm Password:" in data) + + @staticmethod + def test_change_password_status_code_ok(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/changePassword/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_update_password_ok(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert check_password_hash(the_club['password'], "tp5_Tmn50") + + @staticmethod + def test_update_password_fails(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert not check_password_hash(the_club['password'], "tp6_Tmn60") + + @staticmethod + def test_add_club_ok(get_new_club): + server.add_club(name=get_new_club["name"], + email=get_new_club["email"], + password=get_new_club["password"], + points=get_new_club["points"]) + + assert get_new_club in server.clubs + + @staticmethod + def test_profile_ok(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_profile_without_authentication_fails(client): + client_response = client.get('/profile/Simply Lift') + assert client_response.status_code == 403 + + @staticmethod + def test_profile_returns_welcome(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + data = client_response.data.decode('utf-8') + assert "Welcome, kate@shelifts.co.uk" in data + assert "Profile:" in data + assert "Name : She Lifts" in data + assert "Email : kate@shelifts.co.uk" in data + assert "Points available: 12" in data + + @staticmethod + def test_update_booked_places_ok(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert club['booked_places'][competition_name] == str(5) + + @staticmethod + def test_update_booked_places_fails(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert not club['booked_places'][competition_name] == str(6) + + @staticmethod + def test_update_competition_available_places(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert competition['number_of_places'] == str(places_available - 5) + + @staticmethod + def test_update_competition_available_places_fails(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert not competition['number_of_places'] == str(places_available - 4)