created a gestion sports services class that handles the connection while the connector is dedicated to the requests

This commit is contained in:
Stanislas Jouffroy 2024-03-20 23:11:43 +01:00
parent bcd8dc0733
commit e6023e0687
12 changed files with 513 additions and 593 deletions

View file

@ -2,17 +2,12 @@ import asyncio
import logging import logging
import config import config
from connectors import Connector, GestionSportsConnector from gestion_sports_services import GestionSportsServices
from models import Action, BookingFilter, Club, Court, User from models import Action, BookingFilter, Club, Court, User
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
def get_connector(club: Club) -> Connector:
if club.booking_platform.id == "gestion-sports":
return GestionSportsConnector(club)
async def book_court( async def book_court(
club: Club, users: list[User], booking_filter: BookingFilter club: Club, users: list[User], booking_filter: BookingFilter
) -> tuple[Court, User]: ) -> tuple[Court, User]:
@ -26,10 +21,10 @@ async def book_court(
:return: a tuple containing the court that was booked and the user who made the :return: a tuple containing the court that was booked and the user who made the
booking booking
""" """
connector = get_connector(club) service = GestionSportsServices()
for user in users: for user in users:
if not await connector.has_user_ongoing_booking(user): if not await service.has_user_available_slots(user, club):
return await connector.book(user, booking_filter), user return await service.book(club, user, booking_filter), user
async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter) -> None: async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter) -> None:
@ -40,8 +35,8 @@ async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter)
:param user: the user who made the booking :param user: the user who made the booking
:param booking_filter: the conditions to meet to cancel the booking :param booking_filter: the conditions to meet to cancel the booking
""" """
connector = get_connector(club) service = GestionSportsServices()
await connector.cancel_booking(user, booking_filter) await service.cancel_booking(user, club, booking_filter)
async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None: async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
@ -52,8 +47,8 @@ async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
:param user: the user who made the booking :param user: the user who made the booking
:param booking_id: the id of the booking to cancel :param booking_id: the id of the booking to cancel
""" """
connector = get_connector(club) service = GestionSportsServices()
await connector.cancel_booking_id(user, booking_id) await service.cancel_booking_id(user, club, booking_id)
def main() -> tuple[Court, User] | None: def main() -> tuple[Court, User] | None:

View file

