#!/usr/bin/env python3 """ Unit tests for CrossFitBooker session-related methods """ import pytest import os import sys from unittest.mock import patch, Mock from datetime import datetime, timedelta, date import pytz # Add the parent directory to the path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src.crossfit_booker import CrossFitBooker from src.session_manager import SessionManager from src.auth import AuthHandler class TestCrossFitBookerGetAvailableSessions: """Test cases for get_available_sessions method""" def test_get_available_sessions_no_auth(self): """Test get_available_sessions without authentication""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) result = session_manager.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25)) assert result is None @patch('requests.Session.post') def test_get_available_sessions_success(self, mock_post): """Test successful get_available_sessions""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { "success": True, "data": { "activities_calendar": [ {"id": "1", "name": "Test Session"} ] } } mock_post.return_value = mock_response with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') auth_handler.auth_token = "test_token" auth_handler.user_id = "12345" session_manager = SessionManager(auth_handler) result = session_manager.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25)) assert result is not None assert result["success"] is True @patch('requests.Session.post') def test_get_available_sessions_401_error(self, mock_post): """Test get_available_sessions with 401 error""" mock_response = Mock() mock_response.status_code = 401 mock_post.return_value = mock_response with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): booker = CrossFitBooker() booker.auth_handler.auth_token = "test_token" booker.auth_handler.user_id = "12345" result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25)) assert result is None class TestCrossFitBookerBookSession: """Test cases for book_session method""" def test_book_session_no_auth(self): """Test book_session without authentication""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) result = session_manager.book_session("session_123") assert result is False @patch('requests.Session.post') def test_book_session_success(self, mock_post): """Test successful book_session""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"success": True} mock_post.return_value = mock_response with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') auth_handler.auth_token = "test_token" auth_handler.user_id = "12345" session_manager = SessionManager(auth_handler) result = session_manager.book_session("session_123") assert result is True @patch('requests.Session.post') def test_book_session_api_failure(self, mock_post): """Test book_session with API failure""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"success": False, "error": "Session full"} mock_post.return_value = mock_response with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') auth_handler.auth_token = "test_token" auth_handler.user_id = "12345" session_manager = SessionManager(auth_handler) result = session_manager.book_session("session_123") assert result is False class TestCrossFitBookerIsSessionBookable: """Test cases for is_session_bookable method""" def test_is_session_bookable_can_join_true(self): """Test session bookable with can_join=True""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) session = {"user_info": {"can_join": True}} current_time = datetime.now(pytz.timezone("Europe/Paris")) result = session_manager.is_session_bookable(session, current_time) assert result is True def test_is_session_bookable_booking_window_past(self): """Test session bookable with booking window in past""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) session = { "user_info": { "can_join": False, "unableToBookUntilDate": "01-01-2020", "unableToBookUntilTime": "10:00" } } current_time = datetime.now(pytz.timezone("Europe/Paris")) result = session_manager.is_session_bookable(session, current_time) assert result is True def test_is_session_bookable_booking_window_future(self): """Test session not bookable with booking window in future""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) session = { "user_info": { "can_join": False, "unableToBookUntilDate": "01-01-2030", "unableToBookUntilTime": "10:00" } } current_time = datetime.now(pytz.timezone("Europe/Paris")) result = session_manager.is_session_bookable(session, current_time) assert result is False class TestCrossFitBookerExcuteCycle: """Test cases for execute_cycle method""" @patch('src.crossfit_booker.CrossFitBooker.get_available_sessions') @patch('src.crossfit_booker.CrossFitBooker.is_session_bookable') @patch('src.crossfit_booker.CrossFitBooker.matches_preferred_session') @patch('src.crossfit_booker.CrossFitBooker.book_session') async def test_run_booking_cycle_no_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions): """Test run_booking_cycle with no available sessions""" mock_get_sessions.return_value = {"success": False} booker = CrossFitBooker() # Mock the auth_token and user_id to avoid authentication errors booker.auth_handler.auth_token = "test_token" booker.auth_handler.user_id = "12345" # Mock the booker method to use our mocked methods with patch.object(booker.booker, 'get_available_sessions', mock_get_sessions): await booker.run_booking_cycle(datetime.now(pytz.timezone("Europe/Paris"))) mock_get_sessions.assert_called_once() mock_book_session.assert_not_called() @patch('src.crossfit_booker.CrossFitBooker.get_available_sessions') @patch('src.crossfit_booker.CrossFitBooker.is_session_bookable') @patch('src.crossfit_booker.CrossFitBooker.matches_preferred_session') @patch('src.crossfit_booker.CrossFitBooker.book_session') async def test_run_booking_cycle_with_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions): """Test run_booking_cycle with available sessions""" # Use current date for the session to ensure it falls within 0-2 day window current_time = datetime.now(pytz.timezone("Europe/Paris")) session_date = current_time.date() mock_get_sessions.return_value = { "success": True, "data": { "activities_calendar": [ { "id_activity_calendar": "1", "name_activity": "CONDITIONING", "start_timestamp": session_date.strftime("%Y-%m-%d") + " 18:30:00", "user_info": {"can_join": True} } ] } } mock_is_bookable.return_value = True mock_matches_preferred.return_value = True mock_book_session.return_value = True booker = CrossFitBooker() # Mock the auth_token and user_id to avoid authentication errors booker.auth_handler.auth_token = "test_token" booker.auth_handler.user_id = "12345" # Mock the booker method to use our mocked methods with patch.object(booker.booker, 'get_available_sessions', mock_get_sessions): with patch.object(booker.booker, 'is_session_bookable', mock_is_bookable): with patch.object(booker.booker, 'matches_preferred_session', mock_matches_preferred): with patch.object(booker.booker, 'book_session', mock_book_session): await booker.run_booking_cycle(current_time) mock_get_sessions.assert_called_once() mock_is_bookable.assert_called_once() mock_matches_preferred.assert_called_once() mock_book_session.assert_called_once() assert mock_book_session.call_count == 1 class TestCrossFitBookerRun: """Test cases for run method""" def test_run_auth_failure(self): """Test run with authentication failure""" with patch('src.crossfit_booker.CrossFitBooker.login', return_value=False) as mock_login: booker = CrossFitBooker() # Test the authentication failure path through the booker result = booker.login() assert result is False mock_login.assert_called_once() def test_run_booking_outside_window(self): """Test run with booking outside window""" with patch('src.booker.Booker.run') as mock_run: with patch('datetime.datetime') as mock_datetime: with patch('time.sleep') as mock_sleep: # Create a time outside the booking window (19:00) tz = pytz.timezone("Europe/Paris") mock_now = datetime(2025, 7, 25, 19, 0, tzinfo=tz) mock_datetime.now.return_value = mock_now # Make sleep return immediately to allow one iteration, then break call_count = 0 def sleep_side_effect(seconds): nonlocal call_count call_count += 1 if call_count >= 1: # Break the loop after first sleep raise KeyboardInterrupt("Test complete") return None mock_sleep.side_effect = sleep_side_effect booker = CrossFitBooker() # Test the booking window logic directly target_hour, target_minute = map(int, "20:01".split(":")) target_time = datetime.now(tz).replace(hour=target_hour, minute=target_minute, second=0, microsecond=0) booking_window_end = target_time + timedelta(minutes=10) # Current time is outside the booking window assert not (target_time <= mock_now <= booking_window_end) # Run the booker to trigger the login booker.run() # Verify run was called mock_run.assert_called_once() class TestCrossFitBookerQuit: """Test cases for quit method""" def test_quit(self): """Test quit method""" booker = CrossFitBooker() with pytest.raises(SystemExit) as excinfo: booker.quit() assert excinfo.value.code == 0 class TestCrossFitBookerMatchesPreferredSession: """Test cases for matches_preferred_session method""" def test_matches_preferred_session_exact_match(self): """Test exact match with preferred session""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) 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")) # Mock PREFERRED_SESSIONS with patch('src.session_manager.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]): result = session_manager.matches_preferred_session(session, current_time) assert result is True def test_matches_preferred_session_fuzzy_match(self): """Test fuzzy match with preferred session""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) 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")) # Mock PREFERRED_SESSIONS with patch('src.session_manager.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]): result = session_manager.matches_preferred_session(session, current_time) assert result is True def test_matches_preferred_session_no_match(self): """Test no match with preferred session""" with patch.dict(os.environ, { 'CROSSFIT_USERNAME': 'test_user', 'CROSSFIT_PASSWORD': 'test_pass' }): auth_handler = AuthHandler('test_user', 'test_pass') session_manager = SessionManager(auth_handler) 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")) # Mock PREFERRED_SESSIONS with patch('src.session_manager.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]): result = session_manager.matches_preferred_session(session, current_time) assert result is False