//! Factory pattern and configuration validation tests //! //! This module tests the storage factory pattern, configuration validation, //! CLI argument parsing, and backend selection with feature flag handling. use std::path::PathBuf; use tempfile::TempDir; use tokio_test; // Import test helpers and orchestrator types mod helpers; use helpers::{TestEnvironment, StorageAssertions}; use orchestrator::{Args, TaskStatus}; use orchestrator::storage::{ StorageConfig, create_storage, create_storage_from_args, available_storage_types, storage_help_text, DefaultStorageFactory, StorageFactory, TaskStorage }; /// Test storage configuration creation and validation #[tokio::test] async fn test_storage_config_creation() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); // Test filesystem configuration let fs_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: data_path.clone(), ..Default::default() }; assert!(fs_config.validate().is_ok()); #[cfg(feature = "surrealdb")] { // Test SurrealDB embedded configuration let embedded_config = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: data_path.clone(), surrealdb_namespace: Some("test_ns".to_string()), surrealdb_database: Some("test_db".to_string()), ..Default::default() }; assert!(embedded_config.validate().is_ok()); // Test SurrealDB server configuration let server_config = StorageConfig { storage_type: "surrealdb-server".to_string(), data_dir: "".to_string(), surrealdb_url: Some("ws://localhost:8000".to_string()), surrealdb_namespace: Some("test_ns".to_string()), surrealdb_database: Some("test_db".to_string()), surrealdb_username: Some("admin".to_string()), surrealdb_password: Some("password".to_string()), }; assert!(server_config.validate().is_ok()); } } /// Test storage configuration validation failures #[tokio::test] async fn test_storage_config_validation_failures() { // Test filesystem config with empty data_dir let invalid_fs_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: "".to_string(), ..Default::default() }; assert!(invalid_fs_config.validate().is_err()); #[cfg(feature = "surrealdb")] { // Test embedded config with empty data_dir let invalid_embedded_config = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: "".to_string(), ..Default::default() }; assert!(invalid_embedded_config.validate().is_err()); // Test server config with missing URL let invalid_server_config = StorageConfig { storage_type: "surrealdb-server".to_string(), surrealdb_username: Some("admin".to_string()), surrealdb_password: Some("password".to_string()), ..Default::default() }; assert!(invalid_server_config.validate().is_err()); // Test server config with missing credentials let incomplete_server_config = StorageConfig { storage_type: "surrealdb-server".to_string(), surrealdb_url: Some("ws://localhost:8000".to_string()), ..Default::default() }; assert!(incomplete_server_config.validate().is_err()); } // Test unknown storage type let unknown_config = StorageConfig { storage_type: "unknown_backend".to_string(), data_dir: "/tmp".to_string(), ..Default::default() }; assert!(unknown_config.validate().is_err()); #[cfg(not(feature = "surrealdb"))] { // Test SurrealDB config without feature let surrealdb_without_feature = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: "/tmp".to_string(), ..Default::default() }; let validation_result = surrealdb_without_feature.validate(); assert!(validation_result.is_err()); assert!(validation_result.unwrap_err().to_string().contains("surrealdb' feature")); } } /// Test CLI args to storage config conversion #[tokio::test] async fn test_args_to_storage_config_conversion() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); // Test filesystem args let fs_args = Args { port: 8080, data_dir: data_path.clone(), storage_type: "filesystem".to_string(), surrealdb_url: None, surrealdb_namespace: Some("default".to_string()), surrealdb_database: Some("tasks".to_string()), surrealdb_username: None, surrealdb_password: None, nu_path: "nu".to_string(), provisioning_path: "./core/nulib/provisioning".to_string(), }; let config = StorageConfig::from_args(&fs_args); assert_eq!(config.storage_type, "filesystem"); assert_eq!(config.data_dir, data_path); assert!(config.validate().is_ok()); #[cfg(feature = "surrealdb")] { // Test SurrealDB server args let server_args = Args { storage_type: "surrealdb-server".to_string(), surrealdb_url: Some("ws://localhost:8000".to_string()), surrealdb_username: Some("admin".to_string()), surrealdb_password: Some("secret".to_string()), ..fs_args.clone() }; let server_config = StorageConfig::from_args(&server_args); assert_eq!(server_config.storage_type, "surrealdb-server"); assert_eq!(server_config.surrealdb_url, Some("ws://localhost:8000".to_string())); assert_eq!(server_config.surrealdb_username, Some("admin".to_string())); assert!(server_config.validate().is_ok()); } } /// Test factory pattern storage creation #[tokio::test] async fn test_factory_storage_creation() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); // Test filesystem storage creation let fs_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: data_path.clone(), ..Default::default() }; let storage = DefaultStorageFactory::create_storage(fs_config).await.unwrap(); StorageAssertions::assert_healthy(&storage).await.unwrap(); // Verify storage works let task = helpers::TestDataGenerator::new().workflow_task(); storage.enqueue(task.clone(), 1).await.unwrap(); StorageAssertions::assert_task_exists(&storage, &task.id, &task.name).await.unwrap(); #[cfg(feature = "surrealdb")] { // Test SurrealDB embedded storage creation let embedded_temp_dir = TempDir::new().unwrap(); let embedded_path = embedded_temp_dir.path().to_string_lossy().to_string(); let embedded_config = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: embedded_path, surrealdb_namespace: Some("test".to_string()), surrealdb_database: Some("test".to_string()), ..Default::default() }; let embedded_storage = DefaultStorageFactory::create_storage(embedded_config).await.unwrap(); StorageAssertions::assert_healthy(&embedded_storage).await.unwrap(); // Test SurrealDB server storage creation (using memory for testing) let server_config = StorageConfig { storage_type: "surrealdb-server".to_string(), data_dir: "".to_string(), surrealdb_url: Some("memory://test".to_string()), surrealdb_namespace: Some("test".to_string()), surrealdb_database: Some("test".to_string()), surrealdb_username: Some("test".to_string()), surrealdb_password: Some("test".to_string()), }; let server_storage = DefaultStorageFactory::create_storage(server_config).await.unwrap(); StorageAssertions::assert_healthy(&server_storage).await.unwrap(); } } /// Test factory creation from CLI args #[tokio::test] async fn test_factory_from_cli_args() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); let args = Args { port: 8080, data_dir: data_path, storage_type: "filesystem".to_string(), surrealdb_url: None, surrealdb_namespace: Some("orchestrator".to_string()), surrealdb_database: Some("tasks".to_string()), surrealdb_username: None, surrealdb_password: None, nu_path: "nu".to_string(), provisioning_path: "./core/nulib/provisioning".to_string(), }; let storage = create_storage_from_args(&args).await.unwrap(); StorageAssertions::assert_healthy(&storage).await.unwrap(); // Test basic functionality let gen = helpers::TestDataGenerator::new(); let task = gen.workflow_task(); storage.enqueue(task.clone(), 1).await.unwrap(); StorageAssertions::assert_task_count(&storage, 1).await.unwrap(); } /// Test factory error handling #[tokio::test] async fn test_factory_error_handling() { // Test invalid storage type let invalid_config = StorageConfig { storage_type: "invalid_backend".to_string(), data_dir: "/tmp".to_string(), ..Default::default() }; let result = DefaultStorageFactory::create_storage(invalid_config).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Unsupported storage type")); // Test invalid data directory let invalid_dir_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: "/invalid/nonexistent/path".to_string(), ..Default::default() }; let result = DefaultStorageFactory::create_storage(invalid_dir_config).await; assert!(result.is_err()); #[cfg(feature = "surrealdb")] { // Test invalid SurrealDB server URL let invalid_server_config = StorageConfig { storage_type: "surrealdb-server".to_string(), surrealdb_url: Some("invalid://url".to_string()), surrealdb_username: Some("test".to_string()), surrealdb_password: Some("test".to_string()), ..Default::default() }; let result = DefaultStorageFactory::create_storage(invalid_server_config).await; // This may or may not fail depending on SurrealDB's URL validation // We just ensure it doesn't panic } #[cfg(not(feature = "surrealdb"))] { // Test SurrealDB config without feature enabled let surrealdb_config = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: "/tmp".to_string(), ..Default::default() }; let result = DefaultStorageFactory::create_storage(surrealdb_config).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("surrealdb' feature")); } } /// Test available storage types function #[test] fn test_available_storage_types() { let types = available_storage_types(); // Filesystem should always be available assert!(types.contains(&"filesystem".to_string())); #[cfg(feature = "surrealdb")] { assert!(types.contains(&"surrealdb-embedded".to_string())); assert!(types.contains(&"surrealdb-server".to_string())); assert_eq!(types.len(), 3); } #[cfg(not(feature = "surrealdb"))] { assert_eq!(types.len(), 1); } // Verify no duplicates let mut sorted_types = types.clone(); sorted_types.sort(); sorted_types.dedup(); assert_eq!(types.len(), sorted_types.len()); } /// Test storage help text generation #[test] fn test_storage_help_text() { let help_text = storage_help_text(); // Should contain information about filesystem assert!(help_text.contains("filesystem")); assert!(help_text.contains("JSON files")); assert!(help_text.contains("--data-dir")); #[cfg(feature = "surrealdb")] { assert!(help_text.contains("surrealdb-embedded")); assert!(help_text.contains("surrealdb-server")); assert!(help_text.contains("--surrealdb-url")); assert!(help_text.contains("--surrealdb-username")); } #[cfg(not(feature = "surrealdb"))] { assert!(help_text.contains("SurrealDB backends require compilation")); } // Should be well-formatted assert!(help_text.contains("Available storage backends")); assert!(help_text.contains("Example:")); } /// Test configuration edge cases #[tokio::test] async fn test_configuration_edge_cases() { // Test default configuration let default_config = StorageConfig::default(); assert_eq!(default_config.storage_type, "filesystem"); assert_eq!(default_config.data_dir, "./data"); assert_eq!(default_config.surrealdb_namespace, Some("orchestrator".to_string())); assert_eq!(default_config.surrealdb_database, Some("tasks".to_string())); // Test configuration with empty optional fields let minimal_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: "/tmp".to_string(), surrealdb_url: None, surrealdb_namespace: None, surrealdb_database: None, surrealdb_username: None, surrealdb_password: None, }; assert!(minimal_config.validate().is_ok()); #[cfg(feature = "surrealdb")] { // Test SurrealDB with default namespace/database let embedded_minimal = StorageConfig { storage_type: "surrealdb-embedded".to_string(), data_dir: "/tmp".to_string(), surrealdb_namespace: None, surrealdb_database: None, ..Default::default() }; assert!(embedded_minimal.validate().is_ok()); } } /// Test concurrent factory usage #[tokio::test] async fn test_concurrent_factory_usage() { use tokio::task::JoinSet; let mut join_set = JoinSet::new(); let mut temp_dirs = Vec::new(); // Create multiple storage instances concurrently for i in 0..5 { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); let config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: data_path, ..Default::default() }; temp_dirs.push(temp_dir); // Keep alive join_set.spawn(async move { let storage = DefaultStorageFactory::create_storage(config).await?; storage.health_check().await?; // Test basic operation let gen = helpers::TestDataGenerator::new(); let task = gen.workflow_task(); storage.enqueue(task, 1).await?; storage.total_tasks().await }); } // Wait for all factories to complete let mut success_count = 0; while let Some(result) = join_set.join_next().await { match result.unwrap() { Ok(task_count) => { assert_eq!(task_count, 1); success_count += 1; } Err(e) => panic!("Factory creation failed: {}", e), } } assert_eq!(success_count, 5); } /// Test storage type validation in CLI argument parsing #[test] fn test_storage_type_validation() { use orchestrator::validate_storage_type; // Valid storage types assert!(validate_storage_type("filesystem").is_ok()); #[cfg(feature = "surrealdb")] { assert!(validate_storage_type("surrealdb-embedded").is_ok()); assert!(validate_storage_type("surrealdb-server").is_ok()); } // Invalid storage type let result = validate_storage_type("invalid_type"); assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid storage type")); assert!(result.unwrap_err().contains("Available types:")); } /// Test factory with custom storage implementations #[tokio::test] async fn test_factory_extensibility() { // This test verifies that the factory pattern can be extended // The current implementation is focused on the built-in types, // but the trait-based design allows for future extensions let available_types = available_storage_types(); println!("Available storage types: {:?}", available_types); // Verify the factory returns appropriate errors for unknown types let unknown_config = StorageConfig { storage_type: "custom_future_backend".to_string(), data_dir: "/tmp".to_string(), ..Default::default() }; let result = DefaultStorageFactory::create_storage(unknown_config).await; assert!(result.is_err()); let error_msg = result.unwrap_err().to_string(); assert!(error_msg.contains("Unsupported storage type")); assert!(error_msg.contains("custom_future_backend")); } /// Test factory initialization order and dependencies #[tokio::test] async fn test_factory_initialization_order() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path().to_string_lossy().to_string(); let config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: data_path, ..Default::default() }; // Factory should handle initialization internally let storage = DefaultStorageFactory::create_storage(config).await.unwrap(); // Storage should be ready for use immediately StorageAssertions::assert_healthy(&storage).await.unwrap(); // Should be able to perform operations without additional setup let gen = helpers::TestDataGenerator::new(); let task = gen.workflow_task(); storage.enqueue(task.clone(), 1).await.unwrap(); let retrieved = storage.get_task(&task.id).await.unwrap(); assert!(retrieved.is_some()); assert_eq!(retrieved.unwrap().id, task.id); } /// Test factory with different configuration formats #[tokio::test] async fn test_factory_configuration_formats() { let temp_dir = TempDir::new().unwrap(); let data_path = temp_dir.path(); // Test with PathBuf let path_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: data_path.to_string_lossy().to_string(), ..Default::default() }; let storage = DefaultStorageFactory::create_storage(path_config).await.unwrap(); StorageAssertions::assert_healthy(&storage).await.unwrap(); // Test with relative path let relative_config = StorageConfig { storage_type: "filesystem".to_string(), data_dir: "./test_data".to_string(), ..Default::default() }; let storage = DefaultStorageFactory::create_storage(relative_config).await.unwrap(); StorageAssertions::assert_healthy(&storage).await.unwrap(); // Cleanup if std::path::Path::new("./test_data").exists() { std::fs::remove_dir_all("./test_data").ok(); } }