@ -1,13 +1,10 @@
import asyncio import asyncio
import json import json
import logging import logging
import time
from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
import config import config
import pendulum
from aiohttp import ClientResponse, ClientSession from aiohttp import ClientResponse, ClientSession
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from models import Booking, BookingFilter, Club, Court, Sport, User from models import Booking, BookingFilter, Club, Court, Sport, User
@ -19,55 +16,7 @@ LOGGER = logging.getLogger(__name__)
POST_HEADERS = config.get_post_headers("gestion-sports") POST_HEADERS = config.get_post_headers("gestion-sports")
class Connector(ABC): class GestionSportsConnector:
"""
Abstract class that defines the method a connector
to a website for sport booking should have
"""
@abstractmethod
async def book(self, user: User, booking_filter: BookingFilter) -> Court | None:
"""
Book a court matching the filter for a user
:param user: the user who will have the booking
:param booking_filter: the conditions to book (date, time, court)
:return: the court booked
"""
pass
@abstractmethod
async def has_user_ongoing_booking(self, user: User) -> bool:
"""
Test whether the user has ongoing bookings
:param user: the user who will have the booking
:return: true if the user has at least one ongoing booking, false otherwise
"""
pass
@abstractmethod
async def cancel_booking_id(self, user: User, booking_id: int) -> None:
"""
Cancel the booking for a given user
:param user: the user who has the booking
:param booking_id: the id of the booking
"""
pass
@abstractmethod
async def cancel_booking(self, user: User, booking_filter: BookingFilter) -> None:
"""
Cancel the booking that meet some conditions for a given user
:param user: the user who has the booking
:param booking_filter: the booking conditions to meet to cancel the booking
"""
pass
class GestionSportsConnector(Connector):
""" """
The connector for the Gestion Sports platform. The connector for the Gestion Sports platform.
It handles all the requests to the website. It handles all the requests to the website.
@ -266,14 +215,16 @@ class GestionSportsConnector(Connector):
LOGGER.debug("Connexion request response:\n%s", resp_text) LOGGER.debug("Connexion request response:\n%s", resp_text)
return response return response
async def book(self, user: User, booking_filter: BookingFilter) -> Court | None: async def book_any_court(
self, session: ClientSession, booking_filter: BookingFilter
) -> list[tuple[int, dict]]:
""" """
Perform a request for each court at the same time to increase the chances to get Perform a request for each court at the same time to increase the chances to get
a booking. a booking.
The gestion-sports backend does not allow several bookings at the same time The gestion-sports backend does not allow several bookings at the same time
so there is no need to make each request one after the other so there is no need to make each request one after the other
:param user: the user that wants to book the court :param session: the session to use
:param booking_filter: the booking conditions to meet :param booking_filter: the booking conditions to meet
:return: the booked court, or None if no court was booked :return: the booked court, or None if no court was booked
""" """
@ -283,24 +234,18 @@ class GestionSportsConnector(Connector):
sport = self.available_sports.get(booking_filter.sport_name) sport = self.available_sports.get(booking_filter.sport_name)
async with ClientSession() as session: bookings = await asyncio.gather(
# use asyncio to request a booking on every court *[
# the gestion-sports backend is able to book only one court for a user self.send_booking_request(
await self.land(session) session, booking_filter.date, court.id, sport.id
await self.login(session, user) )
self.wait_until_booking_time(booking_filter) for court in sport.courts
bookings = await asyncio.gather( ],
*[ return_exceptions=True,
self.send_booking_request( )
session, booking_filter.date, court.id, sport.id
)
for court in sport.courts
],
return_exceptions=True,
)
LOGGER.debug("Booking results:\n'%s'", bookings) LOGGER.debug("Booking results:\n'%s'", bookings)
return self.get_booked_court(bookings, sport.name) return bookings
async def send_booking_request( async def send_booking_request(
self, self,
@ -308,7 +253,7 @@ class GestionSportsConnector(Connector):
date: DateTime, date: DateTime,
court_id: int, court_id: int,
sport_id: int, sport_id: int,
) -> tuple[ClientResponse, int, bool]: ) -> tuple[int, dict]:
""" """
Book a single court that meets the conditions from the booking filter Book a single court that meets the conditions from the booking filter
@ -316,7 +261,7 @@ class GestionSportsConnector(Connector):
:param date: the booking date :param date: the booking date
:param court_id: the id of the court to book :param court_id: the id of the court to book
:param sport_id: the id of the sport :param sport_id: the id of the sport
:return: a tuple containing the court id and the booking status :return: a tuple containing the court id and the response
""" """
LOGGER.debug("Booking court %s at %s", court_id, date.to_w3c_string()) LOGGER.debug("Booking court %s at %s", court_id, date.to_w3c_string())
payload = PayloadBuilder.build( payload = PayloadBuilder.build(
@ -332,12 +277,12 @@ class GestionSportsConnector(Connector):
self.booking_url, data=payload, headers=POST_HEADERS self.booking_url, data=payload, headers=POST_HEADERS
) as response: ) as response:
assert response.status == 200 assert response.status == 200
resp_json = await response.text() resp_json = json.loads(await response.text())
LOGGER.debug("Response from booking request:\n'%s'", resp_json) LOGGER.debug("Response from booking request:\n'%s'", resp_json)
return response, court_id, self.is_booking_response_status_ok(resp_json) return court_id, resp_json
def get_booked_court( def get_booked_court(
self, bookings: list[tuple[ClientSession, int, bool]], sport_name: str self, bookings: list[tuple[int, dict]], sport_name: str
) -> Court | None: ) -> Court | None:
""" """
Parse the booking list and return the court that was booked Parse the booking list and return the court that was booked
@ -346,8 +291,8 @@ class GestionSportsConnector(Connector):
:param sport_name: the sport name :param sport_name: the sport name
:return: the id of the booked court if any, None otherwise :return: the id of the booked court if any, None otherwise
""" """
for _, court_id, is_booked in bookings: for court_id, response in bookings:
if is_booked: if self.is_booking_response_status_ok(response):
LOGGER.debug("Court %d is booked", court_id) LOGGER.debug("Court %d is booked", court_id)
court_booked = self.find_court(court_id, sport_name) court_booked = self.find_court(court_id, sport_name)
LOGGER.info("Court '%s' is booked", court_booked.name) LOGGER.info("Court '%s' is booked", court_booked.name)
@ -369,70 +314,14 @@ class GestionSportsConnector(Connector):
return court return court
@staticmethod @staticmethod
def is_booking_response_status_ok(response: str) -> bool: def is_booking_response_status_ok(response: dict) -> bool:
""" """
Check if the booking response is OK Check if the booking response is OK
:param response: the response as a string :param response: the response as a string
:return: true if the status is ok, false otherwise :return: true if the status is ok, false otherwise
""" """
formatted_result = response.removeprefix('"').removesuffix('"') return response["status"] == "ok"
result_json = json.loads(formatted_result)
return result_json["status"] == "ok"
def build_booking_datetime(self, booking_filter: BookingFilter) -> DateTime:
"""
Build the date and time when the booking is open for a given match date.
The booking filter contains the date and time of the booking.
The club has the information about when the booking is open for that date.
:param booking_filter: the booking information
:return: the date and time when the booking is open
"""
date_to_book = booking_filter.date
booking_opening = self.club.booking_platform.booking_opening
booking_date = date_to_book.subtract(days=booking_opening.days_before)
opening_time = pendulum.parse(booking_opening.opening_time)
booking_hour = opening_time.hour
booking_minute = opening_time.minute
return booking_date.at(booking_hour, booking_minute)
def wait_until_booking_time(self, booking_filter: BookingFilter) -> None:
"""
Wait until the booking is open.
The booking filter contains the date and time of the booking.
The club has the information about when the booking is open for that date.
:param booking_filter: the booking information
"""
LOGGER.info("Waiting for booking time")
booking_datetime = self.build_booking_datetime(booking_filter)
now = pendulum.now()
duration_until_booking = booking_datetime - now
LOGGER.debug(f"Current time: {now}, Datetime to book: {booking_datetime}")
LOGGER.debug(
f"Time to wait before booking: {duration_until_booking.hours:0>2}"
f":{duration_until_booking.minutes:0>2}"
f":{duration_until_booking.seconds:0>2}"
)
while now < booking_datetime:
time.sleep(1)
now = pendulum.now()
LOGGER.info("It's booking time!")
async def has_user_ongoing_booking(self, user: User) -> bool:
"""
Check if the user currently has bookings in the future
:param user: the user to check the bookings
:return: true if the user has some bookings, false otherwise
"""
async with ClientSession() as session:
await self.land(session)
await self.login(session, user)
return bool(await self.get_ongoing_bookings(session))
async def get_ongoing_bookings(self, session: ClientSession) -> list[Booking]: async def get_ongoing_bookings(self, session: ClientSession) -> list[Booking]:
""" """
@ -494,21 +383,7 @@ class GestionSportsConnector(Connector):
LOGGER.debug("ongoing bookings response: %s\n", resp) LOGGER.debug("ongoing bookings response: %s\n", resp)
return [Booking(**booking) for booking in json.loads(resp)] return [Booking(**booking) for booking in json.loads(resp)]
async def cancel_booking_id(self, user: User, booking_id: int) -> ClientResponse: async def cancel_booking_id(
"""
Cancel a booking based on its id for a given user
:param user: the user that has the booking
:param booking_id: the id of the booking to cancel
:return: the response from the client
"""
async with ClientSession() as session:
await self.land(session)
await self.login(session, user)
return await self.send_cancellation_request(session, booking_id)
async def send_cancellation_request(
self, session: ClientSession, booking_id: int self, session: ClientSession, booking_id: int
) -> ClientResponse: ) -> ClientResponse:
""" """
@ -533,20 +408,16 @@ class GestionSportsConnector(Connector):
return response return response
async def cancel_booking( async def cancel_booking(
self, user: User, booking_filter: BookingFilter self, session: ClientSession, booking_filter: BookingFilter
) -> ClientResponse | None: ) -> ClientResponse | None:
""" """
Cancel the booking that meets some conditions Cancel the booking that meets some conditions
:param user: the user who owns the booking :param session: the session
:param booking_filter: the conditions the booking to cancel should meet :param booking_filter: the conditions the booking to cancel should meet
""" """
async with ClientSession() as session: bookings = await self.get_ongoing_bookings(session)
await self.land(session)
await self.login(session, user)
bookings = await self.get_ongoing_bookings(session) for booking in bookings:
if booking.matches(booking_filter):
for booking in bookings: return await self.cancel_booking_id(session, booking.id)
if booking.matches(booking_filter):
return await self.send_cancellation_request(session, booking.id)

