Added a service that can get all current tournaments list

This commit is contained in:
Stanislas Jouffroy 2024-03-23 11:56:31 +01:00
parent e6023e0687
commit 3d0bd47079
26 changed files with 4305 additions and 204 deletions

View file

@ -3,7 +3,7 @@ import logging
import config import config
from gestion_sports_services import GestionSportsServices from gestion_sports_services import GestionSportsServices
from models import Action, BookingFilter, Club, Court, User from models import Action, BookingFilter, Club, Court, Tournament, User
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -51,7 +51,18 @@ async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
await service.cancel_booking_id(user, club, booking_id) await service.cancel_booking_id(user, club, booking_id)
def main() -> tuple[Court, User] | None: async def get_tournaments(club: Club, user: User) -> list[Tournament]:
"""
Cancel a booking that matches the booking id
:param club: the club in which the booking was made
:param user: the user who made the booking
"""
service = GestionSportsServices()
return await service.get_all_tournaments(user, club)
def main() -> tuple[Court, User] | list[Tournament] | None:
""" """
Main function used to book a court Main function used to book a court
@ -80,3 +91,8 @@ def main() -> tuple[Court, User] | None:
club = config.get_club() club = config.get_club()
booking_filter = config.get_booking_filter() booking_filter = config.get_booking_filter()
asyncio.run(cancel_booking(club, user, booking_filter)) asyncio.run(cancel_booking(club, user, booking_filter))
elif action == Action.TOURNAMENTS:
user = config.get_user()
club = config.get_club()
return asyncio.run(get_tournaments(club, user))

View file

@ -1,42 +0,0 @@
import logging
from aiohttp import ClientSession
from connectors import Connector
from models import BookingFilter, Club, User
LOGGER = logging.getLogger(__name__)
class BookingService:
def __init__(self, club: Club, connector: Connector):
LOGGER.info("Initializing booking service at for club", club.name)
self.club = club
self.connector = connector
self.session: ClientSession | None = None
async def __aenter__(self):
self.session = ClientSession()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.session.close()
async def book(self, user: User, booking_filter: BookingFilter) -> int | None:
"""
Book a court matching the booking filters for a user.
The steps to perform a booking are to go to the landing page, to log in, wait
and for the time when booking is open and then actually book the court
:param user: the user that wants to book a court
:param booking_filter: the booking criteria
:return: the court number if the booking is successful, None otherwise
"""
if self.connector is None:
LOGGER.error("No connection to Gestion Sports is available")
return None
if user is None or booking_filter is None:
LOGGER.error("Not enough information available to book a court")
return None
self.connector.book(user, booking_filter)

View file

@ -143,4 +143,4 @@ def get_action() -> Action:
Get the action to perform from an environment variable Get the action to perform from an environment variable
:return: the action to perform :return: the action to perform
""" """
return Action(os.environ.get("ACTION")) return Action(os.environ.get("ACTION").lower())

View file

