Skip to main content
Glama
integration_tests.rs16.6 kB
use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; use tfmcp::registry::{ batch::BatchFetcher, cache::CacheManager, client::{ProviderInfo, RegistryClient}, fallback::RegistryClientWithFallback, provider::ProviderResolver, }; /// Helper function to create test provider info fn create_test_provider(name: &str, namespace: &str) -> ProviderInfo { ProviderInfo { name: name.to_string(), namespace: namespace.to_string(), version: "1.0.0".to_string(), description: format!("Test provider for {}", name), downloads: 1000, published_at: "2023-01-01".to_string(), id: format!("{}/{}", namespace, name), source: None, tag: None, logo_url: None, owner: None, tier: None, verified: None, trusted: None, extra: std::collections::HashMap::new(), } } #[tokio::test] async fn test_provider_resolver_creation() { let resolver = ProviderResolver::new(); // Test that the resolver creates successfully by verifying it can be cloned let cloned_resolver = resolver.clone(); assert!(std::mem::size_of_val(&resolver) > 0); assert!(std::mem::size_of_val(&cloned_resolver) > 0); } #[tokio::test] async fn test_cache_manager_operations() { let cache_manager = CacheManager::new(); // Test basic cache operations cache_manager .providers_cache .set("test_key".to_string(), "test_value".to_string()) .await; let cached_value = cache_manager.providers_cache.get("test_key").await; assert_eq!(cached_value, Some("test_value".to_string())); // Test cache statistics let stats = cache_manager.providers_cache.stats().await; assert!(stats.total_entries >= 1); assert!(stats.valid_entries >= 1); assert_eq!(stats.expired_entries, 0); } #[tokio::test] async fn test_batch_fetcher_configuration() { let client = Arc::new(RegistryClient::new()); // Test different concurrency settings let fetcher_low = BatchFetcher::new(client.clone(), 2); let fetcher_high = BatchFetcher::new(client.clone(), 15); // Should be capped at 10 let fetcher_zero = BatchFetcher::new(client.clone(), 0); // Should be set to 1 assert_eq!(fetcher_low.max_concurrent, 2); assert_eq!(fetcher_high.max_concurrent, 10); // Capped at maximum assert_eq!(fetcher_zero.max_concurrent, 1); // Minimum enforced } #[tokio::test] async fn test_fallback_client_creation() { let fallback_client = RegistryClientWithFallback::new(); // Test that fallback namespaces are properly configured assert_eq!(fallback_client.fallback_namespaces.len(), 3); assert!(fallback_client .fallback_namespaces .contains(&"hashicorp".to_string())); assert!(fallback_client .fallback_namespaces .contains(&"terraform-providers".to_string())); assert!(fallback_client .fallback_namespaces .contains(&"community".to_string())); } #[tokio::test] async fn test_cache_expiration_and_cleanup() { let cache = tfmcp::registry::cache::SimpleCache::new(Duration::from_millis(100)); // Add test data cache.set("key1".to_string(), "value1".to_string()).await; cache.set("key2".to_string(), "value2".to_string()).await; // Verify data is accessible assert_eq!(cache.get("key1").await, Some("value1".to_string())); assert_eq!(cache.get("key2").await, Some("value2".to_string())); // Wait for expiration tokio::time::sleep(Duration::from_millis(150)).await; // Verify data has expired assert_eq!(cache.get("key1").await, None); assert_eq!(cache.get("key2").await, None); // Test cache statistics after expiration let stats = cache.stats().await; assert_eq!(stats.valid_entries, 0); assert_eq!(stats.expired_entries, 2); } #[tokio::test] async fn test_cache_cleanup_functionality() { let cache = tfmcp::registry::cache::SimpleCache::new(Duration::from_millis(100)); // Add test data cache.set("key1".to_string(), "value1".to_string()).await; cache.set("key2".to_string(), "value2".to_string()).await; // Wait for expiration tokio::time::sleep(Duration::from_millis(150)).await; // Verify expired entries are tracked let stats_before = cache.stats().await; assert_eq!(stats_before.expired_entries, 2); // Clean up expired entries cache.cleanup_expired().await; // Verify cleanup worked let stats_after = cache.stats().await; assert_eq!(stats_after.total_entries, 0); assert_eq!(stats_after.valid_entries, 0); assert_eq!(stats_after.expired_entries, 0); } #[tokio::test] async fn test_empty_batch_operations() { let fetcher = BatchFetcher::default(); // Test empty provider list let results = fetcher.fetch_providers(vec![]).await; assert!(results.is_empty()); // Test empty version list let version_results = fetcher.fetch_provider_versions(vec![]).await; assert!(version_results.is_empty()); // Test empty docs list let docs_results = fetcher.fetch_multiple_docs(vec![]).await; assert!(docs_results.is_empty()); } #[tokio::test] async fn test_provider_search_timeout_handling() { let resolver = ProviderResolver::new(); // Test with a very short timeout to simulate timeout conditions let search_future = resolver.search_providers("aws"); let result = timeout(Duration::from_millis(1), search_future).await; // This should timeout, demonstrating our timeout handling works assert!( result.is_err(), "Expected timeout error for very short duration" ); } #[tokio::test] async fn test_comprehensive_cache_manager_stats() { let cache_manager = CacheManager::new(); // Add data to different caches cache_manager .providers_cache .set("provider1".to_string(), "data1".to_string()) .await; cache_manager .documentation_cache .set("doc1".to_string(), "content1".to_string()) .await; // Test global statistics let global_stats = cache_manager.global_stats().await; assert_eq!(global_stats.len(), 2); assert!(global_stats.contains_key("providers")); assert!(global_stats.contains_key("documentation")); // Verify each cache has the expected data let provider_stats = &global_stats["providers"]; assert!(provider_stats.total_entries >= 1); let doc_stats = &global_stats["documentation"]; assert!(doc_stats.total_entries >= 1); } #[tokio::test] async fn test_output_formatter_provider_list() { let providers = vec![ create_test_provider("aws", "hashicorp"), create_test_provider("google", "hashicorp"), ]; let formatted = tfmcp::formatters::output::OutputFormatter::format_provider_list(providers); // Verify structure assert_eq!(formatted["summary"]["total_providers"], 2); assert!(formatted["providers"].is_array()); assert_eq!(formatted["providers"].as_array().unwrap().len(), 2); // Verify provider data let first_provider = &formatted["providers"][0]; assert_eq!(first_provider["name"], "aws"); assert_eq!(first_provider["namespace"], "hashicorp"); assert_eq!(first_provider["id"], "hashicorp/aws"); } #[tokio::test] async fn test_output_formatter_error_handling() { let error_msg = "Provider not found"; let suggestions = Some(vec![ "Check provider name spelling".to_string(), "Try searching without namespace".to_string(), ]); let formatted = tfmcp::formatters::output::OutputFormatter::format_error_with_suggestions( error_msg, suggestions, None, ); // Verify error structure assert_eq!(formatted["error"]["message"], "Provider not found"); assert_eq!(formatted["error"]["type"], "provider_resolution_error"); // Verify suggestions assert!(formatted["suggestions"].is_object()); let suggested_actions = &formatted["suggestions"]["recommended_actions"]; assert_eq!(suggested_actions.as_array().unwrap().len(), 2); } #[tokio::test] async fn test_tool_description_builder() { use tfmcp::prompts::builder::{ToolDescription, ToolExample}; let example = ToolExample { title: "Basic Search".to_string(), description: "Search for AWS provider".to_string(), input: serde_json::json!({"query": "aws"}), expected_output: "List of AWS-related providers".to_string(), }; let description = ToolDescription::new("Search for Terraform providers") .with_usage_guide("Use this tool to find providers by name or keyword") .with_constraint("Query must be at least 2 characters") .with_security_note("Only searches public Terraform Registry") .with_example(example) .with_error_hint("No Results", "Try broader search terms"); let prompt = description.build_prompt(); // Verify prompt contains all components assert!(prompt.contains("Search for Terraform providers")); assert!(prompt.contains("Usage Guide")); assert!(prompt.contains("Constraints")); assert!(prompt.contains("Security Notes")); assert!(prompt.contains("Examples")); assert!(prompt.contains("Troubleshooting")); } #[tokio::test] async fn test_performance_benchmarks() { use std::time::Instant; let cache_manager = CacheManager::new(); let start = Instant::now(); // Simulate adding multiple cache entries for i in 0..100 { cache_manager .providers_cache .set(format!("key_{}", i), format!("value_{}", i)) .await; } let write_duration = start.elapsed(); assert!( write_duration < Duration::from_millis(100), "Cache writes should be fast" ); let read_start = Instant::now(); // Test read performance for i in 0..100 { let _ = cache_manager .providers_cache .get(&format!("key_{}", i)) .await; } let read_duration = read_start.elapsed(); assert!( read_duration < Duration::from_millis(50), "Cache reads should be very fast" ); } /// Integration test for the complete provider resolution workflow #[tokio::test] async fn test_end_to_end_provider_workflow() { let resolver = ProviderResolver::new(); // Test the complete workflow would work with real API calls // Note: This is a mock test since we don't want to make real API calls in tests // Verify that the resolver can be properly created and used let cloned_resolver = resolver.clone(); assert!(std::mem::size_of_val(&resolver) > 0); assert!(std::mem::size_of_val(&cloned_resolver) > 0); // Test integration with cache manager let cache_manager = CacheManager::new(); let cache_stats = cache_manager.providers_cache.stats().await; assert_eq!(cache_stats.total_entries, 0); // Should start empty assert_eq!(cache_stats.valid_entries, 0); assert_eq!(cache_stats.expired_entries, 0); } // ==================== Module Health Analysis Tests ==================== use std::fs; use tempfile::tempdir; /// Test module health analysis with a sample Terraform project #[tokio::test] async fn test_module_health_analysis() { // Create a temporary Terraform project let temp_dir = tempdir().expect("Failed to create temp dir"); let project_path = temp_dir.path(); // Create main.tf with networking resources let main_tf = r#" resource "aws_vpc" "main" { cidr_block = var.vpc_cidr } resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id cidr_block = var.subnet_cidr } resource "aws_security_group" "web" { vpc_id = aws_vpc.main.id name = "web-sg" } "#; fs::write(project_path.join("main.tf"), main_tf).expect("Failed to write main.tf"); // Create variables.tf let variables_tf = r#" variable "vpc_cidr" { description = "VPC CIDR block" type = string default = "10.0.0.0/16" } variable "subnet_cidr" { type = string default = "10.0.1.0/24" } "#; fs::write(project_path.join("variables.tf"), variables_tf) .expect("Failed to write variables.tf"); // Check if terraform binary exists let terraform_path = which::which("terraform"); if terraform_path.is_err() { println!("Skipping test: terraform binary not found"); return; } // Create TerraformService let service = tfmcp::TerraformService::new(terraform_path.unwrap(), project_path.to_path_buf()); // Test module health analysis let health = service.analyze_module_health().await; assert!(health.is_ok(), "Module health analysis should succeed"); let health = health.unwrap(); assert!(!health.module_path.is_empty()); assert!(health.health_score <= 100); assert!(health.metrics.resource_count > 0); assert!(health.metrics.variable_count > 0); // Cohesion should be good (all networking resources) assert!( health.cohesion_analysis.score > 50, "Cohesion should be good for related resources" ); } /// Test resource dependency graph generation #[tokio::test] async fn test_resource_dependency_graph() { let temp_dir = tempdir().expect("Failed to create temp dir"); let project_path = temp_dir.path(); // Create main.tf with dependencies let main_tf = r#" resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" } resource "aws_instance" "web" { ami = "ami-12345" instance_type = "t3.micro" subnet_id = aws_subnet.public.id depends_on = [aws_vpc.main] } "#; fs::write(project_path.join("main.tf"), main_tf).expect("Failed to write main.tf"); let terraform_path = which::which("terraform"); if terraform_path.is_err() { println!("Skipping test: terraform binary not found"); return; } let service = tfmcp::TerraformService::new(terraform_path.unwrap(), project_path.to_path_buf()); let graph = service.get_dependency_graph().await; assert!(graph.is_ok(), "Dependency graph should be generated"); let graph = graph.unwrap(); assert_eq!(graph.nodes.len(), 3, "Should have 3 resource nodes"); assert!( !graph.module_boundaries.is_empty(), "Should have module boundaries" ); } /// Test refactoring suggestions #[tokio::test] async fn test_refactoring_suggestions() { let temp_dir = tempdir().expect("Failed to create temp dir"); let project_path = temp_dir.path(); // Create main.tf with many different resource types (low cohesion) let main_tf = r#" resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_instance" "web" { ami = "ami-12345" instance_type = "t3.micro" } resource "aws_s3_bucket" "logs" { bucket = "logs-bucket" } resource "aws_lambda_function" "processor" { function_name = "processor" runtime = "python3.9" handler = "main.handler" role = "arn:aws:iam::123456789:role/lambda" filename = "function.zip" } resource "aws_rds_cluster" "database" { cluster_identifier = "database" engine = "aurora-mysql" master_username = "admin" master_password = "password" } "#; fs::write(project_path.join("main.tf"), main_tf).expect("Failed to write main.tf"); // Create variables.tf with many variables (some without descriptions) let variables_tf = r#" variable "var1" { type = string } variable "var2" { type = string } variable "var3" { type = string } variable "var4" { type = string } variable "var5" { type = string } "#; fs::write(project_path.join("variables.tf"), variables_tf) .expect("Failed to write variables.tf"); let terraform_path = which::which("terraform"); if terraform_path.is_err() { println!("Skipping test: terraform binary not found"); return; } let service = tfmcp::TerraformService::new(terraform_path.unwrap(), project_path.to_path_buf()); let suggestions = service.suggest_refactoring().await; assert!( suggestions.is_ok(), "Refactoring suggestions should be generated" ); let suggestions = suggestions.unwrap(); // Should suggest adding descriptions for variables without them let has_description_suggestion = suggestions.iter().any(|s| { matches!( s.suggestion_type, tfmcp::model::RefactoringType::AddDescriptions ) }); assert!( has_description_suggestion, "Should suggest adding descriptions" ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server