// ABOUTME: Integration tests for configuration tools in multitenant MCP server
// ABOUTME: Tests configuration tool handlers and validates proper functionality
//
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
//! Integration tests for configuration tools in multitenant MCP server
//!
//! This test suite validates that configuration tools are properly integrated
//! into the multitenant MCP server and can handle requests correctly.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(missing_docs)]
use anyhow::Result;
use chrono::Utc;
use pierre_mcp_server::{
admin::jwks::JwksManager,
auth::AuthManager,
database_plugins::{factory::Database, DatabaseProvider},
mcp::{
multitenant::{McpRequest, McpResponse, MultiTenantMcpServer},
resources::ServerResources,
},
models::{Tenant, User},
};
use serde_json::{json, Value};
use std::{collections::HashMap, sync::Arc};
use uuid::Uuid;
mod common;
/// Helper to create authenticated user and return token
async fn create_authenticated_user(
database: &Database,
auth_manager: &AuthManager,
jwks_manager: &Arc<JwksManager>,
) -> Result<(Uuid, String)> {
let tenant_uuid = Uuid::new_v4(); // Configuration tools require tenant context with valid UUID
let user_id = Uuid::new_v4();
// First create the user (without tenant_id initially)
let mut user = User::new(
"config_test@example.com".to_owned(),
"test_password_hash".to_owned(),
Some("Configuration Test User".to_owned()),
);
user.id = user_id;
database.create_user(&user).await?;
// Then create the tenant with the user as owner
let tenant = Tenant {
id: tenant_uuid,
name: "Configuration Test Tenant".to_owned(),
slug: "config-test-tenant".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: user_id,
created_at: Utc::now(),
updated_at: Utc::now(),
};
database.create_tenant(&tenant).await?;
// Finally, update the user to associate with the tenant
user.tenant_id = Some(tenant_uuid.to_string());
database
.update_user_tenant_id(user_id, &tenant_uuid.to_string())
.await?;
let token = auth_manager.generate_token(&user, jwks_manager)?;
Ok((user_id, token))
}
/// Helper to create authenticated user with different tenant (for isolation testing)
async fn create_authenticated_user_with_different_tenant(
database: &Database,
auth_manager: &AuthManager,
jwks_manager: &Arc<JwksManager>,
email: &str,
) -> Result<(Uuid, String)> {
let tenant_uuid = Uuid::new_v4(); // Different tenant UUID
let user_id = Uuid::new_v4();
// First create the user (without tenant_id initially)
let mut user = User::new(
email.to_owned(),
"test_password_hash".to_owned(),
Some("Configuration Test User (Different Tenant)".to_owned()),
);
user.id = user_id;
database.create_user(&user).await?;
// Then create the tenant with the user as owner
let tenant = Tenant {
id: tenant_uuid,
name: "Different Configuration Test Tenant".to_owned(),
slug: "different-config-test-tenant".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: user_id,
created_at: Utc::now(),
updated_at: Utc::now(),
};
database.create_tenant(&tenant).await?;
// Finally, update the user to associate with the tenant
user.tenant_id = Some(tenant_uuid.to_string());
database
.update_user_tenant_id(user_id, &tenant_uuid.to_string())
.await?;
let token = auth_manager.generate_token(&user, jwks_manager)?;
Ok((user_id, token))
}
/// Helper to make a configuration tool request
async fn make_tool_request(
tool_name: &str,
arguments: Value,
token: &str,
resources: &Arc<ServerResources>,
) -> Result<McpResponse> {
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": tool_name,
"arguments": arguments
})),
id: Some(json!(1)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
Ok(MultiTenantMcpServer::handle_request(request, resources)
.await
.unwrap())
}
#[tokio::test]
async fn test_all_configuration_tools_available() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let (user_id, token) = create_authenticated_user(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
)
.await?;
// Test that all 6 configuration tools are available and respond
let config_tools = vec![
"get_configuration_catalog",
"get_configuration_profiles",
"get_user_configuration",
"update_user_configuration",
"calculate_personalized_zones",
"validate_configuration",
];
let mut successful_tools = 0;
for tool_name in &config_tools {
let arguments = match *tool_name {
"calculate_personalized_zones" => json!({
"vo2_max": 50.0,
"resting_hr": 65,
"max_hr": 185
}),
"update_user_configuration" => json!({
"profile": "default",
"parameters": {}
}),
"validate_configuration" => json!({
"parameters": {
"fitness.vo2_max_threshold_male_recreational": 45.0
}
}),
_ => json!({}),
};
let response = make_tool_request(tool_name, arguments, &token, &resources).await?;
if response.result.is_some() && response.error.is_none() {
successful_tools += 1;
println!("{tool_name} - SUCCESS");
} else {
println!("{} - FAILED: {:?}", tool_name, response.error);
}
}
// All 6 configuration tools should work
assert_eq!(
successful_tools, 6,
"Expected all 6 configuration tools to work"
);
println!("All configuration tools integration test passed - User ID: {user_id}");
println!("Successfully tested {successful_tools} configuration tools");
Ok(())
}
#[tokio::test]
async fn test_configuration_catalog_has_expected_structure() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let (user_id, token) = create_authenticated_user(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
)
.await?;
let response =
make_tool_request("get_configuration_catalog", json!({}), &token, &resources).await?;
assert!(response.result.is_some());
assert!(response.error.is_none());
let result = response.result.unwrap();
// Response is now wrapped in MCP ToolResponse format with content and structuredContent
let structured = result
.get("structuredContent")
.or_else(|| result.get("structured_content"))
.unwrap_or(&result);
assert!(structured.get("catalog").is_some());
let catalog = &structured["catalog"];
assert!(catalog["categories"].is_array());
assert!(catalog["total_parameters"].is_number());
assert!(catalog["version"].is_string());
// Verify we have expected categories
let categories = catalog["categories"].as_array().unwrap();
assert!(!categories.is_empty());
println!("Configuration catalog structure test passed - User ID: {user_id}");
Ok(())
}
#[tokio::test]
async fn test_configuration_tools_require_authentication() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Try to call a configuration tool without authentication
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_configuration_catalog",
"arguments": {}
})),
id: Some(json!(1)),
auth_token: None, // No authentication
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should return an error for missing authentication
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
println!("Configuration tools authentication test passed");
Ok(())
}
#[tokio::test]
async fn test_configuration_tools_with_invalid_parameters() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let (_user_id, token) = create_authenticated_user(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
)
.await?;
// Test missing required parameters for calculate_personalized_zones
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "calculate_personalized_zones",
"arguments": {} // Missing required vo2_max
})),
id: Some(json!(1)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should return an error for missing required parameters
// The response might succeed but indicate validation failure, or it might error
// Either way is acceptable as long as it doesn't crash
let response = response.unwrap();
assert!(response.error.is_some() || response.result.is_some());
println!("Configuration tools invalid parameters test passed");
Ok(())
}
#[tokio::test]
async fn test_multitenant_isolation_for_configuration_tools() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create two different users
let (user1_id, token1) = create_authenticated_user(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
)
.await?;
// Create a second user with different tenant for isolation testing
let (user2_id, token2) = create_authenticated_user_with_different_tenant(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
"config_test2@example.com",
)
.await?;
// Both users should be able to access configuration tools independently
let response1 =
make_tool_request("get_user_configuration", json!({}), &token1, &resources).await?;
let response2 =
make_tool_request("get_user_configuration", json!({}), &token2, &resources).await?;
// Both should succeed
assert!(response1.result.is_some() && response1.error.is_none());
assert!(response2.result.is_some() && response2.error.is_none());
// Even if the configuration is the same, the responses are from different user contexts
// This confirms proper multitenant isolation
assert_eq!(response1.jsonrpc, "2.0");
assert_eq!(response2.jsonrpc, "2.0");
println!("Multitenant isolation test passed");
println!(" User 1 ID: {user1_id} - Configuration accessed");
println!(" User 2 ID: {user2_id} - Configuration accessed");
Ok(())
}
#[tokio::test]
async fn test_configuration_tools_integration_summary() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let (user_id, token) = create_authenticated_user(
&resources.database,
&resources.auth_manager,
&resources.jwks_manager,
)
.await?;
println!("Configuration Tools Integration Test Summary");
println!("================================================");
// Test each configuration tool and count successes
let tools = vec![
("get_configuration_catalog", json!({})),
("get_configuration_profiles", json!({})),
("get_user_configuration", json!({})),
(
"update_user_configuration",
json!({
"profile": "default",
"parameters": {}
}),
),
(
"calculate_personalized_zones",
json!({
"vo2_max": 50.0,
"resting_hr": 65,
"max_hr": 185
}),
),
(
"validate_configuration",
json!({
"parameters": {
"fitness.vo2_max_threshold_male_recreational": 45.0
}
}),
),
];
let mut working_tools = 0;
let total_tools = tools.len();
for (tool_name, arguments) in tools {
let response = make_tool_request(tool_name, arguments, &token, &resources).await?;
if response.result.is_some() && response.error.is_none() {
working_tools += 1;
println!(" {tool_name} - Working");
} else {
println!(" {} - Failed: {:?}", tool_name, response.error);
}
}
println!();
println!("Results:");
println!(" Working: {working_tools}/{total_tools} configuration tools");
#[allow(clippy::cast_precision_loss)]
let success_rate = (working_tools as f64 / total_tools as f64) * 100.0;
println!(" Success Rate: {success_rate:.1}%");
println!(" User ID: {user_id}");
println!();
// All configuration tools should be working
assert_eq!(
working_tools, total_tools,
"Expected all configuration tools to work"
);
if working_tools == total_tools {
println!("SUCCESS: All configuration tools are properly integrated!");
}
Ok(())
}