// ABOUTME: MCP per-tool integration tests
// ABOUTME: Tests individual tool execution via real HTTP transport
//
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(missing_docs)]
mod common;
mod integration;
use anyhow::Result;
use integration::{IntegrationTestServer, McpTestClient};
use serde_json::json;
// ============================================================================
// TEST HELPERS
// ============================================================================
async fn setup_test_client() -> Result<(IntegrationTestServer, McpTestClient)> {
let mut server = IntegrationTestServer::new().await?;
server.start().await?;
let (_user_id, jwt_token) = server.create_test_user("tools@test.local").await?;
let client = McpTestClient::new(&server.base_url(), &jwt_token);
Ok((server, client))
}
/// Call a tool and return a summary of the result
fn summarize_result(result: &integration::infrastructure::mcp_client::ToolCallResult) -> String {
let text = result
.content
.first()
.and_then(|c| c.text.as_ref())
.cloned()
.unwrap_or_else(|| "No text".to_owned());
let prefix = if result.is_error { "ERROR" } else { "OK" };
format!("{}: {}", prefix, &text[..text.len().min(80)])
}
// ============================================================================
// CORE TOOLS: Provider Connection and Data Retrieval
// ============================================================================
/// Test: `get_connection_status` returns provider status
#[tokio::test]
async fn test_tool_get_connection_status() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_connection_status", json!({"provider": "strava"}))
.await?;
// Should return some response (not panic)
let text = result.content[0].text.as_ref().expect("Should have text");
assert!(!text.is_empty(), "Response should not be empty");
println!("✅ get_connection_status: {}", &text[..text.len().min(100)]);
Ok(())
}
/// Test: `connect_provider` returns OAuth URL for provider connection
#[tokio::test]
async fn test_tool_connect_provider() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("connect_provider", json!({"provider": "strava"}))
.await?;
let text = result.content[0].text.as_ref().expect("Should have text");
println!("✅ connect_provider: {}", &text[..text.len().min(100)]);
Ok(())
}
/// Test: `get_activities` retrieves activities (may fail without connected provider)
#[tokio::test]
async fn test_tool_get_activities_synthetic() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"get_activities",
json!({
"provider": "synthetic",
"limit": 5
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ get_activities: {summary}");
Ok(())
}
/// Test: `get_athlete` retrieves athlete profile
#[tokio::test]
async fn test_tool_get_athlete() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_athlete", json!({"provider": "synthetic"}))
.await?;
let summary = summarize_result(&result);
println!("✅ get_athlete: {summary}");
Ok(())
}
/// Test: `get_stats` retrieves performance statistics
#[tokio::test]
async fn test_tool_get_stats() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_stats", json!({"provider": "synthetic"}))
.await?;
let summary = summarize_result(&result);
println!("✅ get_stats: {summary}");
Ok(())
}
// ============================================================================
// INTELLIGENCE & ANALYTICS TOOLS
// ============================================================================
/// Test: `analyze_training_load` provides load analysis
#[tokio::test]
async fn test_tool_analyze_training_load() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"analyze_training_load",
json!({
"provider": "synthetic",
"days": 7
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ analyze_training_load: {summary}");
Ok(())
}
/// Test: `calculate_fitness_score` returns fitness assessment
#[tokio::test]
async fn test_tool_calculate_fitness_score() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"calculate_fitness_score",
json!({
"provider": "synthetic"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ calculate_fitness_score: {summary}");
Ok(())
}
/// Test: `generate_recommendations` provides training advice
#[tokio::test]
async fn test_tool_generate_recommendations() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"generate_recommendations",
json!({
"provider": "synthetic"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ generate_recommendations: {summary}");
Ok(())
}
/// Test: `analyze_performance_trends` shows trends over time
#[tokio::test]
async fn test_tool_analyze_performance_trends() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"analyze_performance_trends",
json!({
"provider": "synthetic",
"days": 30
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ analyze_performance_trends: {summary}");
Ok(())
}
/// Test: `detect_patterns` finds training patterns
#[tokio::test]
async fn test_tool_detect_patterns() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"detect_patterns",
json!({
"provider": "synthetic",
"pattern_type": "training_consistency"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ detect_patterns: {summary}");
Ok(())
}
/// Test: `predict_performance` forecasts future performance
#[tokio::test]
async fn test_tool_predict_performance() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"predict_performance",
json!({
"provider": "synthetic",
"target_distance_km": 10.0,
"target_date": "2025-03-01"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ predict_performance: {summary}");
Ok(())
}
// ============================================================================
// GOAL MANAGEMENT TOOLS
// ============================================================================
/// Test: `set_goal` creates a new fitness goal
#[tokio::test]
async fn test_tool_set_goal() -> Result<()> {
let (_server, client) = setup_test_client().await?;
// Use timeframe instead of target_date to match SetGoalParams struct
let result = client
.call_tool_raw(
"set_goal",
json!({
"goal_type": "distance",
"target_value": 100.0,
"timeframe": "month"
}),
)
.await;
match result {
Ok(r) => println!("✅ set_goal: {}", summarize_result(&r)),
Err(e) => println!(
"✅ set_goal (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
Ok(())
}
/// Test: `suggest_goals` provides AI-generated goal suggestions
#[tokio::test]
async fn test_tool_suggest_goals() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"suggest_goals",
json!({
"provider": "synthetic"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ suggest_goals: {summary}");
Ok(())
}
/// Test: `track_progress` shows goal progress
#[tokio::test]
async fn test_tool_track_progress() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"track_progress",
json!({
"goal_id": "latest"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ track_progress: {summary}");
Ok(())
}
// ============================================================================
// CONFIGURATION TOOLS
// ============================================================================
/// Test: `get_configuration_catalog` returns all config parameters
#[tokio::test]
async fn test_tool_get_configuration_catalog() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_configuration_catalog", json!({}))
.await?;
assert!(!result.is_error, "get_configuration_catalog should succeed");
let text = result.content[0].text.as_ref().expect("Should have text");
assert!(text.contains("catalog") || text.contains("config") || text.contains('{'));
println!("✅ get_configuration_catalog: response received");
Ok(())
}
/// Test: `get_configuration_profiles` returns available profiles
#[tokio::test]
async fn test_tool_get_configuration_profiles() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_configuration_profiles", json!({}))
.await?;
assert!(
!result.is_error,
"get_configuration_profiles should succeed"
);
println!("✅ get_configuration_profiles: response received");
Ok(())
}
/// Test: `get_user_configuration` returns user-specific config
#[tokio::test]
async fn test_tool_get_user_configuration() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("get_user_configuration", json!({}))
.await?;
let summary = summarize_result(&result);
println!("✅ get_user_configuration: {summary}");
Ok(())
}
/// Test: `calculate_personalized_zones` returns training zones
#[tokio::test]
async fn test_tool_calculate_personalized_zones() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"calculate_personalized_zones",
json!({
"vo2_max": 50.0
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ calculate_personalized_zones: {summary}");
Ok(())
}
// ============================================================================
// SLEEP & RECOVERY TOOLS
// ============================================================================
/// Test: `analyze_sleep_quality` analyzes sleep data
#[tokio::test]
async fn test_tool_analyze_sleep_quality() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"analyze_sleep_quality",
json!({
"provider": "synthetic",
"days": 7
}),
)
.await;
match result {
Ok(r) => println!("✅ analyze_sleep_quality: {}", summarize_result(&r)),
Err(e) => println!(
"✅ analyze_sleep_quality (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
Ok(())
}
/// Test: `calculate_recovery_score` computes recovery metrics
#[tokio::test]
async fn test_tool_calculate_recovery_score() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"calculate_recovery_score",
json!({
"provider": "synthetic"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ calculate_recovery_score: {summary}");
Ok(())
}
/// Test: `suggest_rest_day` recommends rest days
#[tokio::test]
async fn test_tool_suggest_rest_day() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"suggest_rest_day",
json!({
"provider": "synthetic"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ suggest_rest_day: {summary}");
Ok(())
}
// ============================================================================
// NUTRITION TOOLS
// ============================================================================
/// Test: `calculate_daily_nutrition` computes daily macro needs
#[tokio::test]
async fn test_tool_calculate_daily_nutrition() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"calculate_daily_nutrition",
json!({
"weight_kg": 70.0,
"height_cm": 175.0,
"age": 30,
"gender": "male",
"activity_level": "moderate",
"goal": "maintenance"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ calculate_daily_nutrition: {summary}");
Ok(())
}
/// Test: `get_nutrient_timing` provides workout nutrition timing
#[tokio::test]
async fn test_tool_get_nutrient_timing() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"get_nutrient_timing",
json!({
"workout_type": "endurance",
"intensity": "moderate",
"duration_minutes": 60,
"weight_kg": 70.0
}),
)
.await;
match result {
Ok(r) => println!("✅ get_nutrient_timing: {}", summarize_result(&r)),
Err(e) => println!(
"✅ get_nutrient_timing (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
Ok(())
}
/// Test: `search_food` searches USDA database
#[tokio::test]
async fn test_tool_search_food() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"search_food",
json!({
"query": "banana",
"limit": 5
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ search_food: {summary}");
Ok(())
}
// ============================================================================
// RECIPE MANAGEMENT TOOLS
// ============================================================================
/// Test: `get_recipe_constraints` returns macro targets for recipes
#[tokio::test]
async fn test_tool_get_recipe_constraints() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"get_recipe_constraints",
json!({
"training_phase": "build"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ get_recipe_constraints: {summary}");
Ok(())
}
/// Test: `list_recipes` returns user's recipe collection
#[tokio::test]
async fn test_tool_list_recipes() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client.call_tool_raw("list_recipes", json!({})).await?;
let summary = summarize_result(&result);
println!("✅ list_recipes: {summary}");
Ok(())
}
/// Test: `save_recipe` saves a recipe
#[tokio::test]
async fn test_tool_recipe_save_and_get() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let save_result = client
.call_tool_raw(
"save_recipe",
json!({
"name": "Test Recovery Smoothie",
"description": "Post-workout recovery drink",
"meal_timing": "post_workout",
"ingredients": [
{"food_name": "banana", "amount_grams": 120}
],
"instructions": "Blend until smooth"
}),
)
.await;
match save_result {
Ok(r) => println!("✅ save_recipe: {}", summarize_result(&r)),
Err(e) => println!(
"✅ save_recipe (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
Ok(())
}
/// Test: `search_recipes` finds recipes by criteria
#[tokio::test]
async fn test_tool_search_recipes() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"search_recipes",
json!({
"query": "smoothie"
}),
)
.await?;
let summary = summarize_result(&result);
println!("✅ search_recipes: {summary}");
Ok(())
}
// ============================================================================
// FITNESS CONFIG TOOLS
// ============================================================================
/// Test: fitness config CRUD operations
#[tokio::test]
async fn test_tool_fitness_config_crud() -> Result<()> {
let (_server, client) = setup_test_client().await?;
// Set a config
let set_result = client
.call_tool_raw(
"set_fitness_config",
json!({
"configuration_name": "test_zones",
"configuration": {
"heart_rate_zones": {
"zone1_max": 120,
"zone2_max": 140,
"zone3_max": 160,
"zone4_max": 175,
"zone5_max": 190
}
}
}),
)
.await;
match set_result {
Ok(r) => println!("✅ set_fitness_config: {}", summarize_result(&r)),
Err(e) => println!(
"✅ set_fitness_config (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
// List configs
let list_result = client
.call_tool_raw("list_fitness_configs", json!({}))
.await;
match list_result {
Ok(r) => println!("✅ list_fitness_configs: {}", summarize_result(&r)),
Err(e) => println!(
"✅ list_fitness_configs (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
// Get specific config
let get_result = client
.call_tool_raw(
"get_fitness_config",
json!({
"configuration_name": "test_zones"
}),
)
.await;
match get_result {
Ok(r) => println!("✅ get_fitness_config: {}", summarize_result(&r)),
Err(e) => println!(
"✅ get_fitness_config (error handled): {}",
e.to_string().chars().take(60).collect::<String>()
),
}
Ok(())
}
// ============================================================================
// PARAMETER VALIDATION TESTS
// ============================================================================
/// Test: Tools handle missing required parameters gracefully
#[tokio::test]
async fn test_tool_missing_required_params() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw("calculate_daily_nutrition", json!({}))
.await;
match result {
Ok(r) => {
let summary = summarize_result(&r);
println!("✅ Missing params handled: {summary}");
}
Err(e) => {
println!(
"✅ Missing params rejected: {}",
e.to_string().chars().take(60).collect::<String>()
);
}
}
Ok(())
}
/// Test: Tools handle invalid parameter values gracefully
#[tokio::test]
async fn test_tool_invalid_param_values() -> Result<()> {
let (_server, client) = setup_test_client().await?;
let result = client
.call_tool_raw(
"calculate_daily_nutrition",
json!({
"weight_kg": -10.0,
"height_cm": 175.0,
"age": 30,
"gender": "male",
"activity_level": "moderate",
"goal": "maintenance"
}),
)
.await;
match result {
Ok(r) => {
let summary = summarize_result(&r);
println!("✅ Invalid param handled: {summary}");
}
Err(e) => {
println!(
"✅ Invalid param rejected: {}",
e.to_string().chars().take(60).collect::<String>()
);
}
}
Ok(())
}