// ABOUTME: Comprehensive tests for multi-tenant MCP server functionality
// ABOUTME: Tests tenant isolation, MCP protocol handling, and server operations
//
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
//! Comprehensive tests for mcp/multitenant.rs
//!
//! This test suite aims to improve coverage from 38.56% by testing
//! all major functionalities of the multi-tenant MCP server
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(missing_docs)]
use anyhow::Result;
use pierre_mcp_server::{
database_plugins::{factory::Database, DatabaseProvider},
mcp::multitenant::{McpRequest, MultiTenantMcpServer},
models::{Tenant, User},
};
use serde_json::json;
use serial_test::serial;
use std::collections::HashMap;
use tokio::time::{sleep, Duration};
mod common;
// === Test Setup Helpers ===
async fn create_test_server() -> Result<MultiTenantMcpServer> {
let resources = common::create_test_server_resources().await?;
Ok(MultiTenantMcpServer::new(resources))
}
async fn create_test_user_with_auth(database: &Database) -> Result<(User, String)> {
let user = User::new(
"test@example.com".to_owned(),
"password123".to_owned(),
Some("Test User".to_owned()),
);
database.create_user(&user).await?;
let auth_manager = common::create_test_auth_manager();
let jwks_manager = common::get_shared_test_jwks();
let token = auth_manager.generate_token(&user, &jwks_manager)?;
Ok((user, token))
}
// === Core Server Tests ===
#[tokio::test]
async fn test_multitenant_server_creation() -> Result<()> {
let _server = create_test_server().await?;
// Server should be created successfully without panic
Ok(())
}
#[tokio::test]
async fn test_server_public_methods() -> Result<()> {
let server = create_test_server().await?;
// Test public getter methods
let _database = server.database();
let _auth_manager = server.auth_manager();
Ok(())
}
// === MCP Protocol Tests ===
#[tokio::test]
async fn test_mcp_initialize_request() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "initialize".to_owned(),
params: None,
id: Some(json!(1)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully depending on implementation
Ok(())
}
#[tokio::test]
async fn test_mcp_ping_request() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "ping".to_owned(),
params: None,
id: Some(json!(2)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully depending on implementation
Ok(())
}
#[tokio::test]
async fn test_mcp_tools_list_request() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/list".to_owned(),
params: None,
id: Some(json!(3)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully depending on implementation
Ok(())
}
#[tokio::test]
async fn test_mcp_authenticate_request() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create user with known credentials
let user = User::new(
"auth_test@example.com".to_owned(),
bcrypt::hash("test_password", 4)?,
Some("Auth Test User".to_owned()),
);
resources.database.create_user(&user).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "authenticate".to_owned(),
params: Some(json!({
"email": "auth_test@example.com",
"password": "test_password"
})),
id: Some(json!(4)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully depending on implementation
Ok(())
}
#[tokio::test]
async fn test_unknown_method_handling() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "unknown_method".to_owned(),
params: None,
id: Some(json!(5)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
let error = response.error.unwrap();
assert_eq!(error.code, -32601); // METHOD_NOT_FOUND
assert!(error.message.contains("Unknown method"));
Ok(())
}
// === Authentication Tests ===
#[tokio::test]
async fn test_authenticate_method_with_invalid_params() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "authenticate".to_owned(),
params: Some(json!({"invalid_field": "invalid_value"})),
id: Some(json!(6)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
let error = response.error.unwrap();
assert!(error.message.contains("Invalid authentication parameters"));
Ok(())
}
// === Tool Call Tests ===
#[tokio::test]
async fn test_tools_call_without_authentication() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_activities",
"arguments": {
"provider": "strava",
"limit": 10
}
})),
id: Some(json!(7)),
auth_token: None,
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
Ok(())
}
#[tokio::test]
async fn test_tools_call_with_invalid_token() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_activities",
"arguments": {
"provider": "strava",
"limit": 10
}
})),
id: Some(json!(8)),
auth_token: Some("Bearer invalid_token_123".to_owned()),
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
Ok(())
}
#[tokio::test]
async fn test_tools_call_with_valid_authentication() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "connect_strava",
"arguments": {}
})),
id: Some(json!(9)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully (not with authentication error)
Ok(())
}
#[tokio::test]
async fn test_tools_call_with_missing_params() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
// Test request with missing params
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: None, // Missing params
id: Some(json!(10)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.error.is_some());
Ok(())
}
// === Provider Connection Tests ===
#[tokio::test]
async fn test_connect_strava_tool() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "connect_strava",
"arguments": {}
})),
id: Some(json!(11)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully (OAuth might not be configured in test)
Ok(())
}
#[tokio::test]
async fn test_connect_fitbit_tool() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "connect_fitbit",
"arguments": {}
})),
id: Some(json!(12)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully (OAuth might not be configured in test)
Ok(())
}
#[tokio::test]
async fn test_get_connection_status_tool() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_connection_status",
"arguments": {}
})),
id: Some(json!(13)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully
Ok(())
}
#[tokio::test]
async fn test_disconnect_provider_tool() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "disconnect_provider",
"arguments": {
"provider": "strava"
}
})),
id: Some(json!(14)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully depending on implementation
Ok(())
}
// === Provider-Specific Tool Tests ===
#[tokio::test]
async fn test_provider_tools_without_connection() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
// Test provider-specific tools that require connection
let provider_tools = [
("get_activities", "strava"),
("get_athlete_profile", "strava"),
("get_profile", "fitbit"),
];
for (i, (tool_name, provider)) in provider_tools.iter().enumerate() {
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": tool_name,
"arguments": {
"provider": provider
}
})),
id: Some(json!(15 + i)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either fail gracefully or succeed
}
Ok(())
}
// === Intelligence Tool Tests ===
#[tokio::test]
async fn test_intelligence_tools() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
// Test intelligence tools that don't require provider
let tools = [
"analyze_activity",
"generate_training_plan",
"calculate_fitness_score",
"generate_insights",
];
for tool_name in &tools {
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": tool_name,
"arguments": {
"activity_data": {},
"user_preferences": {}
}
})),
id: Some(json!(20)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let _response = MultiTenantMcpServer::handle_request(request, &resources).await;
// Should either succeed or fail gracefully
}
Ok(())
}
// === Error Handling Tests ===
#[tokio::test]
async fn test_tools_call_with_whitespace_token() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_connection_status",
"arguments": {}
})),
id: Some(json!(21)),
auth_token: Some(" \t\n ".to_owned()), // Whitespace only
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
Ok(())
}
#[tokio::test]
async fn test_tools_call_malformed_token() -> Result<()> {
let resources = common::create_test_server_resources().await?;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_connection_status",
"arguments": {}
})),
id: Some(json!(22)),
auth_token: Some("Bearer malformed.token.here".to_owned()),
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.result.is_none());
assert!(response.error.is_some());
Ok(())
}
#[tokio::test]
async fn test_handle_authenticated_tool_call_edge_cases() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create authenticated user
let (_user, token) = create_test_user_with_auth(&resources.database).await?;
// Test with invalid tool name
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "nonexistent_tool",
"arguments": {}
})),
id: Some(json!(23)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
let response = MultiTenantMcpServer::handle_request(request, &resources).await;
let response = response.unwrap();
assert!(response.error.is_some());
Ok(())
}
// === Legacy provider tests removed - all provider access now requires tenant context ===
// === Concurrency Tests ===
#[tokio::test]
#[serial]
async fn test_concurrent_requests() -> Result<()> {
let resources = common::create_test_server_resources().await?;
// Create multiple users with tenants
let mut user_tokens = vec![];
for i in 0..2 {
// Reduce to 2 to avoid pool exhaustion
let mut user = User::new(
format!("concurrent_user_{i}@example.com"),
"password".to_owned(),
Some(format!("Concurrent User {i}")),
);
resources.database.create_user(&user).await?;
// Create tenant for this user
let tenant_slug = format!("concurrent-tenant-{i}");
let tenant = Tenant::new(
format!("Concurrent Tenant {i}"),
tenant_slug.clone(),
Some(format!("concurrent-{i}.example.com")),
"starter".to_owned(),
user.id,
);
resources.database.create_tenant(&tenant).await?;
// Link user to tenant
resources
.database
.update_user_tenant_id(user.id, &tenant_slug)
.await?;
user.tenant_id = Some(tenant_slug);
let token = resources
.auth_manager
.generate_token(&user, &resources.jwks_manager)?;
user_tokens.push((user, token));
}
// Send concurrent requests with staggered timing
let mut handles = vec![];
for (i, (_user, token)) in user_tokens.into_iter().enumerate() {
let resources_clone = resources.clone();
handles.push(tokio::spawn(async move {
// Add small delay to stagger requests
sleep(Duration::from_millis(i as u64 * 10)).await;
let request = McpRequest {
jsonrpc: "2.0".to_owned(),
method: "tools/call".to_owned(),
params: Some(json!({
"name": "get_connection_status",
"arguments": {}
})),
id: Some(json!(100 + i)),
auth_token: Some(format!("Bearer {token}")),
headers: None,
metadata: HashMap::new(),
};
MultiTenantMcpServer::handle_request(request, &resources_clone).await
}));
}
// All requests should complete successfully
for handle in handles {
let response = handle.await?;
if let Some(response) = response {
assert!(response.result.is_some());
}
}
Ok(())
}