feat: Implement development pipeline and boost test coverage
- Add Makefile with `check`, `run` (blocking), and `force-stop` targets - Integrate fitmop.sh logic into Makefile - Achieve 100% backend test coverage - Improve frontend test coverage to >85% (Views/Components >90%) - Enforce `make check` in GEMINI.md - Add comprehensive tests for App.vue and WorkoutVisualEditor.vue
This commit is contained in:
+202
-41
@@ -1,4 +1,4 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -17,6 +17,16 @@ def mock_engine():
|
||||
with patch("main.RecommendationEngine") as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings_manager():
|
||||
with patch("main.SettingsManager") as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workout_manager():
|
||||
with patch("main.WorkoutManager") as mock:
|
||||
yield mock
|
||||
|
||||
def test_health():
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
@@ -38,7 +48,8 @@ def test_get_activities_error(mock_sync):
|
||||
assert response.status_code == 500
|
||||
assert "INTERNAL_SERVER_ERROR" in response.json()["error"]
|
||||
|
||||
def test_get_recommendation(mock_sync, mock_engine):
|
||||
def test_get_recommendation_success(mock_sync, mock_engine, monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
mock_sync_instance = mock_sync.return_value
|
||||
mock_sync_instance.load_local_activities.return_value = []
|
||||
|
||||
@@ -49,71 +60,221 @@ def test_get_recommendation(mock_sync, mock_engine):
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"recommendation": "Great job!"}
|
||||
|
||||
def test_auth_status_unauthenticated(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "")
|
||||
response = client.get("/auth/status")
|
||||
assert response.json()["authenticated"] is False
|
||||
def test_get_recommendation_missing_key(mock_sync, mock_engine, monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "")
|
||||
response = client.get("/recommendation")
|
||||
assert "not configured" in response.json()["recommendation"]
|
||||
|
||||
def test_auth_status_failure(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "test@test.com")
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.get("/auth/status")
|
||||
assert response.json()["authenticated"] is False
|
||||
assert response.json()["message"] == "Login failed"
|
||||
def test_settings_status():
|
||||
with patch("main.env") as mock_env:
|
||||
mock_env.get_status.return_value = {"configured": True}
|
||||
response = client.get("/settings")
|
||||
assert response.status_code == 200
|
||||
assert "garmin" in response.json()
|
||||
|
||||
def test_auth_status_success(monkeypatch, mock_sync):
|
||||
def test_settings_status_v2():
|
||||
response = client.get("/settings/status")
|
||||
assert response.status_code == 200
|
||||
assert "garmin" in response.json()
|
||||
|
||||
def test_update_settings_garmin():
|
||||
with patch("main.env") as mock_env:
|
||||
response = client.post("/settings/garmin", json={"email": "a", "password": "b"})
|
||||
assert response.status_code == 200
|
||||
mock_env.set_credentials.assert_called_once()
|
||||
|
||||
def test_update_settings_garmin_missing():
|
||||
response = client.post("/settings/garmin", json={})
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_update_settings_withings():
|
||||
with patch("main.env") as mock_env:
|
||||
response = client.post("/settings/withings", json={"client_id": "a", "client_secret": "b"})
|
||||
assert response.status_code == 200
|
||||
mock_env.set_credentials.assert_called_once()
|
||||
|
||||
def test_update_settings_gemini():
|
||||
with patch("main.env") as mock_env:
|
||||
response = client.post("/settings/gemini", json={"api_key": "a"})
|
||||
assert response.status_code == 200
|
||||
mock_env.set_credentials.assert_called_once()
|
||||
|
||||
def test_auth_status_success(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "test@test.com")
|
||||
monkeypatch.setenv("GARMIN_PASSWORD", "pass")
|
||||
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
response = client.get("/auth/status")
|
||||
assert response.json()["authenticated"] is True
|
||||
|
||||
def test_auth_status_mfa_required(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "test@test.com")
|
||||
def test_login_mfa():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "MFA_REQUIRED"
|
||||
response = client.get("/auth/status")
|
||||
response = client.post("/auth/login", json={"email": "a", "password": "b"})
|
||||
assert response.json()["status"] == "MFA_REQUIRED"
|
||||
|
||||
def test_login_success(mock_sync):
|
||||
def test_sync_smart(mock_sync):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
with patch("builtins.open", MagicMock()):
|
||||
response = client.post("/auth/login", json={"email": "a", "password": "b"})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "SUCCESS"
|
||||
mock_sync.return_value.sync_smart.return_value = 10
|
||||
response = client.post("/sync/smart")
|
||||
assert response.json()["synced_count"] == 10
|
||||
|
||||
def test_login_mfa_required():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "MFA_REQUIRED"
|
||||
response = client.post("/auth/login", json={"email": "a", "password": "b"})
|
||||
assert response.json()["status"] == "MFA_REQUIRED"
|
||||
|
||||
def test_login_missing_data(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "")
|
||||
monkeypatch.setenv("GARMIN_PASSWORD", "")
|
||||
response = client.post("/auth/login", json={})
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_login_invalid_creds():
|
||||
def test_sync_smart_fail(mock_sync):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.post("/auth/login", json={"email": "a", "password": "b"})
|
||||
response = client.post("/sync/smart")
|
||||
assert response.json()["success"] is False
|
||||
|
||||
def test_analyze_stats(mock_sync):
|
||||
mock_sync.return_value.get_weekly_stats.return_value = {"labels": ["W1"]}
|
||||
response = client.get("/analyze/stats")
|
||||
assert response.json()["weekly"]["labels"] == ["W1"]
|
||||
|
||||
def test_analyze_stats_error(mock_sync):
|
||||
mock_sync.return_value.get_weekly_stats.side_effect = Exception("Err")
|
||||
response = client.get("/analyze/stats")
|
||||
assert "weekly" in response.json()
|
||||
|
||||
def test_profile(mock_settings_manager):
|
||||
mock_settings_manager.return_value.load_profile.return_value = {"name": "Test"}
|
||||
response = client.get("/settings/profile")
|
||||
assert response.json()["name"] == "Test"
|
||||
|
||||
response = client.post("/settings/profile", json={"name": "New"})
|
||||
assert response.json()["status"] == "SUCCESS"
|
||||
|
||||
def test_analyze_chat(mock_engine, monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "k")
|
||||
mock_engine.return_value.chat_with_data.return_value = "Hello"
|
||||
response = client.post("/analyze/chat", json={"message": "hi"})
|
||||
assert response.json()["message"] == "Hello"
|
||||
|
||||
def test_workouts_list():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_client.return_value.get_workouts_list.return_value = []
|
||||
response = client.get("/workouts")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_workouts_chat(mock_workout_manager):
|
||||
# main.py line 289 returns {"workout": workout}
|
||||
mock_workout_manager.return_value.generate_workout_json.return_value = {"ok": True}
|
||||
response = client.post("/workouts/chat", json={"prompt": "test"})
|
||||
assert response.json()["workout"]["ok"] is True
|
||||
|
||||
def test_workout_constants(mock_workout_manager):
|
||||
mock_workout_manager.return_value.get_constants.return_value = {"C": 1}
|
||||
response = client.get("/workouts/constants")
|
||||
assert response.json()["C"] == 1
|
||||
|
||||
def test_workout_upload(mock_workout_manager):
|
||||
mock_workout_manager.return_value.validate_workout_json.return_value = []
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_client.return_value.upload_workout.return_value = {"id": 1}
|
||||
response = client.post("/workouts/upload", json={"name": "W"})
|
||||
assert response.json()["success"] is True
|
||||
|
||||
def test_update_settings_withings_missing():
|
||||
response = client.post("/settings/withings", json={})
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_update_settings_gemini_missing():
|
||||
response = client.post("/settings/gemini", json={})
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_auth_status_not_configured(monkeypatch):
|
||||
monkeypatch.setenv("GARMIN_EMAIL", "")
|
||||
response = client.get("/auth/status")
|
||||
assert response.json()["authenticated"] is False
|
||||
|
||||
def test_sync_full_success(mock_sync):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_sync.return_value.sync_activities.return_value = 100
|
||||
response = client.post("/sync/full")
|
||||
assert response.json()["synced_count"] == 100
|
||||
|
||||
def test_sync_full_fail():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.post("/sync/full")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_trigger_sync_success(mock_sync):
|
||||
def test_sync_smart_error(mock_sync):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_sync.return_value.sync_smart.side_effect = Exception("Smart fail")
|
||||
response = client.post("/sync/smart")
|
||||
assert response.json()["success"] is False
|
||||
|
||||
def test_workouts_list_fail():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.get("/workouts")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_workouts_chat_error(mock_workout_manager):
|
||||
mock_workout_manager.return_value.generate_workout_json.side_effect = Exception("AI Fail")
|
||||
response = client.post("/workouts/chat", json={"prompt": "test"})
|
||||
assert "AI Fail" in response.json()["error"]
|
||||
|
||||
def test_dashboard_stats_error(mock_sync):
|
||||
mock_sync.return_value.get_dashboard_stats.side_effect = Exception("Dash fail")
|
||||
response = client.get("/analyze/dashboard")
|
||||
assert "Dash fail" in response.json()["error"]
|
||||
|
||||
def test_workout_validate_invalid(mock_workout_manager):
|
||||
mock_workout_manager.return_value.validate_workout_json.return_value = ["Error"]
|
||||
response = client.post("/workouts/validate", json={})
|
||||
assert response.json()["valid"] is False
|
||||
|
||||
def test_workout_upload_fail_validate(mock_workout_manager):
|
||||
mock_workout_manager.return_value.validate_workout_json.return_value = ["Error"]
|
||||
response = client.post("/workouts/upload", json={})
|
||||
assert response.json()["success"] is False
|
||||
|
||||
def test_workout_upload_fail_login(mock_workout_manager):
|
||||
mock_workout_manager.return_value.validate_workout_json.return_value = []
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.post("/workouts/upload", json={})
|
||||
assert response.json()["success"] is False
|
||||
|
||||
def test_workout_upload_exception(mock_workout_manager):
|
||||
mock_workout_manager.return_value.validate_workout_json.return_value = []
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_client.return_value.upload_workout.side_effect = Exception("Upload fail")
|
||||
response = client.post("/workouts/upload", json={})
|
||||
assert response.json()["success"] is False
|
||||
|
||||
def test_login_save_credentials(monkeypatch):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
with patch("main.env") as mock_env:
|
||||
response = client.post("/auth/login", json={"email": "new@test.com", "password": "new"})
|
||||
assert response.status_code == 200
|
||||
mock_env.set_credentials.assert_called_once()
|
||||
def test_trigger_sync_endpoint_success(mock_sync):
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "SUCCESS"
|
||||
mock_sync.return_value.sync_activities.return_value = 5
|
||||
response = client.post("/sync")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["synced_count"] == 5
|
||||
|
||||
def test_trigger_sync_unauthorized():
|
||||
def test_trigger_sync_endpoint_fail():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.post("/sync")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_login_credentials_missing():
|
||||
response = client.post("/auth/login", json={"email": ""})
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_login_failed_error():
|
||||
with patch("main.GarminClient") as mock_client:
|
||||
mock_client.return_value.login.return_value = "FAILURE"
|
||||
response = client.post("/auth/login", json={"email": "a", "password": "b"})
|
||||
assert response.status_code == 401
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from common.env_manager import EnvManager
|
||||
from common.settings_manager import SettingsManager
|
||||
|
||||
|
||||
def test_env_manager_basic(tmp_path):
|
||||
# Setup temp env files
|
||||
env_dir = tmp_path / "envs"
|
||||
env_dir.mkdir()
|
||||
|
||||
manager = EnvManager(str(env_dir))
|
||||
|
||||
# Test set_credentials
|
||||
manager.set_credentials("test", {"KEY1": "VAL1", "KEY2": "VAL2"})
|
||||
|
||||
path = manager.get_env_path("test")
|
||||
assert os.path.exists(path)
|
||||
|
||||
with open(path, "r") as f:
|
||||
content = f.read()
|
||||
# set_key might quote values
|
||||
assert "KEY1='VAL1'" in content or "KEY1=VAL1" in content
|
||||
|
||||
# Test load_service_env
|
||||
manager.load_service_env("test")
|
||||
assert os.environ["KEY1"] == "VAL1"
|
||||
|
||||
# Test get_status
|
||||
status = manager.get_status("test", ["KEY1", "KEY3"])
|
||||
assert status["configured"] is False
|
||||
assert "KEY3" in status["missing_keys"]
|
||||
|
||||
status = manager.get_status("test", ["KEY1", "KEY2"])
|
||||
assert status["configured"] is True
|
||||
|
||||
def test_settings_manager_basic(tmp_path):
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir()
|
||||
|
||||
manager = SettingsManager(str(data_dir))
|
||||
|
||||
# Test save_profile
|
||||
profile_data = {"fitness_goals": "Lose weight", "focus_days": ["Monday"]}
|
||||
manager.save_profile(profile_data)
|
||||
|
||||
profile_path = os.path.join(str(data_dir), "user_profile.json")
|
||||
assert os.path.exists(profile_path)
|
||||
|
||||
with open(profile_path, "r") as f:
|
||||
saved = json.load(f)
|
||||
assert saved["fitness_goals"] == "Lose weight"
|
||||
|
||||
# Test get_profile (mapped to load_profile)
|
||||
loaded = manager.load_profile()
|
||||
assert loaded["fitness_goals"] == "Lose weight"
|
||||
assert "dietary_preferences" in loaded # Default value
|
||||
|
||||
# Test get_context_string
|
||||
ctx = manager.get_context_string()
|
||||
assert "Lose weight" in ctx
|
||||
assert "Dietary Preferences" in ctx
|
||||
|
||||
def test_settings_manager_error_handling(tmp_path):
|
||||
data_dir = tmp_path / "data_error"
|
||||
data_dir.mkdir()
|
||||
profile_path = data_dir / "user_profile.json"
|
||||
|
||||
# Write corrupted JSON
|
||||
with open(profile_path, "w") as f:
|
||||
f.write("invalid json")
|
||||
|
||||
manager = SettingsManager(str(data_dir))
|
||||
profile = manager.load_profile()
|
||||
assert profile == {}
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -17,6 +17,13 @@ def mock_sso():
|
||||
patch("garmin.client.resume_login") as mock_resume_login:
|
||||
yield mock_login, mock_resume_login
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_client():
|
||||
"""Ensure static state is clean."""
|
||||
GarminClient._temp_client_state = None
|
||||
yield
|
||||
GarminClient._temp_client_state = None
|
||||
|
||||
def test_client_init():
|
||||
client = GarminClient(email="test@example.com", password="password")
|
||||
assert client.email == "test@example.com"
|
||||
@@ -47,20 +54,33 @@ def test_login_mfa_complete(mock_sso, mock_garmin):
|
||||
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
|
||||
|
||||
def test_login_mfa_complete_no_client_in_state(mock_sso, mock_garmin):
|
||||
_, mock_resume_login = mock_sso
|
||||
state = {"some": "state"}
|
||||
GarminClient._temp_client_state = state
|
||||
mock_resume_login.return_value = (MagicMock(), MagicMock())
|
||||
|
||||
client = GarminClient(email="test@example.com", password="password")
|
||||
with patch("garmin.client.garth") as mock_garth:
|
||||
assert client.login(mfa_code="123456") == "SUCCESS"
|
||||
mock_garth.client.configure.assert_called_once()
|
||||
|
||||
def test_login_mfa_required_no_creds(mock_garmin):
|
||||
client = GarminClient(email="", password="")
|
||||
GarminClient._temp_client_state = {"some": "state"}
|
||||
with patch("os.path.exists", return_value=False):
|
||||
assert client.login() == "MFA_REQUIRED"
|
||||
|
||||
def test_login_resume_success(mock_garmin):
|
||||
client = GarminClient(email="test@example.com", password="password")
|
||||
inst = MagicMock()
|
||||
mock_garmin.return_value = inst
|
||||
inst = mock_garmin.return_value
|
||||
|
||||
# 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"
|
||||
@@ -70,26 +90,24 @@ def test_login_resume_fail_falls_back(mock_garmin, mock_sso):
|
||||
mock_login, _ = mock_sso
|
||||
mock_login.return_value = (MagicMock(), MagicMock())
|
||||
|
||||
inst = MagicMock()
|
||||
inst = mock_garmin.return_value
|
||||
inst.login.side_effect = Exception("Resume fail")
|
||||
mock_garmin.return_value = inst
|
||||
|
||||
client = GarminClient(email="test", password="test")
|
||||
# Step 3 will check if creds exist. If they do, it goes to login.
|
||||
# But resume_fail_falls_back test expects FAILURE if not force_login.
|
||||
with patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.getsize", return_value=100), \
|
||||
patch("os.remove"):
|
||||
# Without force_login=True, it should fail if resume fails
|
||||
assert client.login() == "FAILURE"
|
||||
|
||||
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 needs to return None or something to not throw
|
||||
mock_garmin.side_effect = [inst1, inst2]
|
||||
inst = mock_garmin.return_value
|
||||
# First call to inst.login (resume) fails, second call (new login) succeeds
|
||||
inst.login.side_effect = [Exception("Resume fail"), None]
|
||||
|
||||
client = GarminClient(email="test", password="test")
|
||||
with patch("os.path.exists", return_value=True), \
|
||||
@@ -98,12 +116,117 @@ def test_login_resume_fail_force_retries(mock_garmin, mock_sso):
|
||||
assert client.login(force_login=True) == "SUCCESS"
|
||||
assert mock_login.called
|
||||
|
||||
def test_get_activities_success(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_activities_by_date.return_value = [{"activityId": 123}]
|
||||
def test_login_empty_token_cleanup(mock_garmin):
|
||||
client = GarminClient()
|
||||
with patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.getsize", return_value=0), \
|
||||
patch("os.remove") as mock_remove:
|
||||
assert client.login() == "FAILURE"
|
||||
assert mock_remove.called
|
||||
|
||||
def test_login_json_error_cleanup(mock_garmin):
|
||||
client = GarminClient()
|
||||
inst = mock_garmin.return_value
|
||||
inst.login.side_effect = Exception("Expecting value: line 1 column 1")
|
||||
|
||||
with patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.getsize", return_value=100), \
|
||||
patch("os.remove") as mock_remove:
|
||||
assert client.login() == "FAILURE"
|
||||
assert mock_remove.called
|
||||
|
||||
def test_login_missing_creds(mock_garmin):
|
||||
client = GarminClient(email="", password="")
|
||||
with patch("os.path.exists", return_value=False):
|
||||
assert client.login() == "FAILURE"
|
||||
|
||||
def test_get_activities_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_activities_by_date.side_effect = Exception("API Error")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
|
||||
activities = client.get_activities(date(2023, 1, 1), date(2023, 1, 2))
|
||||
assert activities == [{"activityId": 123}]
|
||||
assert client.get_activities(date(2023, 1, 1), date(2023, 1, 2)) == []
|
||||
|
||||
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
|
||||
assert client.get_stats(date(2023, 1, 1)) == {"steps": 1000}
|
||||
|
||||
def test_get_stats_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_stats.side_effect = Exception("Err")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_stats(date(2023, 1, 1)) == {}
|
||||
|
||||
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
|
||||
assert client.get_user_summary(date(2023, 1, 1)) == {"calories": 2000}
|
||||
|
||||
def test_get_user_summary_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_user_summary.side_effect = Exception("Err")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_user_summary(date(2023, 1, 1)) == {}
|
||||
|
||||
def test_get_workouts_list_success(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_workouts.return_value = [{"name": "W1"}]
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_workouts_list() == [{"name": "W1"}]
|
||||
|
||||
def test_get_workouts_list_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_workouts.side_effect = Exception("Err")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_workouts_list() == []
|
||||
|
||||
def test_get_workout_detail_success(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_workout_by_id.return_value = {"id": "1"}
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_workout_detail("1") == {"id": "1"}
|
||||
|
||||
def test_get_workout_detail_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.get_workout_by_id.side_effect = Exception("Err")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.get_workout_detail("1") == {}
|
||||
|
||||
def test_upload_workout_success(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.upload_workout({"json": True}) is True
|
||||
|
||||
def test_upload_workout_error(mock_garmin):
|
||||
mock_instance = mock_garmin.return_value
|
||||
mock_instance.upload_workout.side_effect = Exception("Err")
|
||||
client = GarminClient()
|
||||
client.client = mock_instance
|
||||
assert client.upload_workout({"json": True}) is False
|
||||
|
||||
def test_not_logged_in_errors():
|
||||
client = GarminClient()
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_activities(date.today(), date.today())
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_stats(date.today())
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_user_summary(date.today())
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_workouts_list()
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_workout_detail("1")
|
||||
with pytest.raises(RuntimeError):
|
||||
client.upload_workout({})
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from garmin.workout_manager import WorkoutManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ai():
|
||||
return MagicMock()
|
||||
|
||||
def test_workout_manager_validation():
|
||||
manager = WorkoutManager()
|
||||
workout = {
|
||||
"workoutName": "Test",
|
||||
"sportType": {"sportTypeId": 1, "sportTypeKey": "running"},
|
||||
"workoutSegments": [{
|
||||
"segmentOrder": 1,
|
||||
"workoutSteps": [
|
||||
{
|
||||
"type": "ExecutableStepDTO",
|
||||
"stepOrder": 1,
|
||||
"stepType": {"stepTypeId": 1, "stepTypeKey": "warmup"},
|
||||
"endCondition": {"conditionTypeId": 2, "conditionTypeKey": "time"},
|
||||
"endConditionValue": 600
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
errors = manager.validate_workout_json(workout)
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_workout_manager_validation_repeat():
|
||||
manager = WorkoutManager()
|
||||
workout = {
|
||||
"workoutName": "Repeat Test",
|
||||
"sportType": {"sportTypeId": 1, "sportTypeKey": "running"},
|
||||
"workoutSegments": [{
|
||||
"segmentOrder": 1,
|
||||
"workoutSteps": [
|
||||
{
|
||||
"type": "RepeatGroupDTO",
|
||||
"numberOfIterations": 3,
|
||||
"workoutSteps": [
|
||||
{
|
||||
"type": "ExecutableStepDTO",
|
||||
"stepType": {"stepTypeId": 3, "stepTypeKey": "interval"},
|
||||
"endCondition": {"conditionTypeId": 1, "conditionTypeKey": "distance"},
|
||||
"endConditionValue": 1000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
errors = manager.validate_workout_json(workout)
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_workout_manager_constants():
|
||||
manager = WorkoutManager()
|
||||
constants = manager.get_constants()
|
||||
assert "SportType" in constants
|
||||
assert "StepType" in constants
|
||||
|
||||
def test_workout_manager_generate_json(mock_ai):
|
||||
manager = WorkoutManager(ai_engine=mock_ai)
|
||||
mock_ai.generate_json.return_value = {"name": "Mocha Workout"}
|
||||
|
||||
res = manager.generate_workout_json("make it harder")
|
||||
assert res["name"] == "Mocha Workout"
|
||||
mock_ai.generate_json.assert_called_once()
|
||||
|
||||
def test_mock_ai_builder():
|
||||
manager = WorkoutManager()
|
||||
|
||||
# Test Run
|
||||
run_workout = manager._mock_ai_builder("I want to run 5k")
|
||||
assert run_workout["workoutName"] == "AI Run Session"
|
||||
assert run_workout["sportType"]["sportTypeKey"] == "running"
|
||||
|
||||
# Test Bike
|
||||
bike_workout = manager._mock_ai_builder("cycling session")
|
||||
assert bike_workout["workoutName"] == "AI Ride"
|
||||
|
||||
# Test Default (Strength)
|
||||
strength_workout = manager._mock_ai_builder("lift weights")
|
||||
assert strength_workout["workoutName"] == "AI Strength"
|
||||
assert strength_workout["sportType"]["sportTypeKey"] == "strength_training"
|
||||
@@ -1,10 +1,12 @@
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
from datetime import date, timedelta
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from garmin.sync import GarminSync
|
||||
from garmin.validator import WorkoutValidator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -32,15 +34,263 @@ def test_load_local_activities(mock_client, temp_storage):
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
|
||||
json.dump({"activityId": 1}, f)
|
||||
# Corrupted file
|
||||
with open(os.path.join(temp_storage, "activity_error.json"), "w") as f:
|
||||
f.write("invalid")
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
activities = sync.load_local_activities()
|
||||
|
||||
assert len(activities) == 1
|
||||
assert activities[0]["activityId"] == 1
|
||||
|
||||
def test_save_activity_no_id(mock_client, temp_storage):
|
||||
def test_sync_smart_no_local(mock_client, temp_storage):
|
||||
mock_client.get_activities.return_value = []
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
sync._save_activity({"name": "No ID"})
|
||||
sync.sync_smart()
|
||||
mock_client.get_activities.assert_called_with(ANY, ANY)
|
||||
|
||||
def test_sync_smart_with_local(mock_client, temp_storage):
|
||||
today = date.today()
|
||||
yesterday = today - timedelta(days=1)
|
||||
|
||||
assert len(os.listdir(temp_storage)) == 0 if os.path.exists(temp_storage) else True
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
|
||||
json.dump({"activityId": 1, "startTimeLocal": yesterday.strftime("%Y-%m-%d %H:%M:%S")}, f)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
mock_client.get_activities.return_value = []
|
||||
sync.sync_smart()
|
||||
mock_client.get_activities.assert_called()
|
||||
|
||||
def test_sync_smart_no_start_time(mock_client, temp_storage):
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
|
||||
json.dump({"activityId": 1}, f) # Missing startTimeLocal
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
mock_client.get_activities.return_value = []
|
||||
sync.sync_smart()
|
||||
mock_client.get_activities.assert_called()
|
||||
|
||||
def test_weekly_stats(mock_client, temp_storage):
|
||||
fixed_today = date(2026, 1, 1)
|
||||
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
# Types covering all color branches
|
||||
types = [
|
||||
"running", "trail_running", "virtual_ride", "indoor_cycling", "cycling",
|
||||
"lap_swimming", "open_water_swimming", "yoga", "pilates", "breathing",
|
||||
"strength_training", "hiking", "walking", "unknown"
|
||||
]
|
||||
for i, t in enumerate(types):
|
||||
with open(os.path.join(temp_storage, f"activity_{i}.json"), "w") as f:
|
||||
json.dump({
|
||||
"activityId": i,
|
||||
"startTimeLocal": fixed_today.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"duration": 3600,
|
||||
"activityType": {"typeKey": t}
|
||||
}, f)
|
||||
|
||||
with patch("garmin.sync.date") as mock_date:
|
||||
mock_date.today.return_value = fixed_today
|
||||
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
stats = sync.get_weekly_stats(weeks=1)
|
||||
|
||||
assert len(stats["datasets"]) > 0
|
||||
|
||||
def test_dashboard_stats(mock_client, temp_storage):
|
||||
fixed_today = date(2026, 1, 1)
|
||||
prev_date = fixed_today - timedelta(days=10)
|
||||
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
# Current period
|
||||
with open(os.path.join(temp_storage, "activity_curr.json"), "w") as f:
|
||||
json.dump({
|
||||
"activityId": 100,
|
||||
"startTimeLocal": fixed_today.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"duration": 7200,
|
||||
"activityType": {"typeKey": "strength_training"}
|
||||
}, f)
|
||||
# Previous period
|
||||
with open(os.path.join(temp_storage, "activity_prev.json"), "w") as f:
|
||||
json.dump({
|
||||
"activityId": 101,
|
||||
"startTimeLocal": prev_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"duration": 3600,
|
||||
"activityType": {"typeKey": "cycling"}
|
||||
}, f)
|
||||
|
||||
# Mocking date more safely
|
||||
with patch("garmin.sync.date") as mock_date:
|
||||
mock_date.today.return_value = fixed_today
|
||||
# Allow creating new date objects
|
||||
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
stats = sync.get_dashboard_stats()
|
||||
|
||||
# Debug print in case of failure
|
||||
if stats["summary"]["total_hours"] != 2.0:
|
||||
print(f"DEBUG: stats={stats}")
|
||||
# Let's check why act_curr wasn't picked up
|
||||
acts = sync.load_local_activities()
|
||||
print(f"DEBUG: loaded activities={acts}")
|
||||
|
||||
assert stats["summary"]["total_hours"] == 2.0
|
||||
assert stats["summary"]["trend_pct"] == 100.0
|
||||
assert stats["strength_sessions"] == 1
|
||||
|
||||
def test_sync_smart_no_days_to_sync(mock_client, temp_storage):
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
today = date.today()
|
||||
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
|
||||
json.dump({"activityId": 1, "startTimeLocal": today.strftime("%Y-%m-%d %H:%M:%S")}, f)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
assert sync.sync_smart() == 0
|
||||
mock_client.get_activities.assert_not_called()
|
||||
|
||||
def test_sync_smart_exception(mock_client, temp_storage):
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
with patch.object(sync, 'load_local_activities', side_effect=Exception("Fail")):
|
||||
with pytest.raises(Exception):
|
||||
sync.sync_smart()
|
||||
|
||||
def test_weekly_stats_missing_data(mock_client, temp_storage):
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_missing.json"), "w") as f:
|
||||
json.dump({"activityId": 1}, f) # No startTimeLocal
|
||||
with open(os.path.join(temp_storage, "activity_bad_date.json"), "w") as f:
|
||||
json.dump({"activityId": 2, "startTimeLocal": "bad"}, f)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
stats = sync.get_weekly_stats(weeks=1)
|
||||
assert len(stats["labels"]) == 0
|
||||
|
||||
def test_dashboard_stats_exception(mock_client, temp_storage):
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
with patch.object(sync, 'load_local_activities', side_effect=Exception("Fail")):
|
||||
with pytest.raises(Exception):
|
||||
sync.get_dashboard_stats()
|
||||
|
||||
def test_validator_more_errors():
|
||||
validator = WorkoutValidator()
|
||||
# Segment with no steps
|
||||
errors = validator.validate_workout({
|
||||
"workoutName": "T", "sportType": {"sportTypeId": 1},
|
||||
"workoutSegments": [{"workoutSteps": []}]
|
||||
})
|
||||
assert "Segment 0 has no steps" in errors
|
||||
|
||||
# Missing stepType or stepTypeId
|
||||
errors = validator._validate_executable_step({}, "Ctx")
|
||||
assert "Ctx: Missing stepType or stepTypeId" in errors
|
||||
|
||||
def test_sync_activities_save_error(mock_client, temp_storage):
|
||||
mock_client.get_activities.return_value = [{"activityId": 1}]
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
with patch("builtins.open", side_effect=IOError("Fail")):
|
||||
# Should not raise exception
|
||||
count = sync.sync_activities(days=1)
|
||||
assert count == 1
|
||||
|
||||
def test_load_local_activities_more_errors(temp_storage):
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
# File not ending in .json (should be ignored by load_local)
|
||||
with open(os.path.join(temp_storage, "other.txt"), "w") as f:
|
||||
f.write("text")
|
||||
|
||||
sync = GarminSync(None, storage_dir=temp_storage)
|
||||
assert sync.load_local_activities() == []
|
||||
|
||||
def test_sync_activities_missing_id(mock_client, temp_storage):
|
||||
# Coverage for sync.py:37
|
||||
mock_client.get_activities.return_value = [{"name": "No ID"}]
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
count = sync.sync_activities(days=1)
|
||||
assert count == 1
|
||||
# File should not be saved
|
||||
assert len(os.listdir(temp_storage)) == 0
|
||||
|
||||
def test_sync_smart_up_to_date(mock_client, temp_storage):
|
||||
# Coverage for sync.py:105
|
||||
today = date.today()
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_1.json"), "w") as f:
|
||||
json.dump({"activityId": 1, "startTimeLocal": today.strftime("%Y-%m-%d %H:%M:%S")}, f)
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
assert sync.sync_smart() == 0
|
||||
|
||||
def test_weekly_stats_cutoff(mock_client, temp_storage):
|
||||
# Coverage for sync.py:139
|
||||
fixed_today = date(2026, 1, 1)
|
||||
old_date = fixed_today - timedelta(days=100)
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_old.json"), "w") as f:
|
||||
json.dump({
|
||||
"activityId": 1,
|
||||
"startTimeLocal": old_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"duration": 3600,
|
||||
"activityType": {"typeKey": "running"}
|
||||
}, f)
|
||||
with patch("garmin.sync.date") as mock_date:
|
||||
mock_date.today.return_value = fixed_today
|
||||
# Allow creating new date objects
|
||||
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
|
||||
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
stats = sync.get_weekly_stats(weeks=1)
|
||||
assert len(stats["datasets"]) == 0
|
||||
|
||||
def test_dashboard_stats_edge_cases(mock_client, temp_storage):
|
||||
# Coverage for sync.py:241, 245-246
|
||||
os.makedirs(temp_storage, exist_ok=True)
|
||||
with open(os.path.join(temp_storage, "activity_no_start.json"), "w") as f:
|
||||
json.dump({"activityId": 1}, f)
|
||||
with open(os.path.join(temp_storage, "activity_bad_start.json"), "w") as f:
|
||||
json.dump({"activityId": 2, "startTimeLocal": "invalid"}, f)
|
||||
sync = GarminSync(mock_client, storage_dir=temp_storage)
|
||||
stats = sync.get_dashboard_stats()
|
||||
assert stats["summary"]["total_hours"] == 0
|
||||
|
||||
def test_validator_all_errors():
|
||||
# Coverage for validator.py
|
||||
v = WorkoutValidator()
|
||||
# Missing fields
|
||||
errors1 = v.validate_workout({})
|
||||
assert any("Missing required field" in e for e in errors1)
|
||||
|
||||
# Missing sportTypeId
|
||||
errors2 = v.validate_workout({
|
||||
"workoutName": "T", "sportType": {}, "workoutSegments": [{"workoutSteps": []}]
|
||||
})
|
||||
assert "Missing sportType.sportTypeId" in errors2
|
||||
|
||||
# Empty segments
|
||||
errors3 = v.validate_workout({
|
||||
"workoutName": "T", "sportType": {"sportTypeId": 1}, "workoutSegments": []
|
||||
})
|
||||
assert "workoutSegments must be a non-empty list" in errors3
|
||||
|
||||
# Unknown step type
|
||||
errors4 = v._validate_steps([{"type": "Unknown"}], "Ctx")
|
||||
assert "Ctx Step 1: Unknown step type 'Unknown'" in errors4
|
||||
|
||||
# Invalid stepTypeId
|
||||
errors5 = v._validate_executable_step({"stepType": {"stepTypeId": 99}, "endCondition": {"conditionTypeId": 1}}, "Ctx")
|
||||
assert "Ctx: Invalid stepTypeId 99" in errors5
|
||||
|
||||
# Invalid iterations
|
||||
errors6 = v._validate_repeat_group({"numberOfIterations": 0}, "Ctx")
|
||||
assert "Ctx: Invalid iterations 0" in errors6
|
||||
|
||||
# Empty repeat group
|
||||
errors7 = v._validate_repeat_group({"numberOfIterations": 1, "workoutSteps": []}, "Ctx")
|
||||
assert "Ctx: Repeat group empty" in errors7
|
||||
|
||||
# Constants
|
||||
constants = v.get_constants()
|
||||
assert "SportType" in constants
|
||||
|
||||
|
||||
@@ -52,3 +52,11 @@ def test_load_local_workouts(temp_workout_dir):
|
||||
workouts = creator.load_local_workouts()
|
||||
assert len(workouts) == 1
|
||||
assert workouts[0].name == "Stored Workout"
|
||||
|
||||
def test_load_local_workouts_corrupted(temp_workout_dir):
|
||||
os.makedirs(temp_workout_dir, exist_ok=True)
|
||||
with open(os.path.join(temp_workout_dir, "corrupted.json"), "w") as f:
|
||||
f.write("invalid json")
|
||||
creator = GarminWorkoutCreator(storage_dir=temp_workout_dir)
|
||||
# Should skip the corrupted file
|
||||
assert len(creator.load_local_workouts()) == 0
|
||||
|
||||
@@ -1,38 +1,142 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from recommendations.engine import RecommendationEngine
|
||||
from recommendations.tools import FitnessTools
|
||||
|
||||
@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
|
||||
|
||||
@pytest.fixture
|
||||
def mock_genai():
|
||||
with patch("recommendations.engine.genai.Client") as mock_client_class, \
|
||||
patch("recommendations.engine.types") as mock_types:
|
||||
mock_client_instance = mock_client_class.return_value
|
||||
# Properly mock Part.from_text to return something simple
|
||||
mock_types.Part.from_text.side_effect = lambda x: MagicMock(text=x)
|
||||
yield mock_client_instance, mock_types
|
||||
|
||||
def test_engine_init():
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
assert engine.api_key == "test-key"
|
||||
assert engine.client is not None
|
||||
|
||||
def test_engine_no_key():
|
||||
with patch("os.getenv", return_value=None):
|
||||
engine = RecommendationEngine(api_key="")
|
||||
assert engine.client is None
|
||||
|
||||
def test_chat_with_data_success(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_chat = mock_client.chats.create.return_value
|
||||
mock_response = mock_chat.send_message.return_value
|
||||
mock_response.text = "Mock AI Response"
|
||||
|
||||
engine = RecommendationEngine(api_key="fake_key")
|
||||
response = engine.chat_with_data("Hello", history=[])
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
history = [{"role": "user", "content": "hi"}]
|
||||
result = engine.chat_with_data("hello", history)
|
||||
|
||||
assert response == "Keep it up!"
|
||||
assert mock_client_inst.chats.create.called
|
||||
assert result == "Mock AI Response"
|
||||
mock_client.chats.create.assert_called_once()
|
||||
|
||||
@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")
|
||||
def test_chat_with_data_no_text(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_chat = mock_client.chats.create.return_value
|
||||
mock_response = mock_chat.send_message.return_value
|
||||
mock_response.text = None
|
||||
|
||||
assert response == "Tip!"
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
result = engine.chat_with_data("hello")
|
||||
assert "analyzed the data but have no specific comment" in result
|
||||
|
||||
@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
|
||||
def test_chat_with_data_no_client():
|
||||
with patch("os.getenv", return_value=None):
|
||||
engine = RecommendationEngine(api_key="")
|
||||
result = engine.chat_with_data("hi")
|
||||
assert "AI unavailable" in result
|
||||
|
||||
def test_chat_with_data_error(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_client.chats.create.side_effect = Exception("API Error")
|
||||
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
result = engine.chat_with_data("hi")
|
||||
assert "error analyzing your data" in result
|
||||
|
||||
def test_get_recommendation(mock_genai):
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
with patch.object(engine, 'chat_with_data', return_value="Tip") as mock_chat:
|
||||
result = engine.get_recommendation([], "run")
|
||||
assert result == "Tip"
|
||||
assert mock_chat.called
|
||||
|
||||
def test_generate_json_success(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_response = mock_client.models.generate_content.return_value
|
||||
mock_response.parsed = {"workout": 1}
|
||||
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
result = engine.generate_json("create workout")
|
||||
assert result == {"workout": 1}
|
||||
|
||||
def test_generate_json_text_fallback(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_response = mock_client.models.generate_content.return_value
|
||||
mock_response.parsed = None
|
||||
# Wrap JSON in Markdown for realistic fallback
|
||||
mock_response.text = '```json\n{"workout": 2}\n```'
|
||||
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
result = engine.generate_json("modify workout", context_json={"id": 0})
|
||||
assert result == {"workout": 2}
|
||||
|
||||
def test_generate_json_error(mock_genai):
|
||||
mock_client, mock_types = mock_genai
|
||||
mock_client.models.generate_content.side_effect = Exception("Gen Error")
|
||||
|
||||
engine = RecommendationEngine(api_key="test-key")
|
||||
with pytest.raises(Exception):
|
||||
engine.generate_json("hi")
|
||||
|
||||
def test_generate_json_no_client():
|
||||
with patch("os.getenv", return_value=None):
|
||||
engine = RecommendationEngine(api_key="")
|
||||
result = engine.generate_json("hi")
|
||||
assert "Offline Workout" in result["workoutName"]
|
||||
|
||||
# --- FitnessTools Tests ---
|
||||
|
||||
@pytest.fixture
|
||||
def fitness_tools():
|
||||
with patch("recommendations.tools.GarminSync"), \
|
||||
patch("recommendations.tools.SettingsManager"):
|
||||
yield FitnessTools(garmin_storage="/tmp")
|
||||
|
||||
def test_tools_get_recent_activities(fitness_tools):
|
||||
mock_sync = fitness_tools.sync
|
||||
mock_sync.load_local_activities.return_value = [
|
||||
{"activityName": "Run", "startTimeLocal": "2023-01-01 10:00:00", "distance": 5000, "duration": 1800, "activityType": {"typeKey": "running"}},
|
||||
{"activityName": "Cycle", "startTimeLocal": "2023-01-02 10:00:00", "distance": 10000, "duration": 3600, "activityType": {"typeKey": "cycling"}}
|
||||
]
|
||||
|
||||
result = fitness_tools.get_recent_activities(limit=2)
|
||||
assert "Run" in result
|
||||
assert "Cycle" in result
|
||||
assert "5.0km" in result
|
||||
|
||||
def test_tools_get_weekly_stats(fitness_tools):
|
||||
mock_sync = fitness_tools.sync
|
||||
mock_sync.get_weekly_stats.return_value = {
|
||||
"labels": ["W1"],
|
||||
"datasets": [{"label": "Running", "data": [5.0]}]
|
||||
}
|
||||
|
||||
result = fitness_tools.get_weekly_stats()
|
||||
assert "W1" in result
|
||||
assert "Running: 5.0h" in result
|
||||
|
||||
def test_tools_get_user_profile(fitness_tools):
|
||||
mock_settings = fitness_tools.settings
|
||||
mock_settings.get_context_string.return_value = "Profile context"
|
||||
|
||||
result = fitness_tools.get_user_profile()
|
||||
assert result == "Profile context"
|
||||
|
||||
Reference in New Issue
Block a user