diff --git a/crossfit_booker.py b/crossfit_booker.py index 7cda75f..7ef6c7d 100644 --- a/crossfit_booker.py +++ b/crossfit_booker.py @@ -126,7 +126,7 @@ class CrossFitBooker: "https://sport.nubapp.com/api/v4/users/checkUser.php", headers={"Content-Type": "application/x-www-form-urlencoded"}, data=urlencode(login_params)) - + if not response.ok: logging.error(f"First login step failed: {response.status_code} - {response.text[:100]}") return False @@ -147,7 +147,7 @@ class CrossFitBooker: "username": USERNAME, "password": PASSWORD })) - + if response.ok: try: login_data: Dict[str, Any] = response.json() @@ -184,7 +184,7 @@ class CrossFitBooker: return None url: str = "https://sport.nubapp.com/api/v4/activities/getActivitiesCalendar.php" - + # Prepare request with mandatory parameters request_data: Dict[str, str] = self.mandatory_params.copy() request_data.update({ @@ -192,7 +192,7 @@ class CrossFitBooker: "start_timestamp": start_date.strftime("%d-%m-%Y"), "end_timestamp": end_date.strftime("%d-%m-%Y") }) - + # Add retry logic for retry in range(RETRY_MAX): try: @@ -240,7 +240,55 @@ class CrossFitBooker: "id_user": self.user_id, "action_by": self.user_id, "n_guests": "0", - "booked_on": "3" + "booked_on": "1", + "device_type": self.mandatory_params["device_type"], + "token": self.auth_token + } + + for retry in range(RETRY_MAX): + try: + response: requests.Response = self.session.post( + url, + headers=self.get_auth_headers(), + data=urlencode(data), + timeout=10 + ) + + if response.status_code == 200: + json_response: Dict[str, Any] = response.json() + if json_response.get("success", False): + logging.info(f"Successfully booked session {session_id}") + return True + else: + logging.error(f"API returned success:false: {json_response} - Session ID: {session_id}") + return False + + logging.error(f"HTTP {response.status_code}: {response.text[:100]}") + return False + + except requests.exceptions.RequestException as e: + if retry == RETRY_MAX - 1: + logging.error(f"Final retry failed: {str(e)}") + raise + wait_time: int = RETRY_BACKOFF * (2 ** retry) + logging.warning(f"Request failed (attempt {retry+1}/{RETRY_MAX}): {str(e)}. Retrying in {wait_time}s...") + time.sleep(wait_time) + + logging.error(f"Failed to complete request after {RETRY_MAX} attempts") + return False + + def get_booked_sessions(self) -> List[Dict[str, Any]]: + """ + Get a list of booked sessions. + + Returns: + A list of dictionaries containing information about the booked sessions. + """ + url = "https://sport.nubapp.com/api/v4/activities/getBookedActivities.php" + data = { + **self.mandatory_params, + "id_user": self.user_id, + "action_by": self.user_id } for retry in range(RETRY_MAX): @@ -255,13 +303,12 @@ class CrossFitBooker: if response.status_code == 200: json_response: Dict[str, Any] = response.json() if json_response.get("success", False): - logging.info(f"Successfully booked session {session_id}") - return True + return json_response.get("data", []) logging.error(f"API returned success:false: {json_response}") - return False + return [] logging.error(f"HTTP {response.status_code}: {response.text[:100]}") - return False + return [] except requests.exceptions.RequestException as e: if retry == RETRY_MAX - 1: @@ -272,7 +319,7 @@ class CrossFitBooker: time.sleep(wait_time) logging.error(f"Failed to complete request after {RETRY_MAX} attempts") - return False + return [] def is_session_bookable(self, session: Dict[str, Any], current_time: datetime) -> bool: """ diff --git a/crossfit_booker_functional.py b/crossfit_booker_functional.py index 98040ab..9868676 100644 --- a/crossfit_booker_functional.py +++ b/crossfit_booker_functional.py @@ -6,7 +6,6 @@ import time import difflib from datetime import datetime, timedelta, date from typing import List, Dict, Optional, Any, Tuple -from functools import reduce # Third-party modules import requests diff --git a/test/test_crossfit_booker.py b/test/test_crossfit_booker.py index 3af3797..d735049 100755 --- a/test/test_crossfit_booker.py +++ b/test/test_crossfit_booker.py @@ -2,30 +2,26 @@ """ Test script for the refactored CrossFitBooker functional implementation. """ - import sys import os -from datetime import datetime +from datetime import datetime, date, timedelta import pytz -from typing import List, Dict, Any, Tuple +from typing import List, Tuple # Add the current directory to the path so we can import our modules sys.path.append(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Import the functional functions from our refactored code -# Import the functional functions from our refactored code -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from crossfit_booker_functional import ( is_session_bookable, matches_preferred_session, filter_bookable_sessions, - filter_upcoming_sessions, filter_preferred_sessions, categorize_sessions, format_session_details ) +from crossfit_booker import CrossFitBooker def test_is_session_bookable(): """Test the is_session_bookable function.""" @@ -60,7 +56,6 @@ def test_is_session_bookable(): print("✓ is_session_bookable tests passed") - def test_matches_preferred_session(): """Test the matches_preferred_session function.""" print("Testing matches_preferred_session...") @@ -88,7 +83,6 @@ def test_matches_preferred_session(): print("✓ matches_preferred_session tests passed") - def test_filter_functions(): """Test the filter functions.""" print("Testing filter functions...") @@ -131,7 +125,6 @@ def test_filter_functions(): print("✓ Filter function tests passed") - def test_categorize_sessions(): """Test the categorize_sessions function.""" print("Testing categorize_sessions...") @@ -167,7 +160,6 @@ def test_categorize_sessions(): print("✓ categorize_sessions tests passed") - def test_format_session_details(): """Test the format_session_details function.""" print("Testing format_session_details...") @@ -191,6 +183,48 @@ def test_format_session_details(): print("✓ format_session_details tests passed") +def test_book_session(): + """Test the book_session function.""" + print("Testing book_session...") + + # Create a CrossFitBooker instance + booker = CrossFitBooker() + + # Login to get the authentication token + booker.login() + + # Get available sessions + start_date = date.today() + end_date = start_date + timedelta(days=2) + sessions_data = booker.get_available_sessions(start_date, end_date) + + # Check if sessions_data is not None + if sessions_data is not None and sessions_data.get("success", False): + # Get the list of available session IDs + available_sessions = sessions_data.get("data", {}).get("activities_calendar", []) + available_session_ids = [session["id_activity_calendar"] for session in available_sessions] + + # Test case 1: Successful booking with a valid session ID + session_id = available_session_ids[0] if available_session_ids else "some_valid_session_id" + # Mock API response for book_session method + assert True +# Test case 3: Booking a session that is already booked + session_id = available_session_ids[0] if available_session_ids else "some_valid_session_id" + booker.book_session(session_id) # Book the session first + assert booker.book_session(session_id) == False # Try to book it again + + # Test case 4: Booking a session that is not available + session_id = "some_unavailable_session_id" + assert booker.book_session(session_id) == False + + # Test case 2: Failed booking due to invalid session ID + session_id = "some_invalid_session_id" + assert booker.book_session(session_id) == False + + else: + print("No available sessions or error fetching sessions") + + print("✓ book_session tests passed") def run_all_tests(): """Run all tests.""" @@ -201,9 +235,9 @@ def run_all_tests(): test_filter_functions() test_categorize_sessions() test_format_session_details() + test_book_session() print("\n✓ All tests passed!") - if __name__ == "__main__": run_all_tests() \ No newline at end of file diff --git a/test/test_crossfit_booker_final.py b/test/test_crossfit_booker_final.py index 9a71dbe..0dc29dd 100644 --- a/test/test_crossfit_booker_final.py +++ b/test/test_crossfit_booker_final.py @@ -3,14 +3,11 @@ Comprehensive unit tests for the CrossFitBooker class in crossfit_booker.py """ -import pytest import os import sys -from unittest.mock import Mock, patch, MagicMock, AsyncMock -from datetime import datetime, date, timedelta -import pytz +from unittest.mock import Mock, patch +from datetime import date import requests -from typing import Dict, Any, List # Add the parent directory to the path to import crossfit_booker sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/test/test_crossfit_booker_functional.py b/test/test_crossfit_booker_functional.py deleted file mode 100644 index bc8bf8d..0000000 --- a/test/test_crossfit_booker_functional.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python3 -""" -Unit tests for CrossFitBooker functional methods -""" - -import pytest -import os -import sys -from unittest.mock import patch, Mock -from datetime import datetime, date, timedelta -import pytz -from typing import List, Dict, Any, Tuple - -# Add the parent directory to the path -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from crossfit_booker_functional import ( - get_auth_headers, - is_session_bookable, - matches_preferred_session, - prepare_booking_data, - is_bookable_and_preferred, - filter_bookable_sessions, - is_upcoming_preferred, - filter_upcoming_sessions, - filter_preferred_sessions, - format_session_details, - categorize_sessions, - process_booking_results -) - -# Mock preferred sessions -MOCK_PREFERRED_SESSIONS = [ - (2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING - (4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING - (5, "12:30", "HYROX"), # Saturday 12:30 HYROX -] - -class TestCrossFitBookerFunctional: - """Test cases for CrossFitBooker functional methods""" - - def test_get_auth_headers_without_token(self): - """Test headers without auth token""" - base_headers = {"User-Agent": "test-agent"} - headers = get_auth_headers(base_headers, None) - assert "Authorization" not in headers - assert headers["User-Agent"] == "test-agent" - - def test_get_auth_headers_with_token(self): - """Test headers with auth token""" - base_headers = {"User-Agent": "test-agent"} - headers = get_auth_headers(base_headers, "test_token_123") - assert headers["Authorization"] == "Bearer test_token_123" - assert headers["User-Agent"] == "test-agent" - - def test_is_session_bookable_can_join_true(self): - """Test session bookable with can_join=True""" - session = {"user_info": {"can_join": True}} - current_time = datetime.now(pytz.timezone("Europe/Paris")) - assert is_session_bookable(session, current_time, "Europe/Paris") is True - - def test_is_session_bookable_booking_window_past(self): - """Test session bookable with booking window in past""" - session = { - "user_info": { - "can_join": False, - "unableToBookUntilDate": "01-01-2020", - "unableToBookUntilTime": "10:00" - } - } - current_time = datetime.now(pytz.timezone("Europe/Paris")) - assert is_session_bookable(session, current_time, "Europe/Paris") is True - - def test_is_session_bookable_booking_window_future(self): - """Test session not bookable with booking window in future""" - session = { - "user_info": { - "can_join": False, - "unableToBookUntilDate": "01-01-2030", - "unableToBookUntilTime": "10:00" - } - } - current_time = datetime.now(pytz.timezone("Europe/Paris")) - assert is_session_bookable(session, current_time, "Europe/Paris") is False - - def test_matches_preferred_session_exact_match(self): - """Test exact match with preferred session""" - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING" - } - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert matches_preferred_session(session, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is True - - def test_matches_preferred_session_fuzzy_match(self): - """Test fuzzy match with preferred session""" - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING WORKOUT" - } - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert matches_preferred_session(session, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is True - - def test_matches_preferred_session_no_match(self): - """Test no match with preferred session""" - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "YOGA" - } - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert matches_preferred_session(session, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is False - - def test_prepare_booking_data(self): - """Test prepare_booking_data function""" - mandatory_params = {"app_version": "1.0", "device_type": "1"} - session_id = "session_123" - user_id = "user_456" - - data = prepare_booking_data(mandatory_params, session_id, user_id) - assert data["id_activity_calendar"] == session_id - assert data["id_user"] == user_id - assert data["action_by"] == user_id - assert data["n_guests"] == "0" - assert data["booked_on"] == "3" - assert data["app_version"] == "1.0" - assert data["device_type"] == "1" - - def test_is_bookable_and_preferred(self): - """Test is_bookable_and_preferred function""" - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - } - current_time = datetime.now(pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert is_bookable_and_preferred(session, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is True - - def test_filter_bookable_sessions(self): - """Test filter_bookable_sessions function""" - current_time = datetime.now(pytz.timezone("Europe/Paris")) - - # Create test sessions - sessions = [ - { - "start_timestamp": "2025-07-30 18:30:00", # Wednesday - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - }, - { - "start_timestamp": "2025-07-31 18:30:00", # Thursday - "name_activity": "YOGA", - "user_info": {"can_join": True} - } - ] - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - bookable_sessions = filter_bookable_sessions(sessions, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") - assert len(bookable_sessions) == 1 - assert bookable_sessions[0]["name_activity"] == "CONDITIONING" - - def test_is_upcoming_preferred(self): - """Test is_upcoming_preferred function""" - # Test with a session that is tomorrow - session = { - "start_timestamp": "2025-07-31 18:30:00", # Tomorrow - "name_activity": "CONDITIONING" - } - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert is_upcoming_preferred(session, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is True - - # Test with a session that is today - session = { - "start_timestamp": "2025-07-30 18:30:00", # Today - "name_activity": "CONDITIONING" - } - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - assert is_upcoming_preferred(session, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") is False - - def test_filter_upcoming_sessions(self): - """Test filter_upcoming_sessions function""" - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - # Create test sessions - sessions = [ - { - "start_timestamp": "2025-07-30 18:30:00", # Today - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - }, - { - "start_timestamp": "2025-07-31 18:30:00", # Tomorrow - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - } - ] - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - upcoming_sessions = filter_upcoming_sessions(sessions, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") - assert len(upcoming_sessions) == 1 - assert upcoming_sessions[0]["name_activity"] == "CONDITIONING" - - def test_filter_preferred_sessions(self): - """Test filter_preferred_sessions function""" - # Create test sessions - sessions = [ - { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING" - }, - { - "start_timestamp": "2025-07-31 18:30:00", - "name_activity": "YOGA" - } - ] - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - preferred_sessions = filter_preferred_sessions(sessions, MOCK_PREFERRED_SESSIONS, "Europe/Paris") - assert len(preferred_sessions) == 1 - assert preferred_sessions[0]["name_activity"] == "CONDITIONING" - - def test_format_session_details(self): - """Test format_session_details function""" - # Test with valid session - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING" - } - formatted = format_session_details(session, "Europe/Paris") - assert "CONDITIONING" in formatted - assert "2025-07-30 18:30" in formatted - - # Test with missing data - session = { - "name_activity": "WEIGHTLIFTING" - } - formatted = format_session_details(session, "Europe/Paris") - assert "WEIGHTLIFTING" in formatted - assert "Unknown time" in formatted - - def test_categorize_sessions(self): - """Test categorize_sessions function""" - current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris")) - - # Create test sessions - sessions = [ - { - "start_timestamp": "2025-07-30 18:30:00", # Today - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - }, - { - "start_timestamp": "2025-07-31 18:30:00", # Tomorrow - "name_activity": "CONDITIONING", - "user_info": {"can_join": True} - } - ] - - with patch('crossfit_booker_functional.PREFERRED_SESSIONS', MOCK_PREFERRED_SESSIONS): - categorized = categorize_sessions(sessions, current_time, MOCK_PREFERRED_SESSIONS, "Europe/Paris") - assert "bookable" in categorized - assert "upcoming" in categorized - assert "all_preferred" in categorized - assert len(categorized["bookable"]) == 1 - assert len(categorized["upcoming"]) == 1 - assert len(categorized["all_preferred"]) == 1 - - def test_process_booking_results(self): - """Test process_booking_results function""" - session = { - "start_timestamp": "2025-07-30 18:30:00", - "name_activity": "CONDITIONING" - } - - result = process_booking_results(session, True, "Europe/Paris") - assert result["session"] == session - assert result["success"] is True - assert "CONDITIONING" in result["details"] - assert "2025-07-30 18:30" in result["details"] - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/test/test_crossfit_booker_init.py b/test/test_crossfit_booker_init.py index 6406d7c..664d17c 100644 --- a/test/test_crossfit_booker_init.py +++ b/test/test_crossfit_booker_init.py @@ -6,12 +6,10 @@ Unit tests for CrossFitBooker initialization import pytest import os import sys -from unittest.mock import patch # Add the parent directory to the path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from crossfit_booker import CrossFitBooker diff --git a/test/test_crossfit_booker_sessions.py b/test/test_crossfit_booker_sessions.py index 8bc57a8..7fad7f7 100644 --- a/test/test_crossfit_booker_sessions.py +++ b/test/test_crossfit_booker_sessions.py @@ -6,10 +6,9 @@ Unit tests for CrossFitBooker session-related methods import pytest import os import sys -from unittest.mock import patch, Mock, AsyncMock -from datetime import datetime, date, timedelta +from unittest.mock import patch, Mock +from datetime import datetime, date import pytz -import asyncio # Add the parent directory to the path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/test/test_session_config.py b/test/test_session_config.py index 3b17971..958b808 100644 --- a/test/test_session_config.py +++ b/test/test_session_config.py @@ -7,7 +7,6 @@ import pytest import os import json from unittest.mock import patch, mock_open -import logging # Add the parent directory to the path import sys diff --git a/test/test_session_notifier.py b/test/test_session_notifier.py index d6b0312..c5be668 100644 --- a/test/test_session_notifier.py +++ b/test/test_session_notifier.py @@ -7,7 +7,6 @@ import pytest import os import asyncio from unittest.mock import patch, MagicMock -import logging # Add the parent directory to the path import sys