New crates
- platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
subject prefixing under provisioning.>, 6 stream definitions on startup
- platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL
Service integrations
- orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
- control-center: solo_auth_middleware (intentional bypass, --mode solo only),
NATS session events, WebSocket bridge via JetStream subscription (no polling)
- vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
SurrealDB storage backend with MVCC retry + exponential backoff
- secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
- extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
cache invalidation via provisioning.workspace.*.deploy.done
Clippy workspace clean
cargo clippy --workspace -- -D warnings: 0 errors
Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
(let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
289 lines
9.8 KiB
Rust
289 lines
9.8 KiB
Rust
// Integration tests for platform-config multi-source schema resolution
|
||
|
||
use std::fs;
|
||
use std::path::PathBuf;
|
||
|
||
use platform_config::{deployment::load_deployment_mode, ExtensionSchemaCache, PlatformStartup};
|
||
use tempfile::TempDir;
|
||
|
||
#[test]
|
||
fn test_deployment_mode_load() {
|
||
// This test requires deployment-mode.ncl to exist
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
assert!(
|
||
config.config.is_object(),
|
||
"Deployment config should be a JSON object"
|
||
);
|
||
println!("✓ Deployment mode loaded successfully");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_platform_startup_initialization() {
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
let startup = PlatformStartup::new(&config.config);
|
||
assert!(
|
||
startup.is_ok(),
|
||
"PlatformStartup initialization should succeed"
|
||
);
|
||
|
||
let startup = startup.unwrap();
|
||
let enabled = startup.enabled_services();
|
||
println!("✓ Enabled services: {:?}", enabled);
|
||
|
||
// Should have at least vault_service and orchestrator
|
||
assert!(
|
||
!enabled.is_empty(),
|
||
"At least one service should be enabled"
|
||
);
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_service_dependencies_validation() {
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
let startup = PlatformStartup::new(&config.config);
|
||
assert!(startup.is_ok(), "PlatformStartup should initialize");
|
||
|
||
let startup = startup.unwrap();
|
||
|
||
// Validate orchestrator dependencies (should depend on vault_service)
|
||
let result = startup.validate_dependencies("orchestrator");
|
||
|
||
// This will pass if vault_service is enabled, or fail if not
|
||
// Both are valid outcomes depending on deployment config
|
||
match result {
|
||
Ok(()) => println!("✓ Orchestrator dependencies validated"),
|
||
Err(e) => println!("ℹ Orchestrator dependency check: {}", e),
|
||
}
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_startup_order_calculation() {
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
let startup = PlatformStartup::new(&config.config);
|
||
assert!(startup.is_ok(), "PlatformStartup should initialize");
|
||
|
||
let startup = startup.unwrap();
|
||
let order = startup.get_startup_order();
|
||
assert!(order.is_ok(), "Startup order calculation should succeed");
|
||
|
||
let order = order.unwrap();
|
||
println!("✓ Startup order: {:?}", order);
|
||
|
||
// vault_service should come before orchestrator (if both enabled)
|
||
let vault_idx = order.iter().position(|s| s == "vault_service");
|
||
let orch_idx = order.iter().position(|s| s == "orchestrator");
|
||
|
||
if let (Some(v), Some(o)) = (vault_idx, orch_idx) {
|
||
assert!(v < o, "vault_service should start before orchestrator");
|
||
}
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_extension_schema_cache_creation() {
|
||
let temp_dir = TempDir::new().unwrap();
|
||
let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf());
|
||
|
||
assert!(
|
||
cache.is_ok(),
|
||
"ExtensionSchemaCache should create successfully"
|
||
);
|
||
println!("✓ Extension schema cache created");
|
||
}
|
||
|
||
#[test]
|
||
fn test_extension_discovery_and_import_paths() {
|
||
let temp_dir = TempDir::new().unwrap();
|
||
let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf()).unwrap();
|
||
|
||
// Create mock extension structure
|
||
let ext_dir = temp_dir.path().join("hetzner@1.0.0");
|
||
let schemas_dir = ext_dir.join("schemas");
|
||
fs::create_dir_all(&schemas_dir).unwrap();
|
||
fs::write(schemas_dir.join("main.ncl"), "{}").unwrap();
|
||
|
||
// List extensions
|
||
let extensions = cache.list_cached_extensions().unwrap();
|
||
assert_eq!(extensions.len(), 1, "Should find hetzner extension");
|
||
assert_eq!(extensions[0].name, "hetzner");
|
||
assert_eq!(extensions[0].version, "1.0.0");
|
||
|
||
// Get import paths
|
||
let import_paths = cache.build_import_paths().unwrap();
|
||
assert_eq!(import_paths.len(), 1, "Should have one import path");
|
||
assert!(import_paths[0].exists(), "Import path should exist");
|
||
|
||
println!("✓ Extension discovery successful");
|
||
}
|
||
|
||
#[test]
|
||
fn test_multiple_extension_versions() {
|
||
let temp_dir = TempDir::new().unwrap();
|
||
let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf()).unwrap();
|
||
|
||
// Create multiple versions of same extension
|
||
for version in &["1.0.0", "1.1.0", "2.0.0"] {
|
||
let ext_dir = temp_dir.path().join(format!("aws@{}", version));
|
||
let schemas_dir = ext_dir.join("schemas");
|
||
fs::create_dir_all(&schemas_dir).unwrap();
|
||
fs::write(schemas_dir.join("main.ncl"), "{}").unwrap();
|
||
}
|
||
|
||
let extensions = cache.list_cached_extensions().unwrap();
|
||
assert_eq!(extensions.len(), 3, "Should find all AWS versions");
|
||
|
||
// Test exact version matching
|
||
let path = cache.get_extension_path("aws", Some("1.1.0"));
|
||
assert!(path.is_some(), "Should find exact version");
|
||
assert!(path.unwrap().ends_with("aws@1.1.0/schemas"));
|
||
|
||
// Test latest version (first found)
|
||
let path = cache.get_extension_path("aws", None);
|
||
assert!(path.is_some(), "Should find at least one version");
|
||
|
||
println!("✓ Multiple extension versions handled correctly");
|
||
}
|
||
|
||
#[test]
|
||
fn test_multi_extension_import_paths() {
|
||
let temp_dir = TempDir::new().unwrap();
|
||
let cache = ExtensionSchemaCache::new(temp_dir.path().to_path_buf()).unwrap();
|
||
|
||
// Create multiple extensions
|
||
for (name, version) in &[("hetzner", "1.0.0"), ("aws", "2.1.0"), ("upcloud", "1.5.0")] {
|
||
let ext_dir = temp_dir.path().join(format!("{}@{}", name, version));
|
||
let schemas_dir = ext_dir.join("schemas");
|
||
fs::create_dir_all(&schemas_dir).unwrap();
|
||
fs::write(schemas_dir.join("main.ncl"), "{}").unwrap();
|
||
}
|
||
|
||
let extensions = cache.list_cached_extensions().unwrap();
|
||
assert_eq!(extensions.len(), 3, "Should find all extensions");
|
||
|
||
let import_paths = cache.build_import_paths().unwrap();
|
||
assert_eq!(import_paths.len(), 3, "Should have three import paths");
|
||
|
||
// All paths should exist
|
||
for path in &import_paths {
|
||
assert!(path.exists(), "Import path should exist: {:?}", path);
|
||
}
|
||
|
||
println!("✓ Multi-extension import paths: {:?}", import_paths);
|
||
}
|
||
|
||
#[test]
|
||
fn test_git_repo_structure_in_startup() {
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
// Just verify that the git config is present in deployment-mode
|
||
let git_config = config.config.get("git");
|
||
assert!(
|
||
git_config.is_some(),
|
||
"Git configuration should be in deployment-mode.ncl"
|
||
);
|
||
|
||
let schemas = git_config.unwrap().get("schemas_repo");
|
||
let configs = git_config.unwrap().get("configs_repo");
|
||
|
||
assert!(schemas.is_some(), "schemas_repo should be defined");
|
||
assert!(configs.is_some(), "configs_repo should be defined");
|
||
|
||
// Verify essential fields
|
||
let schemas = schemas.unwrap();
|
||
assert!(schemas.get("url").is_some(), "URL should be defined");
|
||
assert!(
|
||
schemas.get("cache_dir").is_some(),
|
||
"cache_dir should be defined"
|
||
);
|
||
|
||
println!("✓ Git repo structure validated");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_nickel_import_path_priority() {
|
||
// This test verifies that the NICKEL_IMPORT_PATH priority system works
|
||
// (actual priority order is tested via functional tests, not unit tests)
|
||
|
||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||
|
||
// Priority 1: Extension schemas
|
||
let ext_cache = PathBuf::from(format!("{}/.cache/provisioning/extensions", home));
|
||
if ext_cache.exists() {
|
||
println!("✓ Extension cache dir exists: {:?}", ext_cache);
|
||
}
|
||
|
||
// Priority 2: Git schemas
|
||
let schemas_cache = PathBuf::from(format!("{}/.cache/provisioning/schemas", home));
|
||
if schemas_cache.exists() {
|
||
println!("✓ Schemas cache dir exists: {:?}", schemas_cache);
|
||
}
|
||
|
||
// Priority 3: Git configs
|
||
let configs_cache = PathBuf::from(format!("{}/.cache/provisioning/configs", home));
|
||
if configs_cache.exists() {
|
||
println!("✓ Configs cache dir exists: {:?}", configs_cache);
|
||
}
|
||
|
||
// Priority 4: Local provisioning/schemas
|
||
if PathBuf::from("provisioning/schemas").exists() {
|
||
println!("✓ Local schemas dir exists");
|
||
}
|
||
|
||
println!("✓ NICKEL_IMPORT_PATH cache structure verified");
|
||
}
|
||
|
||
#[test]
|
||
fn test_service_enabled_checking() {
|
||
match load_deployment_mode() {
|
||
Ok(config) => {
|
||
let startup = PlatformStartup::new(&config.config);
|
||
assert!(startup.is_ok());
|
||
|
||
let startup = startup.unwrap();
|
||
|
||
// Core services should be checkable
|
||
for service in &["orchestrator", "vault_service"] {
|
||
let enabled = startup.is_service_enabled(service);
|
||
println!(
|
||
" • {}: {}",
|
||
service,
|
||
if enabled { "enabled" } else { "disabled" }
|
||
);
|
||
}
|
||
|
||
println!("✓ Service enablement checking works");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("ℹ Deployment mode not found (expected in CI): {}", e);
|
||
}
|
||
}
|
||
}
|