2025-10-07 10:59:52 +01:00

549 lines
19 KiB
Rust

//! 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();
}
}