@ -49,6 +49,11 @@ class GestionSportsConnector:
self.club.booking_platform.urls.get(name).path, self.club.booking_platform.urls.get(name).path,
) )
def _get_url_parameter(self, name: str) -> str:
self._check_url_path_exists(name)
return self.club.booking_platform.urls.get(name).parameter
def _get_payload_template(self, name: str) -> Path: def _get_payload_template(self, name: str) -> Path:
""" """
Get the path to the template file for the service with the given name Get the path to the template file for the service with the given name
@ -178,6 +183,18 @@ class GestionSportsConnector:
""" """
return self._get_payload_template("cancellation") return self._get_payload_template("cancellation")
@property
def tournaments_sessions_url(self) -> str:
return self._get_url_path("tournament-sessions")
@property
def tournaments_sessions_template(self) -> Path:
return self._get_payload_template("tournament-sessions")
@property
def tournaments_list_url(self) -> str:
return self._get_url_path("tournaments-list")
@property @property
def available_sports(self) -> dict[str, Sport]: def available_sports(self) -> dict[str, Sport]:
""" """
@ -209,7 +226,7 @@ class GestionSportsConnector:
payload = PayloadBuilder.build(self.login_template, user=user, club=self.club) payload = PayloadBuilder.build(self.login_template, user=user, club=self.club)
async with session.post( async with session.post(
self.login_url, data=payload, headers=POST_HEADERS self.login_url, data=payload, headers=POST_HEADERS, allow_redirects=False
) as response: ) as response:
resp_text = await response.text() resp_text = await response.text()
LOGGER.debug("Connexion request response:\n%s", resp_text) LOGGER.debug("Connexion request response:\n%s", resp_text)
@ -421,3 +438,23 @@ class GestionSportsConnector:
for booking in bookings: for booking in bookings:
if booking.matches(booking_filter): if booking.matches(booking_filter):
return await self.cancel_booking_id(session, booking.id) return await self.cancel_booking_id(session, booking.id)
async def send_tournaments_sessions_request(
self, session: ClientSession
) -> ClientResponse:
payload = self.tournaments_sessions_template.read_text()
async with session.post(
self.tournaments_sessions_url, data=payload, headers=POST_HEADERS
) as response:
LOGGER.debug("tournament sessions: \n%s", await response.text())
return response
async def send_tournaments_request(
self, session: ClientSession, tournement_session_id: str
) -> ClientResponse:
final_url = self.tournaments_list_url + tournement_session_id
LOGGER.debug("Getting tournaments list at %s", final_url)
async with session.get(final_url) as response:
LOGGER.debug("tournaments: %s\n", await response.text())
return response

View file

@ -1,10 +1,12 @@
import json
import logging import logging
import time import time
import pendulum import pendulum
from aiohttp import ClientSession from aiohttp import ClientSession
from connectors import GestionSportsConnector from bs4 import BeautifulSoup
from models import BookingFilter, BookingOpening, Club, Court, User from gestion_sport_connector import GestionSportsConnector
from models import BookingFilter, BookingOpening, Club, Court, Tournament, User
from pendulum import DateTime from pendulum import DateTime
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -125,3 +127,62 @@ class GestionSportsServices:
booking_minute = opening_time.minute booking_minute = opening_time.minute
return booking_date.at(booking_hour, booking_minute) return booking_date.at(booking_hour, booking_minute)
@staticmethod
async def get_all_tournaments(user: User, club: Club) -> list[Tournament]:
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
session_html = await connector.send_tournaments_sessions_request(session)
tournaments_id = GestionSportsServices.retrieve_tournament_session(
await session_html.text()
)
tournaments = await connector.send_tournaments_request(
session, tournaments_id
)
return GestionSportsServices.retrieve_tournaments(await tournaments.text())
@staticmethod
def retrieve_tournament_session(sessions: str) -> str:
session_object = json.loads(sessions).get("Inscription tournois:school-outline")
return list(session_object.keys())[0]
@staticmethod
def retrieve_tournaments(html: str) -> list[Tournament]:
soup = BeautifulSoup(html, "html.parser")
tournaments = []
cards = soup.find_all("div", {"class": "card-body"})
for card in cards:
title = card.find("h5")
price = title.find("span").get_text().strip()
name = title.get_text().strip().removesuffix(price).strip()
elements = card.find("div", {"class": "row"}).find_all("li")
date = elements[0].get_text().strip()
start_time, end_time = (
elements[2].get_text().strip().replace("h", ":").split(" - ")
)
start_datetime = pendulum.from_format(
f"{date} {start_time}", "DD/MM/YYYY HH:mm"
)
end_datetime = pendulum.from_format(
f"{date} {end_time}", "DD/MM/YYYY HH:mm"
)
gender = elements[1].get_text().strip()
places_left = (
card.find("span", {"class": "nb_place_libre"}).get_text().strip()
)
tournament = Tournament(
name=name,
price=price,
start_date=start_datetime,
end_date=end_datetime,
gender=gender,
places_left=places_left,
)
tournaments.append(tournament)
return tournaments

View file

