Compare commits

...

2 Commits

Author SHA1 Message Date
Kevin Bataille
9a35a57c7f fix(auth): Update API response parsing to handle nested structure
- Fix 'id_user' key not found error by navigating the correct nested path
- Extract id_user from data.user.applications[0].users[0].id_user
- Add fallback to old structure for backward compatibility
- Prevents authentication failures due to API response format changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 15:21:21 +02:00
Kevin Bataille
cd5f0a54ac feat(notifier): Add username display and improve config error handling
- Add CROSSFIT_USERNAME from .env to all Telegram notifications
- Make session_config exit safely when config file is missing or invalid
- Remove default hardcoded sessions, return empty list instead
- Update unit tests to reflect new error handling behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 15:19:27 +02:00
3 changed files with 43 additions and 42 deletions

View File

@@ -77,7 +77,13 @@ class AuthHandler:
try:
login_data: Dict[str, Any] = response.json()
self.user_id = str(login_data["data"]["user"]["id_user"])
# Try to find id_user in the nested structure
try:
# First try the new structure: data.user.applications[0].users[0].id_user
self.user_id = str(login_data["data"]["user"]["applications"][0]["users"][0]["id_user"])
except (KeyError, IndexError, TypeError):
# Fallback to old structure if it exists
self.user_id = str(login_data["data"]["user"]["id_user"])
except (KeyError, ValueError) as e:
logging.error(f"Error during login: {str(e)} - Response: {response.text}")
return False

View File

@@ -39,6 +39,9 @@ class SessionNotifier:
# Check environment variable for impossible booking notifications
self.notify_impossible = os.environ.get("NOTIFY_IMPOSSIBLE_BOOKING", "true").lower() in ("true", "1", "yes")
# Get crossfit username from environment
self.crossfit_username = os.environ.get("CROSSFIT_USERNAME", "User")
def send_email_notification(self, message):
"""
Send an email notification with the given message.
@@ -114,8 +117,8 @@ class SessionNotifier:
session_details (str): Details about the booked session
"""
# Create messages for both email and Telegram
email_message = f"Session booked: {session_details}"
telegram_message = f"Session booked: {session_details}"
email_message = f"Session booked for {self.crossfit_username}: {session_details}"
telegram_message = f"Session booked for {self.crossfit_username}: {session_details}"
# Send notifications through enabled channels
if self.enable_email:
@@ -133,8 +136,8 @@ class SessionNotifier:
days_until (int): Number of days until the session
"""
# Create messages for both email and Telegram
email_message = f"Session available soon: {session_details} (in {days_until} days)"
telegram_message = f"Session available soon: {session_details} (in {days_until} days)"
email_message = f"Session available soon for {self.crossfit_username}: {session_details} (in {days_until} days)"
telegram_message = f"Session available soon for {self.crossfit_username}: {session_details} (in {days_until} days)"
# Send notifications through enabled channels
if self.enable_email:
@@ -164,8 +167,8 @@ class SessionNotifier:
return
# Create messages for both email and Telegram
email_message = f"Failed to book session: {session_details}"
telegram_message = f"Failed to book session: {session_details}"
email_message = f"Failed to book session for {self.crossfit_username}: {session_details}"
telegram_message = f"Failed to book session for {self.crossfit_username}: {session_details}"
# Send notifications through enabled channels
if self.enable_email:

View File

@@ -37,18 +37,17 @@ class TestSessionConfig:
"""Test behavior when the config file is not found"""
# Mock the open function to raise FileNotFoundError
with patch('builtins.open', side_effect=FileNotFoundError):
with patch('logging.warning') as mock_warning:
sessions = SessionConfig.load_preferred_sessions()
with patch('logging.error') as mock_error:
with pytest.raises(SystemExit) as excinfo:
SessionConfig.load_preferred_sessions()
# Verify warning was logged
mock_warning.assert_called_once()
assert "not found" in mock_warning.call_args[0][0]
# Verify error was logged
assert mock_error.call_count == 2
assert "not found" in mock_error.call_args_list[0][0][0]
assert "example" in mock_error.call_args_list[1][0][0]
# Verify default sessions are returned
assert len(sessions) == 3
assert sessions[0] == (2, "18:30", "CONDITIONING")
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
assert sessions[2] == (5, "12:30", "HYROX")
# Verify SystemExit was raised with exit code 1
assert excinfo.value.code == 1
def test_load_preferred_sessions_invalid_json(self):
"""Test behavior when the config file contains invalid JSON"""
@@ -57,18 +56,16 @@ class TestSessionConfig:
# Mock the open function to return invalid JSON
with patch('builtins.open', mock_open(read_data=invalid_json)):
with patch('logging.warning') as mock_warning:
sessions = SessionConfig.load_preferred_sessions()
with patch('logging.error') as mock_error:
with pytest.raises(SystemExit) as excinfo:
SessionConfig.load_preferred_sessions()
# Verify warning was logged
mock_warning.assert_called_once()
assert "decode" in mock_warning.call_args[0][0]
# Verify error was logged
mock_error.assert_called_once()
assert "decode" in mock_error.call_args[0][0]
# Verify default sessions are returned
assert len(sessions) == 3
assert sessions[0] == (2, "18:30", "CONDITIONING")
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
assert sessions[2] == (5, "12:30", "HYROX")
# Verify SystemExit was raised with exit code 1
assert excinfo.value.code == 1
def test_load_preferred_sessions_empty_file(self):
"""Test behavior when the config file is empty"""
@@ -79,11 +76,8 @@ class TestSessionConfig:
with patch('builtins.open', mock_open(read_data=empty_json)):
sessions = SessionConfig.load_preferred_sessions()
# Verify default sessions are returned
assert len(sessions) == 3
assert sessions[0] == (2, "18:30", "CONDITIONING")
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
assert sessions[2] == (5, "12:30", "HYROX")
# Verify empty list is returned
assert sessions == []
def test_load_preferred_sessions_missing_fields(self):
"""Test behavior when some fields are missing in the JSON data"""
@@ -111,18 +105,16 @@ class TestSessionConfig:
# Mock the open function to return partial JSON
with patch('builtins.open', mock_open(read_data=partial_json)):
with patch('logging.warning') as mock_warning:
sessions = SessionConfig.load_preferred_sessions()
with patch('logging.error') as mock_error:
with pytest.raises(SystemExit) as excinfo:
SessionConfig.load_preferred_sessions()
# Verify warning was logged
mock_warning.assert_called_once()
assert "decode" in mock_warning.call_args[0][0]
# Verify error was logged
mock_error.assert_called_once()
assert "decode" in mock_error.call_args[0][0]
# Verify default sessions are returned
assert len(sessions) == 3
assert sessions[0] == (2, "18:30", "CONDITIONING")
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
assert sessions[2] == (5, "12:30", "HYROX")
# Verify SystemExit was raised with exit code 1
assert excinfo.value.code == 1
def test_load_preferred_sessions_incorrect_field_types(self):
"""Test behavior when the config file contains JSON with incorrect field types"""