Merge branch 'refactor/booker' into develop

This commit is contained in:
kbe
2025-08-12 01:43:32 +02:00
27 changed files with 1605 additions and 1544 deletions

135
test/test_auth.py Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
Unit tests for AuthHandler class
"""
import pytest
import os
import sys
import requests
from unittest.mock import patch, Mock
# Add the parent directory to the path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.auth import AuthHandler
class TestAuthHandlerAuthHeaders:
"""Test cases for get_auth_headers method"""
def test_get_auth_headers_without_token(self):
"""Test headers without auth token"""
auth_handler = AuthHandler('test_user', 'test_pass')
headers = auth_handler.get_auth_headers()
assert "Authorization" not in headers
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
def test_get_auth_headers_with_token(self):
"""Test headers with auth token"""
auth_handler = AuthHandler('test_user', 'test_pass')
auth_handler.auth_token = "test_token_123"
headers = auth_handler.get_auth_headers()
assert headers["Authorization"] == "Bearer test_token_123"
class TestAuthHandlerLogin:
"""Test cases for login method"""
@patch('requests.Session.post')
def test_login_success(self, mock_post):
"""Test successful login flow"""
# Mock first login response
mock_response1 = Mock()
mock_response1.ok = True
mock_response1.json.return_value = {
"data": {
"user": {
"id_user": "12345"
}
}
}
# Mock second login response
mock_response2 = Mock()
mock_response2.ok = True
mock_response2.json.return_value = {
"token": "test_bearer_token"
}
mock_post.side_effect = [mock_response1, mock_response2]
auth_handler = AuthHandler('test_user', 'test_pass')
result = auth_handler.login()
assert result is True
assert auth_handler.user_id == "12345"
assert auth_handler.auth_token == "test_bearer_token"
@patch('requests.Session.post')
def test_login_first_step_failure(self, mock_post):
"""Test login failure on first step"""
mock_response = Mock()
mock_response.ok = False
mock_response.status_code = 400
mock_response.text = "Bad Request"
mock_post.return_value = mock_response
auth_handler = AuthHandler('test_user', 'test_pass')
result = auth_handler.login()
assert result is False
assert auth_handler.user_id is None
assert auth_handler.auth_token is None
@patch('requests.Session.post')
def test_login_second_step_failure(self, mock_post):
"""Test login failure on second step"""
# First response succeeds
mock_response1 = Mock()
mock_response1.ok = True
mock_response1.json.return_value = {
"data": {
"user": {
"id_user": "12345"
}
}
}
# Second response fails
mock_response2 = Mock()
mock_response2.ok = False
mock_response2.status_code = 401
mock_post.side_effect = [mock_response1, mock_response2]
auth_handler = AuthHandler('test_user', 'test_pass')
result = auth_handler.login()
assert result is False
@patch('requests.Session.post')
def test_login_json_parsing_error(self, mock_post):
"""Test login with JSON parsing error"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_post.return_value = mock_response
auth_handler = AuthHandler('test_user', 'test_pass')
result = auth_handler.login()
assert result is False
@patch('requests.Session.post')
def test_login_request_exception(self, mock_post):
"""Test login with request exception"""
mock_post.side_effect = requests.exceptions.ConnectionError("Network error")
auth_handler = AuthHandler('test_user', 'test_pass')
result = auth_handler.login()
assert result is False
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,243 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the refactored CrossFitBooker functional implementation.
"""
import sys
import os
from datetime import datetime, date, timedelta
import pytz
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
from crossfit_booker_functional import (
is_session_bookable,
matches_preferred_session,
filter_bookable_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."""
print("Testing is_session_bookable...")
# Test case 1: Session with can_join = True
session1 = {
"user_info": {
"can_join": True
}
}
current_time = datetime.now(pytz.timezone("Europe/Paris"))
assert is_session_bookable(session1, current_time, "Europe/Paris") == True
# Test case 2: Session with booking window in the past
session2 = {
"user_info": {
"unableToBookUntilDate": "01-01-2020",
"unableToBookUntilTime": "10:00"
}
}
assert is_session_bookable(session2, current_time, "Europe/Paris") == True
# Test case 3: Session with booking window in the future
session3 = {
"user_info": {
"unableToBookUntilDate": "01-01-2030",
"unableToBookUntilTime": "10:00"
}
}
assert is_session_bookable(session3, current_time, "Europe/Paris") == False
print("✓ is_session_bookable tests passed")
def test_matches_preferred_session():
"""Test the matches_preferred_session function."""
print("Testing matches_preferred_session...")
# Define some preferred sessions (day_of_week, start_time, session_name_contains)
preferred_sessions: List[Tuple[int, str, str]] = [
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX
]
# Test case 1: Exact match
session1 = {
"start_timestamp": "2025-07-30 18:30:00", # Wednesday
"name_activity": "CONDITIONING"
}
assert matches_preferred_session(session1, preferred_sessions, "Europe/Paris") == True
# Test case 2: No match
session2 = {
"start_timestamp": "2025-07-30 18:30:00", # Wednesday
"name_activity": "YOGA"
}
assert matches_preferred_session(session2, preferred_sessions, "Europe/Paris") == False
print("✓ matches_preferred_session tests passed")
def test_filter_functions():
"""Test the filter functions."""
print("Testing filter functions...")
# Define some preferred sessions
preferred_sessions: List[Tuple[int, str, str]] = [
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX
]
# Create some test sessions
current_time = datetime.now(pytz.timezone("Europe/Paris"))
sessions = [
{
"start_timestamp": "2025-07-30 18:30:00", # Wednesday
"name_activity": "CONDITIONING",
"user_info": {"can_join": True}
},
{
"start_timestamp": "2025-07-30 19:00:00", # Wednesday
"name_activity": "YOGA",
"user_info": {"can_join": True}
},
{
"start_timestamp": "2025-08-01 17:00:00", # Friday
"name_activity": "WEIGHTLIFTING",
"user_info": {"can_join": True}
}
]
# Test filter_preferred_sessions
preferred = filter_preferred_sessions(sessions, preferred_sessions, "Europe/Paris")
assert len(preferred) == 2 # CONDITIONING and WEIGHTLIFTING sessions
# Test filter_bookable_sessions
bookable = filter_bookable_sessions(sessions, current_time, preferred_sessions, "Europe/Paris")
assert len(bookable) == 2 # Both preferred sessions are bookable
print("✓ Filter function tests passed")
def test_categorize_sessions():
"""Test the categorize_sessions function."""
print("Testing categorize_sessions...")
# Define some preferred sessions
preferred_sessions: List[Tuple[int, str, str]] = [
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX
]
# Create some test sessions
current_time = datetime.now(pytz.timezone("Europe/Paris"))
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 (tomorrow relative to Wednesday)
"name_activity": "CONDITIONING",
"user_info": {"can_join": False, "unableToBookUntilDate": "01-08-2025", "unableToBookUntilTime": "10:00"}
}
]
# Test categorize_sessions
categorized = categorize_sessions(sessions, current_time, preferred_sessions, "Europe/Paris")
assert "bookable" in categorized
assert "upcoming" in categorized
assert "all_preferred" in categorized
print("✓ categorize_sessions tests passed")
def test_format_session_details():
"""Test the format_session_details function."""
print("Testing format_session_details...")
# Test case 1: Valid session
session1 = {
"start_timestamp": "2025-07-30 18:30:00",
"name_activity": "CONDITIONING"
}
formatted = format_session_details(session1, "Europe/Paris")
assert "CONDITIONING" in formatted
assert "2025-07-30 18:30" in formatted
# Test case 2: Session with missing data
session2 = {
"name_activity": "WEIGHTLIFTING"
}
formatted = format_session_details(session2, "Europe/Paris")
assert "WEIGHTLIFTING" in formatted
assert "Unknown time" in formatted
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."""
print("Running all tests for CrossFitBooker functional implementation...\n")
test_is_session_bookable()
test_matches_preferred_session()
test_filter_functions()
test_categorize_sessions()
test_format_session_details()
test_book_session()
print("\n✓ All tests passed!")
if __name__ == "__main__":
run_all_tests()

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Unit tests for CrossFitBooker authentication methods
Unit tests for CrossFitBooker authentication methods using AuthHandler
"""
import pytest
@@ -12,12 +12,12 @@ from unittest.mock import patch, Mock
# 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.auth import AuthHandler
class TestCrossFitBookerAuthHeaders:
"""Test cases for get_auth_headers method"""
def test_get_auth_headers_without_token(self):
"""Test headers without auth token"""
with patch.dict(os.environ, {
@@ -28,7 +28,7 @@ class TestCrossFitBookerAuthHeaders:
headers = booker.get_auth_headers()
assert "Authorization" not in headers
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
def test_get_auth_headers_with_token(self):
"""Test headers with auth token"""
with patch.dict(os.environ, {
@@ -36,14 +36,13 @@ class TestCrossFitBookerAuthHeaders:
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
booker.auth_token = "test_token_123"
booker.auth_handler.auth_token = "test_token_123"
headers = booker.get_auth_headers()
assert headers["Authorization"] == "Bearer test_token_123"
class TestCrossFitBookerLogin:
"""Test cases for login method"""
@patch('requests.Session.post')
def test_login_success(self, mock_post):
"""Test successful login flow"""
@@ -57,27 +56,27 @@ class TestCrossFitBookerLogin:
}
}
}
# Mock second login response
mock_response2 = Mock()
mock_response2.ok = True
mock_response2.json.return_value = {
"token": "test_bearer_token"
}
mock_post.side_effect = [mock_response1, mock_response2]
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is True
assert booker.user_id == "12345"
assert booker.auth_token == "test_bearer_token"
assert booker.auth_handler.user_id == "12345"
assert booker.auth_handler.auth_token == "test_bearer_token"
@patch('requests.Session.post')
def test_login_first_step_failure(self, mock_post):
"""Test login failure on first step"""
@@ -85,20 +84,20 @@ class TestCrossFitBookerLogin:
mock_response.ok = False
mock_response.status_code = 400
mock_response.text = "Bad Request"
mock_post.return_value = mock_response
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
assert booker.user_id is None
assert booker.auth_token is None
assert booker.auth_handler.user_id is None
assert booker.auth_handler.auth_token is None
@patch('requests.Session.post')
def test_login_second_step_failure(self, mock_post):
"""Test login failure on second step"""
@@ -112,55 +111,54 @@ class TestCrossFitBookerLogin:
}
}
}
# Second response fails
mock_response2 = Mock()
mock_response2.ok = False
mock_response2.status_code = 401
mock_post.side_effect = [mock_response1, mock_response2]
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
@patch('requests.Session.post')
def test_login_json_parsing_error(self, mock_post):
"""Test login with JSON parsing error"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_post.return_value = mock_response
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
@patch('requests.Session.post')
def test_login_request_exception(self, mock_post):
"""Test login with request exception"""
mock_post.side_effect = requests.exceptions.ConnectionError("Network error")
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
assert result is False
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""
Comprehensive unit tests for the CrossFitBooker class in crossfit_booker.py
"""
import os
import sys
from unittest.mock import Mock, patch
from datetime import date
import requests
# 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__))))
from src.crossfit_booker import CrossFitBooker
class TestCrossFitBookerInit:
"""Test cases for CrossFitBooker initialization"""
def test_init_success(self):
"""Test successful initialization with all required env vars"""
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass',
'EMAIL_FROM': 'from@test.com',
'EMAIL_TO': 'to@test.com',
'EMAIL_PASSWORD': 'email_pass',
'TELEGRAM_TOKEN': 'telegram_token',
'TELEGRAM_CHAT_ID': '12345'
}):
booker = CrossFitBooker()
assert booker.auth_handler.auth_token is None
assert booker.auth_handler.user_id is None
assert booker.session is not None
assert booker.notifier is not None
def test_init_missing_credentials(self):
"""Test initialization fails with missing credentials"""
with patch.dict(os.environ, {}, clear=True):
try:
CrossFitBooker()
except ValueError as e:
assert str(e) == "Missing environment variables"
def test_init_partial_credentials(self):
"""Test initialization fails with partial credentials"""
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user'
# Missing PASSWORD
}, clear=True):
try:
CrossFitBooker()
except ValueError as e:
assert str(e) == "Missing environment variables"
class TestCrossFitBookerAuthHeaders:
"""Test cases for get_auth_headers method"""
def test_get_auth_headers_without_token(self):
"""Test headers without auth token"""
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
headers = booker.get_auth_headers()
assert "Authorization" not in headers
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
def test_get_auth_headers_with_token(self):
"""Test headers with auth token"""
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
booker.auth_handler.auth_token = "test_token_123"
headers = booker.get_auth_headers()
assert headers["Authorization"] == "Bearer test_token_123"
class TestCrossFitBookerLogin:
"""Test cases for login method"""
@patch('requests.Session.post')
def test_login_success(self, mock_post):
"""Test successful login flow"""
# Mock first login response
mock_response1 = Mock()
mock_response1.ok = True
mock_response1.json.return_value = {
"data": {
"user": {
"id_user": "12345"
}
}
}
# Mock second login response
mock_response2 = Mock()
mock_response2.ok = True
mock_response2.json.return_value = {
"token": "test_bearer_token"
}
mock_post.side_effect = [mock_response1, mock_response2]
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is True
assert booker.auth_handler.user_id == "12345"
assert booker.auth_handler.auth_token == "test_bearer_token"
@patch('requests.Session.post')
def test_login_first_step_failure(self, mock_post):
"""Test login failure on first step"""
mock_response = Mock()
mock_response.ok = False
mock_response.status_code = 400
mock_response.text = "Bad Request"
mock_post.return_value = mock_response
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
assert booker.auth_handler.user_id is None
assert booker.auth_handler.auth_token is None
@patch('requests.Session.post')
def test_login_json_parsing_error(self, mock_post):
"""Test login with JSON parsing error"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_post.return_value = mock_response
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
@patch('requests.Session.post')
def test_login_request_exception(self, mock_post):
"""Test login with request exception"""
mock_post.side_effect = requests.exceptions.ConnectionError("Network error")
with patch.dict(os.environ, {
'CROSSFIT_USERNAME': 'test_user',
'CROSSFIT_PASSWORD': 'test_pass'
}):
booker = CrossFitBooker()
result = booker.login()
assert result is False
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))
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'
}):
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 not None
assert result["success"] is True
@patch('requests.Session.post')
def test_get_available_sessions_failure(self, mock_post):
"""Test get_available_sessions with API failure"""
mock_response = Mock()
mock_response.status_code = 400
mock_response.text = "Bad Request"
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

View File

@@ -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

View File

@@ -12,7 +12,7 @@ from unittest.mock import patch, mock_open
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from session_config import SessionConfig
from src.session_config import SessionConfig
class TestSessionConfig:

View File

@@ -12,7 +12,7 @@ from unittest.mock import patch, MagicMock
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from session_notifier import SessionNotifier
from src.session_notifier import SessionNotifier
@pytest.fixture
def email_credentials():