@ -70,6 +70,7 @@ class Sport(BaseModel):
class Url(BaseModel): class Url(BaseModel):
name: str name: str
path: str path: str
parameter: Optional[str] = Field(default=None)
payload_template: Optional[str] = Field(default=None, alias="payloadTemplate") payload_template: Optional[str] = Field(default=None, alias="payloadTemplate")
@ -210,3 +211,13 @@ class Booking(BaseModel):
class Action(Enum): class Action(Enum):
BOOK = "book" BOOK = "book"
CANCEL = "cancel" CANCEL = "cancel"
TOURNAMENTS = "tournaments"
class Tournament(BaseModel):
name: str
price: str
start_date: DateTime
end_date: DateTime
gender: str
places_left: str | int

View file

@ -1,12 +1,16 @@
{ {
"Connection": "keep-alive", "Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.5", "Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"DNT": "1", "Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Accept": "application/json, text/javascript, */*; q=0.01", "Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest", "DNT": "1",
"Origin": "https://toulousepadelclub.gestion-sports.com",
"Pragma": "no-cache",
"Sec-Fetch-Dest": "empty", "Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin" "Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
"X-Requested-With": "XMLHttpRequest"
} }

View file

@ -0,0 +1 @@
ajax=loadSessionForSpecDay&date=all

View file

@ -17,3 +17,8 @@ platforms:
- name: cancellation - name: cancellation
path: /membre/mesresas.html path: /membre/mesresas.html
payloadTemplate: gestion-sports/booking-cancellation-payload.txt payloadTemplate: gestion-sports/booking-cancellation-payload.txt
- name: tournament-sessions
path: /membre/index.php
payloadTemplate: gestion-sports/tournament-sessions-payload.txt
- name: tournaments-list
path: /membre/events/event.html?event=

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
{
"Inscription tournois:school-outline": {
"1174": {
"id": 1174,
"sport": "padel",
"clubName": "toulouse padel club",
"nom": "Tournoi",
"dateNextSession": "25\/03\/2024",
"dateDebut": "01\/08\/2022",
"dateFin": "01\/10\/2024",
"logo": "TCP_Ligue_Arcanthe2-01-min.png",
"nbSession": 14,
"icon": "school-outline",
"playerCanSeeThisEvent": null,
"type": "tournoi",
"isJp": false,
"isCiup": false,
"sqlDate": "2024-03-25 13:30:00"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ from pathlib import Path
import config import config
import pendulum import pendulum
import pytest import pytest
from connectors import GestionSportsConnector from gestion_sport_connector import GestionSportsConnector
from models import BookingFilter, Club, User from models import BookingFilter, Club, User
TEST_FOLDER = Path(__file__).parent.parent TEST_FOLDER = Path(__file__).parent.parent
@ -36,11 +36,17 @@ def booking_filter() -> BookingFilter:
@pytest.fixture @pytest.fixture
def booking_success_response() -> dict: def booking_success_response() -> dict:
booking_success_file = RESPONSES_FOLDER / "booking_success.json" booking_success_file = RESPONSES_FOLDER / "booking-success.json"
return json.loads(booking_success_file.read_text(encoding="utf-8")) return json.loads(booking_success_file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def booking_failure_response() -> dict: def booking_failure_response() -> dict:
booking_failure_file = RESPONSES_FOLDER / "booking_failure.json" file = RESPONSES_FOLDER / "booking-failure.json"
return json.loads(booking_failure_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture
def tournament_sessions_json() -> str:
file = RESPONSES_FOLDER / "tournament-sessions.json"
return file.read_text(encoding="utf-8")

View file

@ -1,36 +1,15 @@
import json
import os import os
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import aiohttp
import pendulum import pendulum
import pytest import pytest
from connectors import GestionSportsConnector from aiohttp import ClientSession
from models import BookingFilter, Club from gestion_sport_connector import GestionSportsConnector
from pendulum import DateTime
from yarl import URL from yarl import URL
def retrieve_booking_datetime(
a_booking_filter: BookingFilter, a_club: Club
) -> DateTime:
"""
Utility to retrieve the booking datetime from the booking filter and the club
:param a_booking_filter: the booking filter that contains the date to book
:param a_club: the club which has the number of days before the date and the booking time
"""
booking_opening = a_club.booking_platform.booking_opening
opening_time = pendulum.parse(booking_opening.opening_time)
booking_hour = opening_time.hour
booking_minute = opening_time.minute
date_to_book = a_booking_filter.date
return date_to_book.subtract(days=booking_opening.days_before).at(
booking_hour, booking_minute
)
@patch.dict( @patch.dict(
os.environ, os.environ,
{"CLUB_ID": "tpc"}, {"CLUB_ID": "tpc"},
@ -57,6 +36,10 @@ def test_urls(connector):
connector.booking_cancellation_url connector.booking_cancellation_url
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html" == "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
) )
assert (
connector.tournaments_sessions_url
== "https://toulousepadelclub.gestion-sports.com/membre/index.php"
)
@patch.dict( @patch.dict(
@ -76,11 +59,15 @@ def test_urls_payload_templates(connector):
connector.booking_cancel_template connector.booking_cancel_template
== resources_folder / "booking-cancellation-payload.txt" == resources_folder / "booking-cancellation-payload.txt"
) )
assert (
connector.tournaments_sessions_template
== resources_folder / "tournament-sessions-payload.txt"
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_landing_page(connector): async def test_landing_page(connector):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
response = await connector.land(session) response = await connector.land(session)
assert response.status == 200 assert response.status == 200
@ -93,7 +80,7 @@ async def test_landing_page(connector):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login(connector, user): async def test_login(connector, user):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
response = await connector.login(session, user) response = await connector.login(session, user)
@ -128,7 +115,7 @@ def test_get_booked_court(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_book_one_court(connector, user, booking_filter): async def test_book_one_court(connector, user, booking_filter):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -156,28 +143,9 @@ def test_build_booking_datetime(connector, booking_filter):
assert opening_datetime.minute == 0 assert opening_datetime.minute == 0
@patch("pendulum.now")
def test_wait_until_booking_time(mock_now, connector, booking_filter, club):
booking_datetime = retrieve_booking_datetime(booking_filter, club)
seconds = [
booking_datetime.subtract(seconds=3),
booking_datetime.subtract(seconds=2),
booking_datetime.subtract(seconds=1),
booking_datetime,
booking_datetime.add(microseconds=1),
booking_datetime.add(microseconds=2),
]
mock_now.side_effect = seconds
connector.wait_until_booking_time(booking_filter)
assert pendulum.now() == booking_datetime.add(microseconds=1)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_hash(connector, user): async def test_get_hash(connector, user):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -197,7 +165,7 @@ def test_get_hash_input():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_bookings(connector, user): async def test_get_bookings(connector, user):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -210,7 +178,7 @@ async def test_get_bookings(connector, user):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_ongoing_bookings(connector, user): async def test_get_ongoing_bookings(connector, user):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -218,20 +186,15 @@ async def test_get_ongoing_bookings(connector, user):
print(bookings) print(bookings)
@pytest.mark.asyncio
async def test_has_user_ongoing_bookings(connector, user):
assert await connector.has_user_ongoing_booking(user)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking_id(connector, user): async def test_cancel_booking_id(connector, user):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
ongoing_bookings = await connector.get_ongoing_bookings(session) ongoing_bookings = await connector.get_ongoing_bookings(session)
booking_id = ongoing_bookings[0].id booking_id = ongoing_bookings[0].id
response = await connector.cancel_booking_id(user, booking_id) response = await connector.cancel_booking_id(session, 666)
assert len(await connector.get_ongoing_bookings(session)) == 0 assert len(await connector.get_ongoing_bookings(session)) == 0
@ -245,7 +208,33 @@ def test_find_court(connector):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking(connector, user, booking_filter): async def test_cancel_booking(connector, user, booking_filter):
async with aiohttp.ClientSession() as session: async with ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
await connector.cancel_booking(session, booking_filter) await connector.cancel_booking(session, booking_filter)
@pytest.mark.asyncio
async def test_tournament_sessions(connector, user):
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
response = await connector.send_tournaments_sessions_request(session)
assert response.status == 200
all_sessions = json.loads(await response.text())
sessions = all_sessions.get("Inscription tournois:school-outline")
assert len(sessions) == 1
@pytest.mark.asyncio
async def test_send_tournaments_request(connector, user):
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
tournament_session_id = "1174"
response = await connector.send_tournaments_request(
session, tournament_session_id
)
assert "<span class='nb_place_libre'>Complet</span>" in await response.text()

View file

@ -18,3 +18,9 @@ async def test_user_has_available_slots(club, user):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking(club, user, booking_filter): async def test_cancel_booking(club, user, booking_filter):
await GestionSportsServices.cancel_booking(user, club, booking_filter) await GestionSportsServices.cancel_booking(user, club, booking_filter)
@pytest.mark.asyncio
async def test_get_all_tournaments(user, club):
tournaments = await GestionSportsServices.get_all_tournaments(user, club)
assert len(tournaments) == 14

View file

@ -3,7 +3,7 @@ from pathlib import Path
import pendulum import pendulum
import pytest import pytest
from connectors import GestionSportsConnector from gestion_sport_connector import GestionSportsConnector
from gestion_sports_services import GestionSportsServices from gestion_sports_services import GestionSportsServices
from models import ( from models import (
BookingFilter, BookingFilter,
@ -43,7 +43,7 @@ def court14() -> Court:
@pytest.fixture @pytest.fixture
def sport1(court11, court12, court13, court14) -> Sport: def sport1(court11: Court, court12: Court, court13: Court, court14: Court) -> Sport:
return Sport( return Sport(
name="Sport1", name="Sport1",
id=8, id=8,
@ -75,7 +75,7 @@ def court24() -> Court:
@pytest.fixture @pytest.fixture
def sport2(court21, court22, court23, court24) -> Sport: def sport2(court21: Court, court22: Court, court23: Court, court24: Court) -> Sport:
return Sport( return Sport(
name="Sport 2", name="Sport 2",
id=10, id=10,
@ -130,9 +130,26 @@ def cancellation_url() -> Url:
) )
@pytest.fixture
def tournament_sessions_url() -> Url:
return Url(
name="tournament-sessions",
path="/tournaments_sessions.php",
payloadTemplate="gestion-sports/tournament-sessions-payload.txt",
)
@pytest.fixture
def tournaments_list_url() -> Url:
return Url(
name="tournaments-list",
path="/tournaments_list.html?event=",
)
@pytest.fixture @pytest.fixture
def booking_opening() -> BookingOpening: def booking_opening() -> BookingOpening:
return BookingOpening(daysBefore=10, time="03:27") return BookingOpening(daysBefore=7, time="00:00")
@pytest.fixture @pytest.fixture
@ -142,15 +159,17 @@ def total_bookings() -> TotalBookings:
@pytest.fixture @pytest.fixture
def booking_platform( def booking_platform(
booking_opening, booking_opening: BookingOpening,
total_bookings, total_bookings: TotalBookings,
sport1, sport1: Sport,
sport2, sport2: Sport,
landing_url, landing_url: str,
login_url, login_url: str,
booking_url, booking_url: str,
user_bookings_url, user_bookings_url: str,
cancellation_url, cancellation_url: str,
tournament_sessions_url: str,
tournaments_list_url: str,
) -> BookingPlatform: ) -> BookingPlatform:
return BookingPlatform( return BookingPlatform(
id="gestion-sports", id="gestion-sports",
@ -166,12 +185,14 @@ def booking_platform(
"booking": booking_url, "booking": booking_url,
"user-bookings": user_bookings_url, "user-bookings": user_bookings_url,
"cancellation": cancellation_url, "cancellation": cancellation_url,
"tournament-sessions": tournament_sessions_url,
"tournaments-list": tournaments_list_url,
}, },
) )
@pytest.fixture @pytest.fixture
def club(booking_platform) -> Club: def club(booking_platform: BookingPlatform) -> Club:
return Club( return Club(
id="super_club", id="super_club",
name="Super Club", name="Super Club",
@ -204,42 +225,42 @@ def booking_filter() -> BookingFilter:
@pytest.fixture @pytest.fixture
def landing_response() -> str: def landing_response() -> str:
landing_response_file = RESPONSES_FOLDER / "landing_response.html" file = RESPONSES_FOLDER / "landing-response.html"
return landing_response_file.read_text(encoding="utf-8") return file.read_text(encoding="utf-8")
@pytest.fixture @pytest.fixture
def login_success_response() -> dict: def login_success_response() -> dict:
login_success_file = RESPONSES_FOLDER / "login_success.json" login_success_file = RESPONSES_FOLDER / "login-success.json"
return json.loads(login_success_file.read_text(encoding="utf-8")) return json.loads(login_success_file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def login_failure_response() -> dict: def login_failure_response() -> dict:
login_failure_file = RESPONSES_FOLDER / "login_failure.json" file = RESPONSES_FOLDER / "login-failure.json"
return json.loads(login_failure_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def booking_success_response() -> dict: def booking_success_response() -> dict:
booking_success_file = RESPONSES_FOLDER / "booking_success.json" file = RESPONSES_FOLDER / "booking-success.json"
return json.loads(booking_success_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def booking_failure_response() -> dict: def booking_failure_response() -> dict:
booking_failure_file = RESPONSES_FOLDER / "booking_failure.json" file = RESPONSES_FOLDER / "booking-failure.json"
return json.loads(booking_failure_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def booked_courts_response( def booked_courts_response(
court11, court11: int,
court12, court12: int,
court13, court13: int,
court14, court14: int,
booking_success_response, booking_success_response: dict,
booking_failure_response, booking_failure_response: dict,
) -> list[tuple[int, dict]]: ) -> list[tuple[int, dict]]:
court1_resp = court11.id, booking_failure_response court1_resp = court11.id, booking_failure_response
court2_resp = court12.id, booking_failure_response court2_resp = court12.id, booking_failure_response
@ -250,10 +271,10 @@ def booked_courts_response(
@pytest.fixture @pytest.fixture
def booking_success_from_start( def booking_success_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
booking_success_response, booking_success_response: str,
booking_failure_response, booking_failure_response: str,
): ):
return [ return [
landing_response, landing_response,
@ -267,10 +288,10 @@ def booking_success_from_start(
@pytest.fixture @pytest.fixture
def booking_failure_from_start( def booking_failure_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
booking_success_response, booking_success_response: str,
booking_failure_response, booking_failure_response: str,
): ):
return [ return [
landing_response, landing_response,
@ -284,22 +305,22 @@ def booking_failure_from_start(
@pytest.fixture @pytest.fixture
def user_bookings_get_response() -> str: def user_bookings_get_response() -> str:
user_bookings_file = RESPONSES_FOLDER / "user_bookings_get.html" file = RESPONSES_FOLDER / "user-bookings-get.html"
return user_bookings_file.read_text(encoding="utf-8") return file.read_text(encoding="utf-8")
@pytest.fixture @pytest.fixture
def user_bookings_list() -> list: def user_bookings_list() -> str:
user_bookings_file = RESPONSES_FOLDER / "user_bookings_post.json" file = RESPONSES_FOLDER / "user-bookings-post.json"
return json.loads(user_bookings_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def user_has_ongoing_bookings_from_start( def user_has_ongoing_bookings_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
user_bookings_get_response, user_bookings_get_response: str,
user_bookings_list, user_bookings_list: str,
) -> list: ) -> list:
return [ return [
landing_response, landing_response,
@ -316,10 +337,10 @@ def user_bookings_empty_list() -> list:
@pytest.fixture @pytest.fixture
def user_has_no_ongoing_bookings_from_start( def user_has_no_ongoing_bookings_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
user_bookings_get_response, user_bookings_get_response: str,
user_bookings_empty_list, user_bookings_empty_list: str,
) -> list: ) -> list:
return [ return [
landing_response, landing_response,
@ -331,16 +352,16 @@ def user_has_no_ongoing_bookings_from_start(
@pytest.fixture @pytest.fixture
def cancellation_response() -> list: def cancellation_response() -> list:
cancellation_response_file = RESPONSES_FOLDER / "cancellation_response.json" file = RESPONSES_FOLDER / "cancellation-response.json"
return json.loads(cancellation_response_file.read_text(encoding="utf-8")) return json.loads(file.read_text(encoding="utf-8"))
@pytest.fixture @pytest.fixture
def cancellation_by_id_from_start( def cancellation_by_id_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
user_bookings_get_response, user_bookings_get_response: str,
cancellation_response, cancellation_response: str,
): ):
return [ return [
landing_response, landing_response,
@ -352,11 +373,11 @@ def cancellation_by_id_from_start(
@pytest.fixture @pytest.fixture
def cancellation_success_from_start( def cancellation_success_from_start(
landing_response, landing_response: str,
login_success_response, login_success_response: str,
user_bookings_get_response, user_bookings_get_response: str,
user_bookings_list, user_bookings_list: str,
cancellation_response, cancellation_response: str,
): ):
return [ return [
landing_response, landing_response,
@ -377,3 +398,30 @@ def cancellation_success_booking_filter() -> BookingFilter:
@pytest.fixture @pytest.fixture
def service() -> GestionSportsServices: def service() -> GestionSportsServices:
return GestionSportsServices() return GestionSportsServices()
@pytest.fixture
def tournament_sessions_json() -> str:
file = RESPONSES_FOLDER / "tournament-sessions.json"
return file.read_text(encoding="utf-8")
@pytest.fixture
def tournaments_html() -> str:
file = RESPONSES_FOLDER / "tournaments.html"
return file.read_text(encoding="utf-8")
@pytest.fixture
def full_tournaments_responses(
landing_response: str,
login_success_response: str,
tournament_sessions_json: str,
tournaments_html: str,
) -> list[str]:
return [
landing_response,
login_success_response,
tournament_sessions_json,
tournaments_html,
]

View file

@ -1,31 +1,24 @@
import json
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from aiohttp import ClientSession from aiohttp import ClientSession
from connectors import GestionSportsConnector from gestion_sport_connector import GestionSportsConnector
from tests.unit_tests import responses from tests.unit_tests import responses
def test_urls(connector, club): def test_urls(connector, club):
base_url = club.booking_platform.url base_url = "https://ptf1.com"
relative_urls = club.booking_platform.urls
relative_landing_url = relative_urls.get("landing-page").path assert connector.landing_url == f"{base_url}/landing.html"
assert connector.landing_url == f"{base_url}/{relative_landing_url}" assert connector.login_url == f"{base_url}/login.html"
assert connector.booking_url == f"{base_url}/booking.html"
relative_login_url = relative_urls.get("login").path assert connector.user_bookings_url == f"{base_url}/user_bookings.html"
assert connector.login_url == f"{base_url}/{relative_login_url}" assert connector.booking_cancellation_url == f"{base_url}/cancel.html"
assert connector.tournaments_sessions_url == f"{base_url}/tournaments_sessions.php"
relative_booking_url = relative_urls.get("booking").path assert connector.tournaments_list_url == f"{base_url}/tournaments_list.html?event="
assert connector.booking_url == f"{base_url}/{relative_booking_url}"
relative_user_bookings_url = relative_urls.get("user-bookings").path
assert connector.user_bookings_url == f"{base_url}/{relative_user_bookings_url}"
relative_cancel_url = relative_urls.get("cancellation").path
assert connector.booking_cancellation_url == f"{base_url}/{relative_cancel_url}"
@patch("config.get_resources_folder") @patch("config.get_resources_folder")
@ -34,19 +27,27 @@ def test_urls_payload_templates(mock_resources, club):
mock_resources.return_value = path_to_resources mock_resources.return_value = path_to_resources
connector = GestionSportsConnector(club) connector = GestionSportsConnector(club)
relative_urls = club.booking_platform.urls
login_payload = relative_urls.get("login").payload_template assert (
assert connector.login_template == path_to_resources / login_payload connector.login_template
== path_to_resources / "gestion-sports/login-payload.txt"
booking_payload = relative_urls.get("booking").payload_template )
assert connector.booking_template == path_to_resources / booking_payload assert (
connector.booking_template
user_bookings_payload = relative_urls.get("user-bookings").payload_template == path_to_resources / "gestion-sports/booking-payload.txt"
assert connector.user_bookings_template == path_to_resources / user_bookings_payload )
assert (
cancel_payload = relative_urls.get("cancellation").payload_template connector.user_bookings_template
assert connector.booking_cancel_template == path_to_resources / cancel_payload == path_to_resources / "gestion-sports/user-bookings-payload.txt"
)
assert (
connector.booking_cancel_template
== path_to_resources / "gestion-sports/booking-cancellation-payload.txt"
)
assert (
connector.tournaments_sessions_template
== path_to_resources / "gestion-sports/tournament-sessions-payload.txt"
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -143,3 +144,35 @@ async def test_cancel_booking_success(
) )
assert await response.json() == cancellation_success_from_start[4] assert await response.json() == cancellation_success_from_start[4]
@pytest.mark.asyncio
async def test_tournament_sessions(
aioresponses, connector, user, tournament_sessions_json
):
responses.set_tournaments_sessions_response(
aioresponses, connector, tournament_sessions_json
)
async with ClientSession() as session:
response = await connector.send_tournaments_sessions_request(session)
assert response.status == 200
all_sessions = json.loads(await response.text())
sessions = all_sessions.get("Inscription tournois:school-outline")
assert len(sessions) == 1
@pytest.mark.asyncio
async def test_send_tournaments_request(
aioresponses, connector, user, tournaments_html
):
tournament_session_id = "255"
responses.set_tournaments_list_response(
aioresponses, connector, tournament_session_id, tournaments_html
)
async with ClientSession() as session:
response = await connector.send_tournaments_request(
session, tournament_session_id
)
assert "<span class='nb_place_libre'>Complet</span>" in await response.text()

View file

@ -1,5 +1,9 @@
from unittest.mock import patch
import pendulum
import pytest import pytest
from gestion_sports_services import GestionSportsServices from gestion_sports_services import GestionSportsServices
from models import BookingFilter, BookingOpening
from tests.unit_tests import responses from tests.unit_tests import responses
@ -108,3 +112,65 @@ async def test_cancel_booking_id(
) )
await gs_services.cancel_booking_id(user, club, 65464) await gs_services.cancel_booking_id(user, club, 65464)
@patch("pendulum.now")
def test_wait_until_booking_time(mock_now, club, user):
booking_filter = BookingFilter(
sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00+01:00")
)
booking_datetime = pendulum.parse("2024-03-14T00:00:00+01:00")
seconds = [
booking_datetime.subtract(seconds=3),
booking_datetime.subtract(seconds=2),
booking_datetime.subtract(seconds=1),
booking_datetime,
booking_datetime.add(microseconds=1),
booking_datetime.add(microseconds=2),
]
mock_now.side_effect = seconds
booking_opening = club.booking_platform.booking_opening
GestionSportsServices.wait_until_booking_time(booking_filter, booking_opening)
assert pendulum.now() == booking_datetime.add(microseconds=1)
def test_build_booking_time():
booking_filter = BookingFilter(
sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00+01:00")
)
booking_opening = BookingOpening(daysBefore=7, time="00:00")
booking_time = GestionSportsServices.build_booking_datetime(
booking_filter, booking_opening
)
assert booking_time == pendulum.parse("2024-03-13T23:00:00Z")
def test_retrieve_tournament_id(tournament_sessions_json):
session_id = GestionSportsServices.retrieve_tournament_session(
tournament_sessions_json
)
assert session_id == "1174"
def test_retrieve_tournaments(tournaments_html):
tournaments = GestionSportsServices.retrieve_tournaments(tournaments_html)
assert len(tournaments) == 14
@pytest.mark.asyncio
async def test_get_all_tournaments(
aioresponses, user, connector, club, full_tournaments_responses
):
responses.set_full_tournaments_requests(
aioresponses, connector, full_tournaments_responses, 1174
)
tournaments = await GestionSportsServices.get_all_tournaments(user, club)
assert len(tournaments) == 14