use anyhow::Result;
use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder, rpc_params};
use std::time::Duration;
use tokio::time::timeout;
#[tokio::test]
async fn test_health_check_endpoint() -> Result<()> {
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let response: context_creator::mcp_server::HealthResponse =
client.request("health_check", rpc_params![]).await?;
assert_eq!(response.status, "healthy");
assert!(response.timestamp > 0);
assert!(!response.version.is_empty());
Ok(())
}
#[tokio::test]
async fn test_health_check_performance() -> Result<()> {
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let response_future = client
.request::<context_creator::mcp_server::HealthResponse, _>("health_check", rpc_params![]);
let response = timeout(Duration::from_millis(100), response_future).await??;
assert_eq!(response.status, "healthy");
Ok(())
}
#[tokio::test]
async fn test_graceful_shutdown() -> Result<()> {
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
server_handle.stop()?;
tokio::time::sleep(Duration::from_millis(100)).await;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let result = client
.request::<context_creator::mcp_server::HealthResponse, _>("health_check", rpc_params![])
.await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_port_already_in_use() -> Result<()> {
let server1 = start_test_server("127.0.0.1:0").await?;
let addr = server1.local_addr()?;
let result = start_test_server(&addr.to_string()).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_process_local_codebase_handler() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("main.rs"),
r#"fn main() {
println!("Hello, world!");
}"#,
)?;
std::fs::write(
temp_dir.path().join("lib.rs"),
r#"pub fn add(a: i32, b: i32) -> i32 {
a + b
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct ProcessLocalRequest {
path: PathBuf,
include_patterns: Vec<String>,
ignore_patterns: Vec<String>,
include_imports: bool,
max_tokens: Option<u32>,
}
let request = ProcessLocalRequest {
path: temp_dir.path().to_path_buf(),
include_patterns: vec!["*.rs".to_string()],
ignore_patterns: vec![],
include_imports: false,
max_tokens: Some(10000),
};
let response: serde_json::Value = client
.request("process_local_codebase", rpc_params![request])
.await?;
assert!(response.get("markdown").is_some());
assert!(response.get("file_count").is_some());
assert!(response.get("token_count").is_some());
assert!(response.get("processing_time_ms").is_some());
let markdown = response["markdown"].as_str().unwrap();
assert!(markdown.contains("main.rs"));
assert!(markdown.contains("lib.rs"));
assert!(markdown.contains("Hello, world!"));
assert!(markdown.contains("pub fn add"));
Ok(())
}
#[tokio::test]
async fn test_process_local_codebase_path_traversal_rejected() -> Result<()> {
use std::path::PathBuf;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct ProcessLocalRequest {
path: PathBuf,
include_patterns: Vec<String>,
ignore_patterns: Vec<String>,
include_imports: bool,
max_tokens: Option<u32>,
}
let request = ProcessLocalRequest {
path: PathBuf::from("../../../etc/passwd"),
include_patterns: vec![],
ignore_patterns: vec![],
include_imports: false,
max_tokens: None,
};
let result: Result<serde_json::Value, _> = client
.request("process_local_codebase", rpc_params![request])
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid path") || err.to_string().contains("security"));
Ok(())
}
#[tokio::test]
async fn test_process_local_codebase_caching() -> Result<()> {
use std::path::PathBuf;
use std::time::Instant;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let content = "fn main() {\n println!(\"Hello, world!\");\n}\n".repeat(1000);
std::fs::write(temp_dir.path().join("main.rs"), &content)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct ProcessLocalRequest {
path: PathBuf,
include_patterns: Vec<String>,
ignore_patterns: Vec<String>,
include_imports: bool,
max_tokens: Option<u32>,
}
let request = ProcessLocalRequest {
path: temp_dir.path().to_path_buf(),
include_patterns: vec!["*.rs".to_string()],
ignore_patterns: vec![],
include_imports: false,
max_tokens: None,
};
let start1 = Instant::now();
let response1: serde_json::Value = client
.request("process_local_codebase", rpc_params![&request])
.await?;
let time1 = start1.elapsed();
let start2 = Instant::now();
let response2: serde_json::Value = client
.request("process_local_codebase", rpc_params![&request])
.await?;
let time2 = start2.elapsed();
assert!(
time2 < time1 / 2,
"Second request should be at least 2x faster due to caching: {time1:?} vs {time2:?}"
);
assert_eq!(response1["markdown"], response2["markdown"]);
assert_eq!(response1["file_count"], response2["file_count"]);
assert_eq!(response1["token_count"], response2["token_count"]);
Ok(())
}
#[tokio::test]
async fn test_process_remote_repo_handler() -> Result<()> {
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct ProcessRemoteRequest {
repo_url: String,
include_patterns: Vec<String>,
ignore_patterns: Vec<String>,
include_imports: bool,
max_tokens: Option<u32>,
}
let request = ProcessRemoteRequest {
repo_url: "https://github.com/octocat/Hello-World".to_string(),
include_patterns: vec!["*".to_string()],
ignore_patterns: vec![],
include_imports: false,
max_tokens: Some(10000),
};
let response: serde_json::Value = client
.request("process_remote_repo", rpc_params![request])
.await?;
assert!(response.get("markdown").is_some());
assert!(response.get("file_count").is_some());
assert!(response.get("token_count").is_some());
assert!(response.get("processing_time_ms").is_some());
assert!(response.get("repo_name").is_some());
let markdown = response["markdown"].as_str().unwrap();
assert!(!markdown.is_empty());
assert!(response["file_count"].as_u64().unwrap() > 0);
Ok(())
}
#[tokio::test]
async fn test_process_remote_repo_invalid_url() -> Result<()> {
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct ProcessRemoteRequest {
repo_url: String,
include_patterns: Vec<String>,
ignore_patterns: Vec<String>,
include_imports: bool,
max_tokens: Option<u32>,
}
let request = ProcessRemoteRequest {
repo_url: "not-a-valid-url".to_string(),
include_patterns: vec![],
ignore_patterns: vec![],
include_imports: false,
max_tokens: None,
};
let result: Result<serde_json::Value, _> = client
.request("process_remote_repo", rpc_params![request])
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid URL") || err.to_string().contains("validation"));
Ok(())
}
#[tokio::test]
async fn test_get_file_metadata_handler() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("test.rs");
std::fs::write(&test_file, "fn main() { println!(\"Hello!\"); }")?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct GetFileMetadataRequest {
file_path: PathBuf,
}
let request = GetFileMetadataRequest {
file_path: test_file.clone(),
};
let response: serde_json::Value = client
.request("get_file_metadata", rpc_params![request])
.await?;
assert_eq!(response["path"], test_file.to_string_lossy().as_ref());
assert!(response["size"].as_u64().unwrap() > 0);
assert!(response["modified"].as_u64().unwrap() > 0);
assert_eq!(response["is_symlink"], false);
assert_eq!(response["language"].as_str(), Some("rust"));
Ok(())
}
#[tokio::test]
async fn test_get_file_metadata_non_existent() -> Result<()> {
use std::path::PathBuf;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct GetFileMetadataRequest {
file_path: PathBuf,
}
let request = GetFileMetadataRequest {
file_path: PathBuf::from("/non/existent/file.rs"),
};
let result: Result<serde_json::Value, _> = client
.request("get_file_metadata", rpc_params![request])
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("not found") || err.to_string().contains("does not exist"));
Ok(())
}
#[tokio::test]
async fn test_get_file_metadata_symlink() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("target.py");
std::fs::write(&test_file, "print('Hello from Python')")?;
let symlink = temp_dir.path().join("link.py");
#[cfg(unix)]
std::os::unix::fs::symlink(&test_file, &symlink)?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(&test_file, &symlink)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct GetFileMetadataRequest {
file_path: PathBuf,
}
let request = GetFileMetadataRequest {
file_path: symlink.clone(),
};
let response: serde_json::Value = client
.request("get_file_metadata", rpc_params![request])
.await?;
assert_eq!(response["path"], symlink.to_string_lossy().as_ref());
assert_eq!(response["is_symlink"], true);
assert_eq!(response["language"].as_str(), Some("python"));
Ok(())
}
#[tokio::test]
async fn test_search_codebase_handler() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("main.rs"),
r#"fn main() {
println!("Hello, world!");
let important_value = 42;
process_data(important_value);
}
fn process_data(value: i32) {
println!("Processing value: {}", value);
}"#,
)?;
std::fs::write(
temp_dir.path().join("lib.rs"),
r#"pub fn important_function() {
// This is an important function
let result = calculate_important_stuff();
println!("Important result: {}", result);
}
fn calculate_important_stuff() -> i32 {
42
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct SearchCodebaseRequest {
path: PathBuf,
query: String,
max_results: Option<u32>,
file_pattern: Option<String>,
}
let request = SearchCodebaseRequest {
path: temp_dir.path().to_path_buf(),
query: "important".to_string(),
max_results: Some(10),
file_pattern: Some("*.rs".to_string()),
};
let response: serde_json::Value = client
.request("search_codebase", rpc_params![request])
.await?;
assert!(response["results"].is_array());
let results = response["results"].as_array().unwrap();
assert!(!results.is_empty());
assert!(response["total_matches"].as_u64().unwrap() >= 3); assert_eq!(response["files_searched"].as_u64().unwrap(), 2);
assert!(response["search_time_ms"].is_number());
let first_result = &results[0];
assert!(first_result["file_path"].is_string());
assert!(first_result["line_number"].is_number());
assert!(first_result["line_content"].is_string());
assert!(first_result["match_context"].is_string());
Ok(())
}
#[tokio::test]
async fn test_search_codebase_no_results() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("test.txt"),
"This is just a test file with no special content",
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct SearchCodebaseRequest {
path: PathBuf,
query: String,
max_results: Option<u32>,
file_pattern: Option<String>,
}
let request = SearchCodebaseRequest {
path: temp_dir.path().to_path_buf(),
query: "nonexistentstring123".to_string(),
max_results: None,
file_pattern: None,
};
let response: serde_json::Value = client
.request("search_codebase", rpc_params![request])
.await?;
assert!(response["results"].as_array().unwrap().is_empty());
assert_eq!(response["total_matches"].as_u64().unwrap(), 0);
assert!(response["files_searched"].as_u64().unwrap() > 0);
Ok(())
}
#[tokio::test]
async fn test_search_codebase_with_pattern() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(temp_dir.path().join("code.rs"), "fn test() { /* test */ }")?;
std::fs::write(temp_dir.path().join("doc.md"), "# Test documentation")?;
std::fs::write(temp_dir.path().join("config.toml"), "test = true")?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct SearchCodebaseRequest {
path: PathBuf,
query: String,
max_results: Option<u32>,
file_pattern: Option<String>,
}
let request = SearchCodebaseRequest {
path: temp_dir.path().to_path_buf(),
query: "test".to_string(),
max_results: None,
file_pattern: Some("*.rs".to_string()),
};
let response: serde_json::Value = client
.request("search_codebase", rpc_params![request])
.await?;
let results = response["results"].as_array().unwrap();
for result in results {
assert!(result["file_path"].as_str().unwrap().ends_with(".rs"));
}
Ok(())
}
#[tokio::test]
async fn test_diff_files_handler() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let file1_path = temp_dir.path().join("file1.rs");
let file2_path = temp_dir.path().join("file2.rs");
std::fs::write(
&file1_path,
r#"fn main() {
println!("Hello, world!");
}
fn helper() {
println!("Helper function");
}"#,
)?;
std::fs::write(
&file2_path,
r#"fn main() {
println!("Hello, Rust!");
println!("Welcome!");
}
fn helper() {
println!("Helper function");
}
fn new_function() {
println!("New function");
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct DiffFilesRequest {
file1_path: PathBuf,
file2_path: PathBuf,
context_lines: Option<u32>,
}
let request = DiffFilesRequest {
file1_path: file1_path.clone(),
file2_path: file2_path.clone(),
context_lines: Some(3),
};
let response: serde_json::Value = client.request("diff_files", rpc_params![request]).await?;
assert_eq!(
response["file1_path"],
file1_path.to_string_lossy().as_ref()
);
assert_eq!(
response["file2_path"],
file2_path.to_string_lossy().as_ref()
);
assert!(response["hunks"].is_array());
assert!(!response["hunks"].as_array().unwrap().is_empty());
assert!(response["added_lines"].as_u64().unwrap() > 0);
assert!(response["removed_lines"].as_u64().unwrap() > 0);
assert_eq!(response["is_binary"], false);
Ok(())
}
#[tokio::test]
async fn test_diff_files_identical() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let content = "fn main() { println!(\"Same content\"); }";
let file1_path = temp_dir.path().join("same1.rs");
let file2_path = temp_dir.path().join("same2.rs");
std::fs::write(&file1_path, content)?;
std::fs::write(&file2_path, content)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct DiffFilesRequest {
file1_path: PathBuf,
file2_path: PathBuf,
context_lines: Option<u32>,
}
let request = DiffFilesRequest {
file1_path,
file2_path,
context_lines: None,
};
let response: serde_json::Value = client.request("diff_files", rpc_params![request]).await?;
assert!(response["hunks"].as_array().unwrap().is_empty());
assert_eq!(response["added_lines"].as_u64().unwrap(), 0);
assert_eq!(response["removed_lines"].as_u64().unwrap(), 0);
Ok(())
}
#[tokio::test]
async fn test_diff_files_binary() -> Result<()> {
use std::path::PathBuf;
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
let file1_path = temp_dir.path().join("binary1.bin");
let file2_path = temp_dir.path().join("binary2.bin");
std::fs::write(&file1_path, [0u8, 1, 2, 3, 255, 254, 253])?;
std::fs::write(&file2_path, [0u8, 1, 2, 4, 255, 254, 252])?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
#[derive(serde::Serialize)]
struct DiffFilesRequest {
file1_path: PathBuf,
file2_path: PathBuf,
context_lines: Option<u32>,
}
let request = DiffFilesRequest {
file1_path,
file2_path,
context_lines: None,
};
let response: serde_json::Value = client.request("diff_files", rpc_params![request]).await?;
assert_eq!(response["is_binary"], true);
assert!(response["hunks"].as_array().unwrap().is_empty());
Ok(())
}
#[tokio::test]
async fn test_semantic_search_functions() -> Result<()> {
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("main.rs"),
r#"fn main() {
calculate_total(10, 20);
}
fn calculate_total(a: i32, b: i32) -> i32 {
add_numbers(a, b)
}
fn add_numbers(x: i32, y: i32) -> i32 {
x + y
}"#,
)?;
std::fs::write(
temp_dir.path().join("lib.rs"),
r#"pub fn calculate_average(values: &[f64]) -> f64 {
let sum: f64 = values.iter().sum();
sum / values.len() as f64
}
pub fn calculate_median(values: &mut [f64]) -> f64 {
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mid = values.len() / 2;
values[mid]
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let response: serde_json::Value = client
.request(
"semantic_search",
rpc_params![serde_json::json!({
"path": temp_dir.path(),
"query": "calculate",
"search_type": "functions",
"max_results": 10
})],
)
.await?;
assert!(response["results"].is_array());
let results = response["results"].as_array().unwrap();
assert!(results.len() >= 3);
for result in results {
assert!(result["symbol_name"]
.as_str()
.unwrap()
.contains("calculate"));
assert_eq!(result["symbol_type"].as_str().unwrap(), "function");
assert!(result["file_path"].is_string());
assert!(result["line_number"].is_number());
assert!(result["context"].is_string());
}
Ok(())
}
#[tokio::test]
async fn test_semantic_search_types() -> Result<()> {
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("types.rs"),
r#"struct UserData {
name: String,
age: u32,
}
pub struct UserProfile {
data: UserData,
preferences: UserPreferences,
}
struct UserPreferences {
theme: String,
notifications: bool,
}
enum UserRole {
Admin,
User,
Guest,
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let response: serde_json::Value = client
.request(
"semantic_search",
rpc_params![serde_json::json!({
"path": temp_dir.path(),
"query": "User",
"search_type": "types",
"max_results": null
})],
)
.await?;
let results = response["results"].as_array().unwrap();
assert!(!results.is_empty());
for result in results {
assert!(result["symbol_name"].as_str().unwrap().contains("User"));
assert_eq!(result["symbol_type"].as_str().unwrap(), "type");
}
Ok(())
}
#[tokio::test]
async fn test_semantic_search_imports() -> Result<()> {
use tempfile::TempDir;
let server_handle = start_test_server("127.0.0.1:0").await?;
let addr = server_handle.local_addr()?;
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("main.rs"),
r#"use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Serialize, Deserialize};
use tokio::fs;
mod utils;
use utils::helpers::process_data;
fn main() {
let map = HashMap::new();
}"#,
)?;
let client = HttpClientBuilder::default().build(format!("http://{addr}"))?;
let response: serde_json::Value = client
.request(
"semantic_search",
rpc_params![serde_json::json!({
"path": temp_dir.path(),
"query": "std",
"search_type": "imports",
"max_results": 10
})],
)
.await?;
let results = response["results"].as_array().unwrap();
assert!(results.len() >= 2);
for result in results {
assert!(result["symbol_name"].as_str().unwrap().contains("std"));
assert_eq!(result["symbol_type"].as_str().unwrap(), "import");
}
Ok(())
}
async fn start_test_server(addr: &str) -> Result<context_creator::mcp_server::ServerHandle> {
context_creator::mcp_server::start_server(addr).await
}