View file

@ -0,0 +1,127 @@
import logging
import time
import pendulum
from aiohttp import ClientSession
from connectors import GestionSportsConnector
from models import BookingFilter, BookingOpening, Club, Court, User
from pendulum import DateTime
LOGGER = logging.getLogger(__name__)
class GestionSportsServices:
@staticmethod
async def book(
club: Club, user: User, booking_filter: BookingFilter
) -> Court | None:
"""
Perform a request for each court at the same time to increase the chances to get
a booking.
The gestion-sports backend does not allow several bookings at the same time
so there is no need to make each request one after the other
:param club: the club in which the booking will be made
:param user: the user that wants to book the court
:param booking_filter: the booking conditions to meet
:return: the booked court, or None if no court was booked
"""
connector = GestionSportsConnector(club)
LOGGER.info(
"Booking any available court from GestionSports API at %s",
connector.booking_url,
)
async with ClientSession() as session:
# use asyncio to request a booking on every court
# the gestion-sports backend is able to book only one court for a user
await connector.land(session)
await connector.login(session, user)
booking_opening = club.booking_platform.booking_opening
GestionSportsServices.wait_until_booking_time(
booking_filter, booking_opening
)
bookings = await connector.book_any_court(session, booking_filter)
LOGGER.debug("Booking results:\n'%s'", bookings)
return connector.get_booked_court(bookings, booking_filter.sport_name)
@staticmethod
async def has_user_available_slots(user: User, club: Club) -> bool:
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
bookings = await connector.get_ongoing_bookings(session)
return bool(bookings)
@staticmethod
async def cancel_booking(user: User, club: Club, booking_filter: BookingFilter):
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
await connector.cancel_booking(session, booking_filter)
@staticmethod
async def cancel_booking_id(user: User, club: Club, booking_id: int):
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
await connector.cancel_booking_id(session, booking_id)
@staticmethod
def wait_until_booking_time(
booking_filter: BookingFilter, booking_opening: BookingOpening
) -> None:
"""
Wait until the booking is open.
The booking filter contains the date and time of the booking.
The club has the information about when the booking is open for that date.
:param booking_opening:
:param booking_filter: the booking information
"""
LOGGER.info("Waiting for booking time")
booking_datetime = GestionSportsServices.build_booking_datetime(
booking_filter, booking_opening
)
now = pendulum.now()
duration_until_booking = booking_datetime - now
LOGGER.debug(f"Current time: {now}, Datetime to book: {booking_datetime}")
LOGGER.debug(
f"Time to wait before booking: {duration_until_booking.hours:0>2}"
f":{duration_until_booking.minutes:0>2}"
f":{duration_until_booking.seconds:0>2}"
)
while now < booking_datetime:
time.sleep(1)
now = pendulum.now()
LOGGER.info("It's booking time!")
@staticmethod
def build_booking_datetime(
booking_filter: BookingFilter, booking_opening: BookingOpening
) -> DateTime:
"""
Build the date and time when the booking is open for a given match date.
The booking filter contains the date and time of the booking.
The club has the information about when the booking is open for that date.
:param booking_opening:the booking opening conditions
:param booking_filter: the booking information
:return: the date and time when the booking is open
"""
date_to_book = booking_filter.date
booking_date = date_to_book.subtract(days=booking_opening.days_before)
opening_time = pendulum.parse(booking_opening.opening_time)
booking_hour = opening_time.hour
booking_minute = opening_time.minute
return booking_date.at(booking_hour, booking_minute)

View file

