# Integration Test Helpers # Common utilities for integration testing use std log # Test configuration export def load-test-config [] { let config_path = $"($env.PWD)/provisioning/tests/integration/test_config.yaml" if not ($config_path | path exists) { error make { msg: "Test configuration not found" label: { text: $"Config file not found: ($config_path)" span: (metadata $config_path).span } } } open $config_path } # Assertion helpers export def assert-eq [actual: any, expected: any, message: string = ""] { if $actual != $expected { let error_msg = if ($message | is-empty) { $"Assertion failed: expected ($expected), got ($actual)" } else { $"($message): expected ($expected), got ($actual)" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $actual).span } } } } export def assert-true [condition: bool, message: string = ""] { if not $condition { let error_msg = if ($message | is-empty) { "Assertion failed: expected true, got false" } else { $"($message): expected true, got false" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $condition).span } } } } export def assert-false [condition: bool, message: string = ""] { if $condition { let error_msg = if ($message | is-empty) { "Assertion failed: expected false, got true" } else { $"($message): expected false, got true" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $condition).span } } } } export def assert-contains [haystack: list, needle: any, message: string = ""] { if not ($needle in $haystack) { let error_msg = if ($message | is-empty) { $"Assertion failed: ($haystack) does not contain ($needle)" } else { $"($message): ($haystack) does not contain ($needle)" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $haystack).span } } } } export def assert-not-contains [haystack: list, needle: any, message: string = ""] { if $needle in $haystack { let error_msg = if ($message | is-empty) { $"Assertion failed: ($haystack) contains ($needle)" } else { $"($message): ($haystack) contains ($needle)" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $haystack).span } } } } export def assert-not-empty [value: any, message: string = ""] { let is_empty = match ($value | describe) { "string" => { ($value | str length) == 0 } "list" => { ($value | length) == 0 } "record" => { ($value | columns | length) == 0 } _ => { false } } if $is_empty { let error_msg = if ($message | is-empty) { "Assertion failed: value is empty" } else { $"($message): value is empty" } error make { msg: $error_msg label: { text: "Assertion failed" span: (metadata $value).span } } } } export def assert-http-success [response: record, message: string = ""] { let status = $response.status if $status < 200 or $status >= 300 { let error_msg = if ($message | is-empty) { $"HTTP request failed with status ($status)" } else { $"($message): HTTP request failed with status ($status)" } error make { msg: $error_msg label: { text: "HTTP assertion failed" span: (metadata $response).span } } } } # Test fixture helpers export def create-test-workspace [name: string, config: record] { let test_config = (load-test-config) let workspace_path = $"($test_config.test_workspace.path)-($name)" # Create workspace directory structure mkdir $workspace_path mkdir $"($workspace_path)/config" mkdir $"($workspace_path)/infra" mkdir $"($workspace_path)/extensions" mkdir $"($workspace_path)/runtime" # Write workspace config let workspace_config = { workspace: { name: $name version: "1.0.0" created: (date now | format date "%Y-%m-%d") } settings: $config } $workspace_config | save -f $"($workspace_path)/config/provisioning.yaml" { name: $name path: $workspace_path config: $workspace_config } } export def cleanup-test-workspace [workspace: record] { if ($workspace.path | path exists) { rm -rf $workspace.path log info $"Cleaned up test workspace: ($workspace.name)" } } export def create-test-server [ name: string provider: string = "local" config: record = {} ] { let test_config = (load-test-config) # Prepare server configuration let server_config = { hostname: $name provider: $provider cores: ($config.cores? | default 2) memory: ($config.memory? | default 4096) storage: ($config.storage? | default 50) zone: ($config.zone? | default "local") } # Call orchestrator API to create server let orchestrator_url = $"http://($test_config.services.orchestrator.host):($test_config.services.orchestrator.port)" let response = (http post $"($orchestrator_url)/workflows/servers/create" { server: $server_config check: true }) assert-http-success $response "Server creation failed" $response.body } export def delete-test-server [server_id: string] { let test_config = (load-test-config) let orchestrator_url = $"http://($test_config.services.orchestrator.host):($test_config.services.orchestrator.port)" let response = (http delete $"($orchestrator_url)/servers/($server_id)") assert-http-success $response "Server deletion failed" $response.body } # Retry helper for flaky operations export def with-retry [ --max-attempts: int = 3 --delay: int = 5 --backoff-multiplier: float = 2.0 operation: closure ] { mut attempts = 0 mut current_delay = $delay mut last_error = null while $attempts < $max_attempts { try { return (do $operation) } catch { |err| $attempts = $attempts + 1 $last_error = $err if $attempts < $max_attempts { log warning $"Attempt ($attempts) failed: ($err.msg). Retrying in ($current_delay)s..." sleep ($current_delay * 1sec) $current_delay = ($current_delay * $backoff_multiplier | into int) } } } error make { msg: $"Operation failed after ($max_attempts) attempts: ($last_error.msg)" label: { text: "Retry exhausted" span: (metadata $operation).span } } } # Wait for condition with timeout export def wait-for-condition [ --timeout: int = 60 --interval: int = 5 condition: closure description: string = "condition" ] { let start_time = (date now) let timeout_duration = ($timeout * 1sec) while true { try { let result = (do $condition) if $result { return true } } catch { |err| # Ignore errors, keep waiting } let elapsed = ((date now) - $start_time) if $elapsed > $timeout_duration { error make { msg: $"Timeout waiting for ($description) after ($timeout)s" label: { text: "Timeout" span: (metadata $condition).span } } } sleep ($interval * 1sec) } } # Service health check helpers export def check-service-health [service_name: string] { let test_config = (load-test-config) let service_config = match $service_name { "orchestrator" => { $test_config.services.orchestrator } "coredns" => { $test_config.services.coredns } "gitea" => { $test_config.services.gitea } "postgres" => { $test_config.services.postgres } "prometheus" => { $test_config.services.prometheus } "grafana" => { $test_config.services.grafana } _ => { error make { msg: $"Unknown service: ($service_name)" } } } match $service_name { "orchestrator" => { let url = $"http://($service_config.host):($service_config.port)($service_config.health_endpoint)" try { let response = (http get $url) $response.status == 200 } catch { false } } "coredns" => { # Check DNS resolution try { dig @$service_config.host test.local | complete | get exit_code | $in == 0 } catch { false } } "gitea" => { let url = $"http://($service_config.host):($service_config.port)/api/v1/version" try { let response = (http get $url) $response.status == 200 } catch { false } } "postgres" => { try { psql -h $service_config.host -p $service_config.port -U $service_config.username -d $service_config.database -c "SELECT 1" | complete | get exit_code | $in == 0 } catch { false } } _ => { let url = $"http://($service_config.host):($service_config.port)/" try { let response = (http get $url) $response.status == 200 } catch { false } } } } export def wait-for-service [service_name: string, timeout: int = 60] { wait-for-condition --timeout $timeout --interval 5 { check-service-health $service_name } $"service ($service_name) to be healthy" } # Test result tracking export def create-test-result [ test_name: string status: string # "passed", "failed", "skipped" duration_ms: int error_message: string = "" ] { { test_name: $test_name status: $status duration_ms: $duration_ms error_message: $error_message timestamp: (date now | format date "%Y-%m-%dT%H:%M:%S%.3fZ") } } # Test runner helpers export def run-test [ test_name: string test_fn: closure ] { log info $"Running test: ($test_name)" let start_time = (date now) try { do $test_fn let duration = ((date now) - $start_time | into int) / 1000000 log info $"✓ Test passed: ($test_name) \(($duration)ms\)" create-test-result $test_name "passed" $duration } catch { |err| let duration = ((date now) - $start_time | into int) / 1000000 log error $"✗ Test failed: ($test_name) - ($err.msg)" create-test-result $test_name "failed" $duration $err.msg } } # Cleanup helpers export def cleanup-on-exit [cleanup_fn: closure] { # Register cleanup function to run on exit # Note: Nushell doesn't have built-in exit hooks, so this is a best-effort approach try { do $cleanup_fn } catch { |err| log warning $"Cleanup failed: ($err.msg)" } }