Jesús Pérez 93b0e5225c
feat(platform): control plane — NATS JetStream + SurrealDB + SOLID enforcement
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
2026-02-17 23:58:14 +00:00

289 lines
9.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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