@ -0,0 +1,46 @@
import json
from pathlib import Path
import config
import pendulum
import pytest
from connectors import GestionSportsConnector
from models import BookingFilter, Club, User
TEST_FOLDER = Path(__file__).parent.parent
DATA_FOLDER = TEST_FOLDER / "data"
RESPONSES_FOLDER = DATA_FOLDER / "responses"
@pytest.fixture
def club() -> Club:
return config.get_clubs()["tpc"]
@pytest.fixture
def connector(club) -> GestionSportsConnector:
return GestionSportsConnector(club)
@pytest.fixture
def user() -> User:
return User(login="padel.testing@jouf.fr", password="ridicule")
@pytest.fixture
def booking_filter() -> BookingFilter:
return BookingFilter(
sport_name="Padel", date=pendulum.parse("2024-03-21T13:30:00+01:00")
)
@pytest.fixture
def booking_success_response() -> dict:
booking_success_file = RESPONSES_FOLDER / "booking_success.json"
return json.loads(booking_success_file.read_text(encoding="utf-8"))
@pytest.fixture
def booking_failure_response() -> dict:
booking_failure_file = RESPONSES_FOLDER / "booking_failure.json"
return json.loads(booking_failure_file.read_text(encoding="utf-8"))

View file

@ -2,10 +2,6 @@ import asyncio
import os import os
from unittest.mock import patch from unittest.mock import patch
import config
import pendulum
from models import BookingFilter, User
from resa_padel import booking from resa_padel import booking
@ -14,12 +10,7 @@ from resa_padel import booking
{"CLUB_ID": "tpc"}, {"CLUB_ID": "tpc"},
clear=True, clear=True,
) )
def test_booking(): def test_booking(club, user, booking_filter):
club = config.get_club()
user = User(login="padel.testing@jouf.fr", password="ridicule")
booking_filter = BookingFilter(
sport_name="Padel", date=pendulum.parse("2024-03-21T13:30:00+01:00")
)
booked_court, user_that_booked = asyncio.run( booked_court, user_that_booked = asyncio.run(
booking.book_court(club, [user], booking_filter) booking.book_court(club, [user], booking_filter)
) )
@ -32,10 +23,8 @@ def test_booking():
{"CLUB_ID": "tpc"}, {"CLUB_ID": "tpc"},
clear=True, clear=True,
) )
def test_cancellation(): def test_cancellation(club, user, booking_filter):
club = config.get_club() asyncio.run(booking.cancel_booking(club, user, booking_filter))
user = User(login="padel.testing@jouf.fr", password="ridicule")
asyncio.run(booking.cancel_booking_id(club, user, 3605033))
@patch.dict( @patch.dict(
@ -49,9 +38,9 @@ def test_cancellation():
clear=True, clear=True,
) )
def test_main_booking(): def test_main_booking():
court, user = booking.main() court, player = booking.main()
assert court is not None assert court is not None
assert user.username == "padel.testing@jouf" assert player.login == "padel.testing@jouf.fr"
@patch.dict( @patch.dict(

View file

@ -3,12 +3,10 @@ from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import aiohttp import aiohttp
import config
import pendulum import pendulum
import pytest import pytest
from aiohttp import ClientSession
from connectors import GestionSportsConnector from connectors import GestionSportsConnector
from models import Booking, BookingFilter, Club, User from models import BookingFilter, Club
from pendulum import DateTime from pendulum import DateTime
from yarl import URL from yarl import URL
@ -38,9 +36,7 @@ def retrieve_booking_datetime(
{"CLUB_ID": "tpc"}, {"CLUB_ID": "tpc"},
clear=True, clear=True,
) )
def test_urls(): def test_urls(connector):
club = config.get_club()
connector = GestionSportsConnector(club)
assert ( assert (
connector.landing_url connector.landing_url
== "https://toulousepadelclub.gestion-sports.com/connexion.php" == "https://toulousepadelclub.gestion-sports.com/connexion.php"
@ -65,12 +61,10 @@ def test_urls():
@patch.dict( @patch.dict(
os.environ, os.environ,
{"CLUB_ID": "tpc", "RESOURCES_FOLDER": "/some/path"}, {"RESOURCES_FOLDER": "/some/path"},
clear=True, clear=True,
) )
def test_urls_payload_templates(): def test_urls_payload_templates(connector):
club = config.get_club()
connector = GestionSportsConnector(club)
resources_folder = Path("/some", "path", "gestion-sports") resources_folder = Path("/some", "path", "gestion-sports")
assert connector.login_template == resources_folder / "login-payload.txt" assert connector.login_template == resources_folder / "login-payload.txt"
assert connector.booking_template == resources_folder / "booking-payload.txt" assert connector.booking_template == resources_folder / "booking-payload.txt"
@ -84,15 +78,8 @@ def test_urls_payload_templates():
) )
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_landing_page(): async def test_landing_page(connector):
club = config.get_club()
connector = GestionSportsConnector(club)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
response = await connector.land(session) response = await connector.land(session)
@ -104,17 +91,8 @@ async def test_landing_page():
assert response.cookies.get("PHPSESSID") is not None assert response.cookies.get("PHPSESSID") is not None
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login(): async def test_login(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
@ -130,90 +108,46 @@ async def test_login():
assert response.cookies.get("COOK_ID_USER").value == "232382" assert response.cookies.get("COOK_ID_USER").value == "232382"
@patch.dict( def test_get_booked_court(
os.environ, connector, booking_success_response, booking_failure_response
{"CLUB_ID": "tpc"}, ):
clear=True,
)
def test_get_booked_court():
club = config.get_club()
connector = GestionSportsConnector(club)
session = ClientSession()
bookings = [ bookings = [
(session, 601, False), (601, booking_failure_response),
(session, 602, False), (602, booking_failure_response),
(session, 603, False), (603, booking_failure_response),
(session, 614, False), (614, booking_failure_response),
(session, 605, False), (605, booking_failure_response),
(session, 606, True), (606, booking_success_response),
(session, 607, False), (607, booking_failure_response),
(session, 608, False), (608, booking_failure_response),
] ]
court = connector.get_booked_court(bookings, "padel") court = connector.get_booked_court(bookings, "padel")
assert court.number == 9 assert court.number == 9
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_book_one_court(): async def test_book_one_court(connector, user, booking_filter):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
response, court_id, ok = await connector.send_booking_request(
session, pendulum.parse("2024-03-21T13:30:00Z"), 610, 217 court_id, response = await connector.send_booking_request(
session, pendulum.parse("2024-03-21T13:30:00+01:00"), 610, 217
) )
assert response.status == 200 assert court_id == 610
assert response.request_info.method == "POST" assert response.get("status") == "ok"
assert response.content_type == "text/html"
assert response.request_info.url == URL(connector.booking_url)
assert response.charset == "UTF-8"
assert response.text is not None
assert court_id == 610
assert ok is True
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_book(): async def test_book(connector, user, booking_filter):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
booking_filter = BookingFilter(
sport_name="Padel", date=pendulum.parse("2024-03-21T13:30:00Z")
)
booked_court = await connector.book(user, booking_filter) booked_court = await connector.book(user, booking_filter)
assert booked_court is not None assert booked_court is not None
@patch.dict( def test_build_booking_datetime(connector, booking_filter):
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
def test_build_booking_datetime():
club = config.get_club()
connector = GestionSportsConnector(club)
booking_filter = BookingFilter(
sport_name="Padel", date=pendulum.parse("2024-03-21T13:30:00Z")
)
opening_datetime = connector.build_booking_datetime(booking_filter) opening_datetime = connector.build_booking_datetime(booking_filter)
assert opening_datetime.year == 2024 assert opening_datetime.year == 2024
assert opening_datetime.month == 3 assert opening_datetime.month == 3
@ -222,19 +156,8 @@ def test_build_booking_datetime():
assert opening_datetime.minute == 0 assert opening_datetime.minute == 0
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@patch("pendulum.now") @patch("pendulum.now")
def test_wait_until_booking_time(mock_now): def test_wait_until_booking_time(mock_now, connector, booking_filter, club):
club = config.get_club()
connector = GestionSportsConnector(club)
booking_filter = BookingFilter(
sport_name="Padel", date=pendulum.parse("2024-03-21T13:30:00Z")
)
booking_datetime = retrieve_booking_datetime(booking_filter, club) booking_datetime = retrieve_booking_datetime(booking_filter, club)
seconds = [ seconds = [
@ -252,17 +175,8 @@ def test_wait_until_booking_time(mock_now):
assert pendulum.now() == booking_datetime.add(microseconds=1) assert pendulum.now() == booking_datetime.add(microseconds=1)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_hash(): async def test_get_hash(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -281,17 +195,8 @@ def test_get_hash_input():
assert hash_value == "63470fa38e300fd503de1ee21a71b3bdb6fb206b" assert hash_value == "63470fa38e300fd503de1ee21a71b3bdb6fb206b"
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_bookings(): async def test_get_bookings(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -303,17 +208,8 @@ async def test_get_bookings():
print(bookings) print(bookings)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_ongoing_bookings(): async def test_get_ongoing_bookings(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -322,31 +218,13 @@ async def test_get_ongoing_bookings():
print(bookings) print(bookings)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_has_user_ongoing_bookings(): async def test_has_user_ongoing_bookings(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
assert await connector.has_user_ongoing_booking(user) assert await connector.has_user_ongoing_booking(user)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking_id(): async def test_cancel_booking_id(connector, user):
club = config.get_club()
connector = GestionSportsConnector(club)
user = User(login="padel.testing@jouf.fr", password="ridicule")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
await connector.land(session) await connector.land(session)
await connector.login(session, user) await connector.login(session, user)
@ -358,73 +236,16 @@ async def test_cancel_booking_id():
assert len(await connector.get_ongoing_bookings(session)) == 0 assert len(await connector.get_ongoing_bookings(session)) == 0
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
def test_is_booking_matching_filter():
club = config.get_club()
connector = GestionSportsConnector(club)
filter_date = pendulum.parse("2024-03-02T15:00:00+01:00")
booking = Booking(
id=1,
dateResa="02/03/2024",
startTime="15:00",
sport="Padel",
court="10",
)
booking_filter = BookingFilter(date=filter_date, sport_name="Padel")
assert connector.is_booking_matching_filter(booking, booking_filter)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
def test_is_booking_not_matching_filter():
club = config.get_club()
connector = GestionSportsConnector(club)
filter_date = pendulum.parse("2024-03-02T15:00:00+01:00")
booking = Booking(
id=1,
dateResa="02/03/2024",
startTime="16:00",
sport="Padel",
court="10",
)
booking_filter = BookingFilter(date=filter_date, sport_name="Padel")
assert not connector.is_booking_matching_filter(booking, booking_filter)
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
def test_find_court(): def test_find_court(connector):
club = config.get_club()
connector = GestionSportsConnector(club)
court = connector.find_court(603, "Padel") court = connector.find_court(603, "Padel")
assert court.number == 6 assert court.number == 6
@patch.dict(
os.environ,
{"CLUB_ID": "tpc"},
clear=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking(): async def test_cancel_booking(connector, user, booking_filter):
club = config.get_club() async with aiohttp.ClientSession() as session:
connector = GestionSportsConnector(club) await connector.land(session)
user = User(login="padel.testing@jouf.fr", password="ridicule") await connector.login(session, user)
filter_date = pendulum.parse("2024-03-21T13:30:00+01:00") await connector.cancel_booking(session, booking_filter)
booking_filter = BookingFilter(date=filter_date, sport_name="Padel")
await connector.cancel_booking(user, booking_filter)

View file

@ -0,0 +1,20 @@
import pytest
from gestion_sports_services import GestionSportsServices
@pytest.mark.asyncio
async def test_booking_success(club, user, booking_filter):
court_booked = await GestionSportsServices.book(club, user, booking_filter)
assert court_booked.id is not None
@pytest.mark.asyncio
async def test_user_has_available_slots(club, user):
has_slots = await GestionSportsServices.has_user_available_slots(user, club)
assert has_slots
@pytest.mark.asyncio
async def test_cancel_booking(club, user, booking_filter):
await GestionSportsServices.cancel_booking(user, club, booking_filter)

View file

@ -4,6 +4,7 @@ from pathlib import Path
import pendulum import pendulum
import pytest import pytest
from connectors import GestionSportsConnector from connectors import GestionSportsConnector
from gestion_sports_services import GestionSportsServices
from models import ( from models import (
BookingFilter, BookingFilter,
BookingOpening, BookingOpening,
@ -184,6 +185,11 @@ def connector(club) -> GestionSportsConnector:
return GestionSportsConnector(club) return GestionSportsConnector(club)
@pytest.fixture
def gs_services() -> GestionSportsServices:
return GestionSportsServices()
@pytest.fixture @pytest.fixture
def user() -> User: def user() -> User:
return User(login="padel.testing@jouf.fr", password="ridicule") return User(login="padel.testing@jouf.fr", password="ridicule")
@ -226,6 +232,22 @@ def booking_failure_response() -> dict:
return json.loads(booking_failure_file.read_text(encoding="utf-8")) return json.loads(booking_failure_file.read_text(encoding="utf-8"))
@pytest.fixture
def booked_courts_response(
court11,
court12,
court13,
court14,
booking_success_response,
booking_failure_response,
) -> list[tuple[int, dict]]:
court1_resp = court11.id, booking_failure_response
court2_resp = court12.id, booking_failure_response
court3_resp = court13.id, booking_success_response
court4_resp = court14.id, booking_failure_response
return [court1_resp, court2_resp, court3_resp, court4_resp]
@pytest.fixture @pytest.fixture
def booking_success_from_start( def booking_success_from_start(
landing_response, landing_response,
@ -350,3 +372,8 @@ def cancellation_success_booking_filter() -> BookingFilter:
return BookingFilter( return BookingFilter(
sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00Z") sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00Z")
) )
@pytest.fixture
def service() -> GestionSportsServices:
return GestionSportsServices()

View file

@ -0,0 +1,83 @@
def make_landing_request_success(aioresponses, connector, landing_response):
aioresponses.get(
connector.landing_url,
status=200,
headers={"Set-Cookie": "PHPSESSID=987512"},
body=landing_response,
)
def make_login_request_fail(aioresponses, connector, login_failure_response):
aioresponses.post(
connector.login_url,
status=200,
payload=login_failure_response,
)
def make_login_request_success(aioresponses, connector, login_success_response):
aioresponses.post(
connector.login_url,
status=200,
headers={"Set-Cookie": "COOK_COMPTE=e2be1;" "COOK_ID_CLUB=22;COOK_ID_USER=666"},
payload=login_success_response,
)
def set_booking_request(aioresponses, connector, booking_response):
aioresponses.post(connector.booking_url, status=200, payload=booking_response)
def set_full_booking_requests_responses(aioresponses, connector, responses_list):
make_landing_request_success(aioresponses, connector, responses_list[0])
make_login_request_success(aioresponses, connector, responses_list[1])
for response in responses_list[2:]:
set_booking_request(aioresponses, connector, response)
def set_ongoing_bookings_response(
aioresponses, connector, user_bookings_get_response, user_bookings_post_response
):
set_hash_response(aioresponses, connector, user_bookings_get_response)
set_bookings_response(aioresponses, connector, user_bookings_post_response)
def set_hash_response(aioresponses, connector, user_bookings_get_response):
aioresponses.get(
connector.user_bookings_url, status=200, body=user_bookings_get_response
)
def set_bookings_response(aioresponses, connector, user_bookings_post_response):
aioresponses.post(
connector.user_bookings_url, status=200, payload=user_bookings_post_response
)
def set_full_user_bookings_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
set_ongoing_bookings_response(aioresponses, connector, *responses[2:])
def set_cancellation_response(aioresponses, connector, response):
aioresponses.post(connector.booking_cancellation_url, status=200, payload=response)
def set_full_cancellation_by_id_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
set_hash_response(aioresponses, connector, responses[2])
set_cancellation_response(aioresponses, connector, responses[3])
def set_full_cancellation_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
# the request to get the hash is made twice
set_hash_response(aioresponses, connector, responses[2])
set_hash_response(aioresponses, connector, responses[2])
set_bookings_response(aioresponses, connector, responses[3])
set_cancellation_response(aioresponses, connector, responses[4])

View file

@ -15,7 +15,7 @@ from pendulum import DateTime, Timezone
) )
def test_get_booking_filter(): def test_get_booking_filter():
booking_filter = config.get_booking_filter() booking_filter = config.get_booking_filter()
assert booking_filter.sport_id == "padel" assert booking_filter.sport_name == "padel"
assert booking_filter.date == DateTime( assert booking_filter.date == DateTime(
year=2024, year=2024,
month=2, month=2,

View file

@ -5,90 +5,7 @@ import pytest
from aiohttp import ClientSession from aiohttp import ClientSession
from connectors import GestionSportsConnector from connectors import GestionSportsConnector
from tests.unit_tests import responses
def make_landing_request_success(aioresponses, connector, landing_response):
aioresponses.get(
connector.landing_url,
status=200,
headers={"Set-Cookie": "PHPSESSID=987512"},
body=landing_response,
)
def make_login_request_fail(aioresponses, connector, login_failure_response):
aioresponses.post(
connector.login_url,
status=200,
payload=login_failure_response,
)
def make_login_request_success(aioresponses, connector, login_success_response):
aioresponses.post(
connector.login_url,
status=200,
headers={"Set-Cookie": "COOK_COMPTE=e2be1;" "COOK_ID_CLUB=22;COOK_ID_USER=666"},
payload=login_success_response,
)
def set_booking_request(aioresponses, connector, booking_response):
aioresponses.post(connector.booking_url, status=200, payload=booking_response)
def set_full_booking_requests_responses(aioresponses, connector, responses_list):
make_landing_request_success(aioresponses, connector, responses_list[0])
make_login_request_success(aioresponses, connector, responses_list[1])
for response in responses_list[2:]:
set_booking_request(aioresponses, connector, response)
def set_ongoing_bookings_response(
aioresponses, connector, user_bookings_get_response, user_bookings_post_response
):
set_hash_response(aioresponses, connector, user_bookings_get_response)
set_bookings_response(aioresponses, connector, user_bookings_post_response)
def set_hash_response(aioresponses, connector, user_bookings_get_response):
aioresponses.get(
connector.user_bookings_url, status=200, body=user_bookings_get_response
)
def set_bookings_response(aioresponses, connector, user_bookings_post_response):
aioresponses.post(
connector.user_bookings_url, status=200, payload=user_bookings_post_response
)
def set_full_user_bookings_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
set_ongoing_bookings_response(aioresponses, connector, *responses[2:])
def set_cancellation_response(aioresponses, connector, response):
aioresponses.post(connector.booking_cancellation_url, status=200, payload=response)
def set_full_cancellation_by_id_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
set_hash_response(aioresponses, connector, responses[2])
set_cancellation_response(aioresponses, connector, responses[3])
def set_full_cancellation_responses(aioresponses, connector, responses):
make_landing_request_success(aioresponses, connector, responses[0])
make_login_request_success(aioresponses, connector, responses[1])
# the request to get the hash is made twice
set_hash_response(aioresponses, connector, responses[2])
set_hash_response(aioresponses, connector, responses[2])
set_bookings_response(aioresponses, connector, responses[3])
set_cancellation_response(aioresponses, connector, responses[4])
def test_urls(connector, club): def test_urls(connector, club):
@ -134,7 +51,7 @@ def test_urls_payload_templates(mock_resources, club):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_landing_page(aioresponses, connector, landing_response): async def test_landing_page(aioresponses, connector, landing_response):
make_landing_request_success(aioresponses, connector, landing_response) responses.make_landing_request_success(aioresponses, connector, landing_response)
async with ClientSession() as session: async with ClientSession() as session:
response = await connector.land(session) response = await connector.land(session)
@ -146,7 +63,9 @@ async def test_landing_page(aioresponses, connector, landing_response):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login_success(aioresponses, connector, user, login_success_response): async def test_login_success(aioresponses, connector, user, login_success_response):
make_login_request_success(aioresponses, connector, login_success_response) responses.make_login_request_success(
aioresponses, connector, login_success_response
)
async with ClientSession() as session: async with ClientSession() as session:
response = await connector.login(session, user) response = await connector.login(session, user)
@ -160,7 +79,7 @@ async def test_login_success(aioresponses, connector, user, login_success_respon
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login_failure(aioresponses, connector, user, login_failure_response): async def test_login_failure(aioresponses, connector, user, login_failure_response):
make_login_request_fail(aioresponses, connector, login_failure_response) responses.make_login_request_fail(aioresponses, connector, login_failure_response)
async with ClientSession() as session: async with ClientSession() as session:
response = await connector.login(session, user) response = await connector.login(session, user)
@ -170,38 +89,9 @@ async def test_login_failure(aioresponses, connector, user, login_failure_respon
assert await response.json() == login_failure_response assert await response.json() == login_failure_response
@pytest.mark.asyncio def test_get_booked_court(connector, booked_courts_response):
async def test_booking_success( booked_court = connector.get_booked_court(booked_courts_response, "Sport1")
aioresponses, assert booked_court.number == 3
connector,
user,
booking_filter,
booking_success_from_start,
):
set_full_booking_requests_responses(
aioresponses, connector, booking_success_from_start
)
court_booked = await connector.book(user, booking_filter)
assert court_booked.id == 2
@pytest.mark.asyncio
async def test_booking_failure(
aioresponses,
connector,
user,
booking_filter,
booking_failure_from_start,
):
set_full_booking_requests_responses(
aioresponses, connector, booking_failure_from_start
)
court_booked = await connector.book(user, booking_filter)
assert court_booked is None
@pytest.mark.asyncio @pytest.mark.asyncio
@ -212,7 +102,7 @@ async def test_get_ongoing_bookings(
user_bookings_get_response, user_bookings_get_response,
user_bookings_list, user_bookings_list,
): ):
set_ongoing_bookings_response( responses.set_ongoing_bookings_response(
aioresponses, connector, user_bookings_get_response, user_bookings_list aioresponses, connector, user_bookings_get_response, user_bookings_list
) )
@ -222,81 +112,19 @@ async def test_get_ongoing_bookings(
assert len(bookings) == 2 assert len(bookings) == 2
@pytest.mark.asyncio
async def test_get_ongoing_bookings(
aioresponses,
connector,
user,
user_bookings_get_response,
user_bookings_list,
):
set_ongoing_bookings_response(
aioresponses, connector, user_bookings_get_response, user_bookings_list
)
async with ClientSession() as session:
bookings = await connector.get_ongoing_bookings(session)
assert len(bookings) == 2
@pytest.mark.asyncio
async def test_has_user_ongoing_bookings(
aioresponses,
connector,
user,
user_has_ongoing_bookings_from_start,
):
set_full_user_bookings_responses(
aioresponses, connector, user_has_ongoing_bookings_from_start
)
has_bookings = await connector.has_user_ongoing_booking(user)
assert has_bookings
@pytest.mark.asyncio
async def test_has_user_ongoing_bookings(
aioresponses,
connector,
user,
user_has_no_ongoing_bookings_from_start,
):
set_full_user_bookings_responses(
aioresponses, connector, user_has_no_ongoing_bookings_from_start
)
has_bookings = await connector.has_user_ongoing_booking(user)
assert not has_bookings
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancellation_request( async def test_cancellation_request(
aioresponses, connector, user_bookings_get_response, cancellation_response aioresponses, connector, user_bookings_get_response, cancellation_response
): ):
set_hash_response(aioresponses, connector, user_bookings_get_response) responses.set_hash_response(aioresponses, connector, user_bookings_get_response)
set_cancellation_response(aioresponses, connector, cancellation_response) responses.set_cancellation_response(aioresponses, connector, cancellation_response)
async with ClientSession() as session: async with ClientSession() as session:
response = await connector.send_cancellation_request(session, 123) response = await connector.cancel_booking_id(session, 123)
assert await response.json() == cancellation_response assert await response.json() == cancellation_response
@pytest.mark.asyncio
async def test_cancel_booking_id(
aioresponses, connector, user, cancellation_by_id_from_start
):
set_full_cancellation_by_id_responses(
aioresponses, connector, cancellation_by_id_from_start
)
response = await connector.cancel_booking_id(user, 132)
assert await response.json() == cancellation_by_id_from_start[3]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cancel_booking_success( async def test_cancel_booking_success(
aioresponses, aioresponses,
@ -305,10 +133,13 @@ async def test_cancel_booking_success(
cancellation_success_booking_filter, cancellation_success_booking_filter,
cancellation_success_from_start, cancellation_success_from_start,
): ):
set_full_cancellation_responses( responses.set_full_cancellation_responses(
aioresponses, connector, cancellation_success_from_start aioresponses, connector, cancellation_success_from_start
) )
response = await connector.cancel_booking(user, cancellation_success_booking_filter) async with ClientSession() as session:
response = await connector.cancel_booking(
session, cancellation_success_booking_filter
)
assert await response.json() == cancellation_success_from_start[4] assert await response.json() == cancellation_success_from_start[4]

View file

@ -0,0 +1,110 @@
import pytest
from gestion_sports_services import GestionSportsServices
from tests.unit_tests import responses
@pytest.mark.asyncio
async def test_booking_success(
aioresponses,
connector,
club,
user,
booking_filter,
booking_success_from_start,
):
responses.set_full_booking_requests_responses(
aioresponses, connector, booking_success_from_start
)
court_booked = await GestionSportsServices.book(club, user, booking_filter)
assert court_booked.id == 2
@pytest.mark.asyncio
async def test_booking_failure(
aioresponses,
gs_services,
connector,
club,
user,
booking_filter,
booking_failure_from_start,
):
responses.set_full_booking_requests_responses(
aioresponses, connector, booking_failure_from_start
)
court_booked = await gs_services.book(club, user, booking_filter)
assert court_booked is None
@pytest.mark.asyncio
async def test_user_has_available_booking_slots(
aioresponses,
gs_services,
connector,
user,
club,
user_has_ongoing_bookings_from_start,
):
responses.set_full_user_bookings_responses(
aioresponses, connector, user_has_ongoing_bookings_from_start
)
has_user_available_slots = await gs_services.has_user_available_slots(user, club)
assert has_user_available_slots
@pytest.mark.asyncio
async def test_user_has_no_available_booking_slots(
aioresponses,
gs_services,
connector,
user,
club,
user_has_no_ongoing_bookings_from_start,
):
responses.set_full_user_bookings_responses(
aioresponses, connector, user_has_no_ongoing_bookings_from_start
)
has_user_available_slots = await gs_services.has_user_available_slots(user, club)
assert not has_user_available_slots
@pytest.mark.asyncio
async def test_cancel_booking(
aioresponses,
gs_services,
connector,
user,
club,
booking_filter,
cancellation_success_from_start,
):
responses.set_full_cancellation_responses(
aioresponses, connector, cancellation_success_from_start
)
await gs_services.cancel_booking(user, club, booking_filter)
@pytest.mark.asyncio
async def test_cancel_booking_id(
aioresponses,
gs_services,
connector,
user,
club,
cancellation_success_from_start,
):
responses.set_full_cancellation_responses(
aioresponses, connector, cancellation_success_from_start
)
await gs_services.cancel_booking_id(user, club, 65464)