Merge branch 'refactor/booker' into develop
This commit is contained in:
@@ -6,29 +6,33 @@ Unit tests for CrossFitBooker session-related methods
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch, Mock
|
||||
from datetime import datetime, date
|
||||
from unittest.mock import patch, Mock, AsyncMock
|
||||
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 crossfit_booker import CrossFitBooker
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
||||
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"""
|
||||
@@ -42,122 +46,126 @@ class TestCrossFitBookerGetAvailableSessions:
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
'CROSSFIT_USERNAME': 'test_user',
|
||||
'CROSSFIT_PASSWORD': 'test_pass'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
booker.auth_token = "test_token"
|
||||
booker.user_id = "12345"
|
||||
|
||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
||||
|
||||
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_token = "test_token"
|
||||
booker.user_id = "12345"
|
||||
|
||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
||||
|
||||
assert result is None
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
result = booker.book_session("session_123")
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
booker.auth_token = "test_token"
|
||||
booker.user_id = "12345"
|
||||
|
||||
result = booker.book_session("session_123")
|
||||
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
booker.auth_token = "test_token"
|
||||
booker.user_id = "12345"
|
||||
|
||||
result = booker.book_session("session_123")
|
||||
|
||||
assert result is False
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
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 = booker.is_session_bookable(session, current_time)
|
||||
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||
session_manager = SessionManager(auth_handler)
|
||||
session = {
|
||||
"user_info": {
|
||||
"can_join": False,
|
||||
@@ -166,17 +174,18 @@ class TestCrossFitBookerIsSessionBookable:
|
||||
}
|
||||
}
|
||||
current_time = datetime.now(pytz.timezone("Europe/Paris"))
|
||||
|
||||
result = booker.is_session_bookable(session, current_time)
|
||||
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||
session_manager = SessionManager(auth_handler)
|
||||
session = {
|
||||
"user_info": {
|
||||
"can_join": False,
|
||||
@@ -185,36 +194,40 @@ class TestCrossFitBookerIsSessionBookable:
|
||||
}
|
||||
}
|
||||
current_time = datetime.now(pytz.timezone("Europe/Paris"))
|
||||
|
||||
result = booker.is_session_bookable(session, current_time)
|
||||
assert result is False
|
||||
|
||||
result = session_manager.is_session_bookable(session, current_time)
|
||||
assert result is False
|
||||
|
||||
class TestCrossFitBookerExcuteCycle:
|
||||
"""Test cases for execute_cycle method"""
|
||||
|
||||
@patch('crossfit_booker.CrossFitBooker.get_available_sessions')
|
||||
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
||||
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
||||
@patch('crossfit_booker.CrossFitBooker.book_session')
|
||||
async def test_execute_cycle_no_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
||||
"""Test execute_cycle with no available sessions"""
|
||||
@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()
|
||||
await booker.execute_cycle(datetime.now(pytz.timezone("Europe/Paris")))
|
||||
# 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('crossfit_booker.CrossFitBooker.get_available_sessions')
|
||||
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
||||
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
||||
@patch('crossfit_booker.CrossFitBooker.book_session')
|
||||
async def test_execute_cycle_with_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
||||
"""Test execute_cycle with available sessions"""
|
||||
@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": {
|
||||
@@ -233,7 +246,15 @@ class TestCrossFitBookerExcuteCycle:
|
||||
mock_book_session.return_value = True
|
||||
|
||||
booker = CrossFitBooker()
|
||||
await booker.execute_cycle(current_time)
|
||||
# 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()
|
||||
@@ -244,59 +265,52 @@ class TestCrossFitBookerExcuteCycle:
|
||||
class TestCrossFitBookerRun:
|
||||
"""Test cases for run method"""
|
||||
|
||||
@patch('crossfit_booker.CrossFitBooker.login')
|
||||
@patch('crossfit_booker.CrossFitBooker.execute_cycle')
|
||||
async def test_run_auth_failure(self, mock_execute_cycle, mock_login):
|
||||
def test_run_auth_failure(self):
|
||||
"""Test run with authentication failure"""
|
||||
mock_login.return_value = False
|
||||
booker = CrossFitBooker()
|
||||
with patch.object(booker, 'run', new=booker.run) as mock_run:
|
||||
await booker.run()
|
||||
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()
|
||||
mock_execute_cycle.assert_not_called()
|
||||
|
||||
@patch('crossfit_booker.CrossFitBooker.login')
|
||||
@patch('crossfit_booker.CrossFitBooker.execute_cycle')
|
||||
@patch('crossfit_booker.CrossFitBooker.quit')
|
||||
@patch('time.sleep')
|
||||
@patch('datetime.datetime')
|
||||
async def test_run_booking_outside_window(self, mock_datetime, mock_sleep, mock_quit, mock_execute_cycle, mock_login):
|
||||
def test_run_booking_outside_window(self):
|
||||
"""Test run with booking outside window"""
|
||||
mock_login.return_value = True
|
||||
mock_quit.return_value = None # Prevent actual exit
|
||||
|
||||
# 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()
|
||||
|
||||
try:
|
||||
await booker.run()
|
||||
except KeyboardInterrupt:
|
||||
pass # Expected to break the loop
|
||||
|
||||
# Verify login was called
|
||||
mock_login.assert_called_once()
|
||||
|
||||
# Verify execute_cycle was NOT called since we're outside the booking window
|
||||
mock_execute_cycle.assert_not_called()
|
||||
|
||||
# Verify quit was called (due to KeyboardInterrupt)
|
||||
mock_quit.assert_called_once()
|
||||
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"""
|
||||
@@ -308,25 +322,27 @@ class TestCrossFitBookerQuit:
|
||||
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'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
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('crossfit_booker.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]):
|
||||
result = booker.matches_preferred_session(session, current_time)
|
||||
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):
|
||||
@@ -335,7 +351,8 @@ class TestCrossFitBookerMatchesPreferredSession:
|
||||
'CROSSFIT_USERNAME': 'test_user',
|
||||
'CROSSFIT_PASSWORD': 'test_pass'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
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"
|
||||
@@ -343,8 +360,8 @@ class TestCrossFitBookerMatchesPreferredSession:
|
||||
current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris"))
|
||||
|
||||
# Mock PREFERRED_SESSIONS
|
||||
with patch('crossfit_booker.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]):
|
||||
result = booker.matches_preferred_session(session, current_time)
|
||||
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):
|
||||
@@ -353,7 +370,8 @@ class TestCrossFitBookerMatchesPreferredSession:
|
||||
'CROSSFIT_USERNAME': 'test_user',
|
||||
'CROSSFIT_PASSWORD': 'test_pass'
|
||||
}):
|
||||
booker = CrossFitBooker()
|
||||
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||
session_manager = SessionManager(auth_handler)
|
||||
session = {
|
||||
"start_timestamp": "2025-07-30 18:30:00",
|
||||
"name_activity": "YOGA"
|
||||
@@ -361,6 +379,6 @@ class TestCrossFitBookerMatchesPreferredSession:
|
||||
current_time = datetime(2025, 7, 30, 12, 0, 0, tzinfo=pytz.timezone("Europe/Paris"))
|
||||
|
||||
# Mock PREFERRED_SESSIONS
|
||||
with patch('crossfit_booker.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]):
|
||||
result = booker.matches_preferred_session(session, current_time)
|
||||
with patch('src.session_manager.PREFERRED_SESSIONS', [(2, "18:30", "CONDITIONING")]):
|
||||
result = session_manager.matches_preferred_session(session, current_time)
|
||||
assert result is False
|
||||
Reference in New Issue
Block a user