feat: New script for booking test function in shell and python
This commit is contained in:
@@ -304,7 +304,7 @@ class CrossFitBooker:
|
|||||||
for session_type, s in found_preferred_sessions:
|
for session_type, s in found_preferred_sessions:
|
||||||
st_dt = self._parse_local(s["start_timestamp"])
|
st_dt = self._parse_local(s["start_timestamp"])
|
||||||
logging.info(f"Attempting to book {session_type} session at {st_dt} ({s['name_activity']})")
|
logging.info(f"Attempting to book {session_type} session at {st_dt} ({s['name_activity']})")
|
||||||
if await self.book_session(s["id_activity_calendar"]):
|
if self.book_session(s["id_activity_calendar"]):
|
||||||
details = f"{s['name_activity']} at {st_dt.strftime('%Y-%m-%d %H:%M')}"
|
details = f"{s['name_activity']} at {st_dt.strftime('%Y-%m-%d %H:%M')}"
|
||||||
await self.notifier.notify_session_booking(details)
|
await self.notifier.notify_session_booking(details)
|
||||||
logging.info(f"Successfully booked {session_type} session at {st_dt}")
|
logging.info(f"Successfully booked {session_type} session at {st_dt}")
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
#!/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 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_token is None
|
|
||||||
assert booker.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_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.user_id == "12345"
|
|
||||||
assert booker.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.user_id is None
|
|
||||||
assert booker.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_token = "test_token"
|
|
||||||
booker.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_token = "test_token"
|
|
||||||
booker.user_id = "12345"
|
|
||||||
|
|
||||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
|
||||||
|
|
||||||
assert result is None
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Unit tests for CrossFitBooker initialization
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Add the parent directory to the path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pytest.main([__file__, "-v"])
|
|
||||||
@@ -190,18 +190,18 @@ class TestCrossFitBookerIsSessionBookable:
|
|||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
class TestCrossFitBookerRunBookingCycle:
|
class TestCrossFitBookerExcuteCycle:
|
||||||
"""Test cases for run_booking_cycle method"""
|
"""Test cases for execute_cycle method"""
|
||||||
|
|
||||||
@patch('crossfit_booker.CrossFitBooker.get_available_sessions')
|
@patch('crossfit_booker.CrossFitBooker.get_available_sessions')
|
||||||
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
||||||
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
||||||
@patch('crossfit_booker.CrossFitBooker.book_session')
|
@patch('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):
|
async def test_execute_cycle_no_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
||||||
"""Test run_booking_cycle with no available sessions"""
|
"""Test execute_cycle with no available sessions"""
|
||||||
mock_get_sessions.return_value = {"success": False}
|
mock_get_sessions.return_value = {"success": False}
|
||||||
booker = CrossFitBooker()
|
booker = CrossFitBooker()
|
||||||
await booker.run_booking_cycle(datetime.now(pytz.timezone("Europe/Paris")))
|
await booker.execute_cycle(datetime.now(pytz.timezone("Europe/Paris")))
|
||||||
mock_get_sessions.assert_called_once()
|
mock_get_sessions.assert_called_once()
|
||||||
mock_book_session.assert_not_called()
|
mock_book_session.assert_not_called()
|
||||||
|
|
||||||
@@ -209,8 +209,8 @@ class TestCrossFitBookerRunBookingCycle:
|
|||||||
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
@patch('crossfit_booker.CrossFitBooker.is_session_bookable')
|
||||||
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
@patch('crossfit_booker.CrossFitBooker.matches_preferred_session')
|
||||||
@patch('crossfit_booker.CrossFitBooker.book_session')
|
@patch('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):
|
async def test_execute_cycle_with_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
||||||
"""Test run_booking_cycle with available sessions"""
|
"""Test execute_cycle with available sessions"""
|
||||||
# Use current date for the session to ensure it falls within 0-2 day window
|
# Use current date for the session to ensure it falls within 0-2 day window
|
||||||
current_time = datetime.now(pytz.timezone("Europe/Paris"))
|
current_time = datetime.now(pytz.timezone("Europe/Paris"))
|
||||||
session_date = current_time.date()
|
session_date = current_time.date()
|
||||||
@@ -233,7 +233,7 @@ class TestCrossFitBookerRunBookingCycle:
|
|||||||
mock_book_session.return_value = True
|
mock_book_session.return_value = True
|
||||||
|
|
||||||
booker = CrossFitBooker()
|
booker = CrossFitBooker()
|
||||||
await booker.run_booking_cycle(current_time)
|
await booker.execute_cycle(current_time)
|
||||||
|
|
||||||
mock_get_sessions.assert_called_once()
|
mock_get_sessions.assert_called_once()
|
||||||
mock_is_bookable.assert_called_once()
|
mock_is_bookable.assert_called_once()
|
||||||
@@ -245,22 +245,22 @@ class TestCrossFitBookerRun:
|
|||||||
"""Test cases for run method"""
|
"""Test cases for run method"""
|
||||||
|
|
||||||
@patch('crossfit_booker.CrossFitBooker.login')
|
@patch('crossfit_booker.CrossFitBooker.login')
|
||||||
@patch('crossfit_booker.CrossFitBooker.run_booking_cycle')
|
@patch('crossfit_booker.CrossFitBooker.execute_cycle')
|
||||||
async def test_run_auth_failure(self, mock_run_booking_cycle, mock_login):
|
async def test_run_auth_failure(self, mock_execute_cycle, mock_login):
|
||||||
"""Test run with authentication failure"""
|
"""Test run with authentication failure"""
|
||||||
mock_login.return_value = False
|
mock_login.return_value = False
|
||||||
booker = CrossFitBooker()
|
booker = CrossFitBooker()
|
||||||
with patch.object(booker, 'run', new=booker.run) as mock_run:
|
with patch.object(booker, 'run', new=booker.run) as mock_run:
|
||||||
await booker.run()
|
await booker.run()
|
||||||
mock_login.assert_called_once()
|
mock_login.assert_called_once()
|
||||||
mock_run_booking_cycle.assert_not_called()
|
mock_execute_cycle.assert_not_called()
|
||||||
|
|
||||||
@patch('crossfit_booker.CrossFitBooker.login')
|
@patch('crossfit_booker.CrossFitBooker.login')
|
||||||
@patch('crossfit_booker.CrossFitBooker.run_booking_cycle')
|
@patch('crossfit_booker.CrossFitBooker.execute_cycle')
|
||||||
@patch('crossfit_booker.CrossFitBooker.quit')
|
@patch('crossfit_booker.CrossFitBooker.quit')
|
||||||
@patch('time.sleep')
|
@patch('time.sleep')
|
||||||
@patch('datetime.datetime')
|
@patch('datetime.datetime')
|
||||||
async def test_run_booking_outside_window(self, mock_datetime, mock_sleep, mock_quit, mock_run_booking_cycle, mock_login):
|
async def test_run_booking_outside_window(self, mock_datetime, mock_sleep, mock_quit, mock_execute_cycle, mock_login):
|
||||||
"""Test run with booking outside window"""
|
"""Test run with booking outside window"""
|
||||||
mock_login.return_value = True
|
mock_login.return_value = True
|
||||||
mock_quit.return_value = None # Prevent actual exit
|
mock_quit.return_value = None # Prevent actual exit
|
||||||
@@ -292,8 +292,8 @@ class TestCrossFitBookerRun:
|
|||||||
# Verify login was called
|
# Verify login was called
|
||||||
mock_login.assert_called_once()
|
mock_login.assert_called_once()
|
||||||
|
|
||||||
# Verify run_booking_cycle was NOT called since we're outside the booking window
|
# Verify execute_cycle was NOT called since we're outside the booking window
|
||||||
mock_run_booking_cycle.assert_not_called()
|
mock_execute_cycle.assert_not_called()
|
||||||
|
|
||||||
# Verify quit was called (due to KeyboardInterrupt)
|
# Verify quit was called (due to KeyboardInterrupt)
|
||||||
mock_quit.assert_called_once()
|
mock_quit.assert_called_once()
|
||||||
|
|||||||
Reference in New Issue
Block a user