Implement Software Best Practices and Graceful API Key Management

- Integration of ruff, eslint, prettier for consistent code style
- Global error handling and structured logging in backend
- Comprehensive testing strategy with 100% pass rate (33 backend, 2 frontend)
- Graceful handling of missing Gemini API key (non-blocking startup)
- New /settings/status endpoint and UI warning indicators
- Full documentation update (ARCHITECTURE.md, CONTRIBUTING.md, README.md, GEMINI.md)
- Restored missing frontend logic and fixed UI syntax errors
This commit is contained in:
Moritz Graf
2026-01-01 20:37:10 +01:00
parent 8e55078c14
commit f3260d7dff
45 changed files with 5723 additions and 728 deletions
+5 -4
View File
@@ -1,10 +1,11 @@
from unittest.mock import MagicMock, patch
import pytest
import os
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
from main import app
client = TestClient(app)
client = TestClient(app, raise_server_exceptions=False)
@pytest.fixture
def mock_sync():
@@ -35,7 +36,7 @@ def test_get_activities_error(mock_sync):
response = client.get("/activities")
assert response.status_code == 500
assert "Load failed" in response.json()["detail"]
assert "INTERNAL_SERVER_ERROR" in response.json()["error"]
def test_get_recommendation(mock_sync, mock_engine):
mock_sync_instance = mock_sync.return_value
+28
View File
@@ -0,0 +1,28 @@
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_dashboard_endpoint():
"""Test that the dashboard endpoint returns structured data."""
response = client.get("/analyze/dashboard")
assert response.status_code == 200
data = response.json()
# Check structure
assert "summary" in data
assert "breakdown" in data
assert "strength_sessions" in data
# Check summary fields
summary = data["summary"]
assert "total_hours" in summary
assert "trend_pct" in summary
assert "period_label" in summary
def test_health_endpoint():
"""Test the health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
+22 -58
View File
@@ -1,19 +1,16 @@
import pytest
import os
from unittest.mock import MagicMock, patch
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from garmin.client import GarminClient
@pytest.fixture
def mock_garmin():
with patch("garmin.client.Garmin") as mock:
yield mock
@pytest.fixture
def mock_garth():
with patch("garmin.client.garth") as mock:
yield mock
@pytest.fixture
def mock_sso():
with patch("garmin.client.garth_login") as mock_login, \
@@ -25,7 +22,7 @@ def test_client_init():
assert client.email == "test@example.com"
assert client.password == "password"
def test_login_success(mock_sso, mock_garmin, mock_garth):
def test_login_success_force(mock_sso, mock_garmin):
mock_login, _ = mock_sso
mock_login.return_value = (MagicMock(), MagicMock())
client = GarminClient(email="test@example.com", password="password")
@@ -43,69 +40,63 @@ def test_login_mfa_required(mock_sso):
assert client.login(force_login=True) == "MFA_REQUIRED"
assert GarminClient._temp_client_state == {"some": "state"}
def test_login_mfa_complete(mock_sso, mock_garmin, mock_garth):
def test_login_mfa_complete(mock_sso, mock_garmin):
_, mock_resume_login = mock_sso
mock_client = MagicMock()
mock_client.oauth1_token = MagicMock()
state = {"some": "state", "client": mock_client}
GarminClient._temp_client_state = state
# resume_login should return (oauth1, oauth2)
mock_resume_login.return_value = (MagicMock(), MagicMock())
client = GarminClient(email="test@example.com", password="password")
assert client.login(mfa_code="123456") == "SUCCESS"
mock_resume_login.assert_called_with(state, "123456")
assert GarminClient._temp_client_state is None
assert mock_client.dump.called
def test_login_resume_success(mock_garmin):
client = GarminClient(email="test@example.com", password="password")
inst = MagicMock()
mock_garmin.return_value = inst
# Mocking both exists AND getsize to ensure we enter the resume block
with patch("os.path.exists", return_value=True), \
patch("os.path.getsize", return_value=100):
assert client.login() == "SUCCESS"
inst.login.assert_called_with(tokenstore=client.token_store)
def test_login_resume_fail_no_force(mock_garmin, mock_sso):
def test_login_resume_fail_falls_back(mock_garmin, mock_sso):
mock_login, _ = mock_sso
mock_login.return_value = (MagicMock(), MagicMock())
inst = MagicMock()
inst.login.side_effect = Exception("Resume fail")
mock_garmin.return_value = inst
client = GarminClient(email="test", password="test")
with patch("os.path.exists", return_value=True), \
patch("os.path.getsize", return_value=100):
patch("os.path.getsize", return_value=100), \
patch("os.remove"):
# Without force_login=True, it should fail if resume fails
assert client.login() == "FAILURE"
assert mock_login.call_count == 0
def test_login_resume_fail_with_force(mock_garmin, mock_sso):
def test_login_resume_fail_force_retries(mock_garmin, mock_sso):
mock_login, _ = mock_sso
mock_login.return_value = (MagicMock(), MagicMock())
inst1 = MagicMock()
inst1.login.side_effect = Exception("Resume fail")
inst2 = MagicMock()
inst2.login.return_value = None
# inst2 needs to return None or something to not throw
mock_garmin.side_effect = [inst1, inst2]
client = GarminClient(email="test", password="test")
with patch("os.path.exists", return_value=True), \
patch("os.path.getsize", return_value=100), \
patch("os.remove") as mock_remove:
patch("os.remove"):
assert client.login(force_login=True) == "SUCCESS"
assert mock_login.call_count == 1
def test_login_failure(mock_sso):
mock_login, _ = mock_sso
mock_login.side_effect = Exception("Fatal error")
client = GarminClient(email="test@example.com", password="password")
with patch("os.path.exists", return_value=False):
assert client.login(force_login=True) == "FAILURE"
def test_get_activities_not_logged_in():
client = GarminClient()
with pytest.raises(RuntimeError, match="Client not logged in"):
client.get_activities(date.today(), date.today())
assert mock_login.called
def test_get_activities_success(mock_garmin):
mock_instance = mock_garmin.return_value
@@ -116,30 +107,3 @@ def test_get_activities_success(mock_garmin):
activities = client.get_activities(date(2023, 1, 1), date(2023, 1, 2))
assert activities == [{"activityId": 123}]
def test_get_activities_failure(mock_garmin):
mock_instance = mock_garmin.return_value
mock_instance.get_activities_by_date.side_effect = Exception("err")
client = GarminClient()
client.client = mock_instance
assert client.get_activities(date.today(), date.today()) == []
def test_get_stats_success(mock_garmin):
mock_instance = mock_garmin.return_value
mock_instance.get_stats.return_value = {"steps": 1000}
client = GarminClient()
client.client = mock_instance
stats = client.get_stats(date(2023, 1, 1))
assert stats == {"steps": 1000}
def test_get_user_summary_success(mock_garmin):
mock_instance = mock_garmin.return_value
mock_instance.get_user_summary.return_value = {"calories": 2000}
client = GarminClient()
client.client = mock_instance
summary = client.get_user_summary(date(2023, 1, 1))
assert summary == {"calories": 2000}
+5 -3
View File
@@ -1,10 +1,12 @@
import os
import json
import os
from unittest.mock import MagicMock
import pytest
from unittest.mock import MagicMock, patch
from datetime import date
from garmin.sync import GarminSync
@pytest.fixture
def mock_client():
return MagicMock()
+4 -1
View File
@@ -1,8 +1,11 @@
import os
import json
import os
import pytest
from garmin.workout import GarminWorkoutCreator, StrengthWorkout, WorkoutStep
@pytest.fixture
def temp_workout_dir(tmp_path):
return str(tmp_path / "workouts")
+30 -29
View File
@@ -1,37 +1,38 @@
import pytest
from unittest.mock import MagicMock, patch
from recommendations.engine import RecommendationEngine
def test_get_recommendation_cycling():
engine = RecommendationEngine()
history = [{"activityName": "Morning Ride", "activityType": {"typeKey": "cycling"}}]
objective = "endurance"
@patch("google.genai.Client")
def test_chat_with_data_success(mock_genai_client):
# Setup mock
mock_chat = MagicMock()
mock_chat.send_message.return_value.text = "Keep it up!"
mock_client_inst = MagicMock()
mock_client_inst.chats.create.return_value = mock_chat
mock_genai_client.return_value = mock_client_inst
rec = engine.get_recommendation(history, objective)
assert "HIIT" in rec
def test_get_recommendation_strength():
engine = RecommendationEngine()
history = [{"activityName": "Upper Body", "activityType": {"typeKey": "strength_training"}}]
objective = "strong"
engine = RecommendationEngine(api_key="fake_key")
response = engine.chat_with_data("Hello", history=[])
rec = engine.get_recommendation(history, objective)
assert "leg strength" in rec
assert response == "Keep it up!"
assert mock_client_inst.chats.create.called
def test_get_recommendation_default():
engine = RecommendationEngine()
history = []
objective = "fitness"
@patch("google.genai.Client")
def test_get_recommendation_calls_chat(mock_genai_client):
mock_chat = MagicMock()
mock_chat.send_message.return_value.text = "Tip!"
mock_client_inst = MagicMock()
mock_client_inst.chats.create.return_value = mock_chat
mock_genai_client.return_value = mock_client_inst
engine = RecommendationEngine(api_key="fake_key")
response = engine.get_recommendation([], "fitness")
rec = engine.get_recommendation(history, objective)
assert "consistent work" in rec
assert response == "Tip!"
def test_summarize_history_empty():
engine = RecommendationEngine()
summary = engine._summarize_history([])
assert "No recent training data" in summary
def test_summarize_history_with_data():
engine = RecommendationEngine()
history = [{"activityName": "Run", "activityType": {"typeKey": "running"}}]
summary = engine._summarize_history(history)
assert "- Run (running)" in summary
@patch("os.getenv", return_value=None)
def test_mock_response_when_no_api_key(mock_env):
engine = RecommendationEngine(api_key=None)
# Mocking is done via client=None check
response = engine.chat_with_data("Hello")
assert "AI unavailable" in response