mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
feat(models): add agent profile validation
- Add profile validation for agent configuration - Implement validation rules for profile fields
This commit is contained in:
365
internal/models/profile_validation.go
Normal file
365
internal/models/profile_validation.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConfigKeyDefinition defines a valid configuration key with its type and constraints.
|
||||
type ConfigKeyDefinition struct {
|
||||
Key string // Config key name
|
||||
Type ConfigType // Expected value type
|
||||
Description string // Human-readable description
|
||||
Default interface{} // Default value (nil if required)
|
||||
Required bool // Whether the key is required
|
||||
Min *float64 // Minimum value for numbers
|
||||
Max *float64 // Maximum value for numbers
|
||||
Pattern string // Regex pattern for strings
|
||||
Enum []string // Allowed values for enums
|
||||
}
|
||||
|
||||
// ConfigType represents the type of a configuration value.
|
||||
type ConfigType string
|
||||
|
||||
const (
|
||||
ConfigTypeString ConfigType = "string"
|
||||
ConfigTypeBool ConfigType = "bool"
|
||||
ConfigTypeInt ConfigType = "int"
|
||||
ConfigTypeFloat ConfigType = "float"
|
||||
ConfigTypeDuration ConfigType = "duration"
|
||||
ConfigTypeEnum ConfigType = "enum"
|
||||
)
|
||||
|
||||
// ValidConfigKeys defines all valid agent configuration keys.
|
||||
var ValidConfigKeys = []ConfigKeyDefinition{
|
||||
{
|
||||
Key: "interval",
|
||||
Type: ConfigTypeDuration,
|
||||
Description: "Polling interval for metrics collection",
|
||||
Default: "30s",
|
||||
},
|
||||
{
|
||||
Key: "enable_docker",
|
||||
Type: ConfigTypeBool,
|
||||
Description: "Enable Docker container monitoring",
|
||||
Default: true,
|
||||
},
|
||||
{
|
||||
Key: "enable_system_metrics",
|
||||
Type: ConfigTypeBool,
|
||||
Description: "Enable system-level metrics (CPU, memory, disk)",
|
||||
Default: true,
|
||||
},
|
||||
{
|
||||
Key: "enable_process_metrics",
|
||||
Type: ConfigTypeBool,
|
||||
Description: "Enable process-level metrics",
|
||||
Default: false,
|
||||
},
|
||||
{
|
||||
Key: "enable_network_metrics",
|
||||
Type: ConfigTypeBool,
|
||||
Description: "Enable network interface metrics",
|
||||
Default: true,
|
||||
},
|
||||
{
|
||||
Key: "log_level",
|
||||
Type: ConfigTypeEnum,
|
||||
Description: "Agent log verbosity level",
|
||||
Default: "info",
|
||||
Enum: []string{"debug", "info", "warn", "error"},
|
||||
},
|
||||
{
|
||||
Key: "metric_buffer_size",
|
||||
Type: ConfigTypeInt,
|
||||
Description: "Size of the metric buffer before flush",
|
||||
Default: 100,
|
||||
Min: ptrFloat(10),
|
||||
Max: ptrFloat(10000),
|
||||
},
|
||||
{
|
||||
Key: "connection_timeout",
|
||||
Type: ConfigTypeDuration,
|
||||
Description: "Timeout for server connections",
|
||||
Default: "30s",
|
||||
},
|
||||
{
|
||||
Key: "retry_interval",
|
||||
Type: ConfigTypeDuration,
|
||||
Description: "Interval between connection retries",
|
||||
Default: "5s",
|
||||
},
|
||||
{
|
||||
Key: "max_retries",
|
||||
Type: ConfigTypeInt,
|
||||
Description: "Maximum number of connection retries",
|
||||
Default: 3,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "disk_paths",
|
||||
Type: ConfigTypeString,
|
||||
Description: "Comma-separated list of disk paths to monitor",
|
||||
Default: "/",
|
||||
},
|
||||
{
|
||||
Key: "exclude_containers",
|
||||
Type: ConfigTypeString,
|
||||
Description: "Regex pattern for container names to exclude",
|
||||
Default: "",
|
||||
},
|
||||
{
|
||||
Key: "include_containers",
|
||||
Type: ConfigTypeString,
|
||||
Description: "Regex pattern for container names to include (empty = all)",
|
||||
Default: "",
|
||||
},
|
||||
{
|
||||
Key: "cpu_threshold_warning",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "CPU usage threshold for warnings (%)",
|
||||
Default: 80.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "cpu_threshold_critical",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "CPU usage threshold for critical alerts (%)",
|
||||
Default: 95.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "memory_threshold_warning",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "Memory usage threshold for warnings (%)",
|
||||
Default: 80.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "memory_threshold_critical",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "Memory usage threshold for critical alerts (%)",
|
||||
Default: 95.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "disk_threshold_warning",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "Disk usage threshold for warnings (%)",
|
||||
Default: 80.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
{
|
||||
Key: "disk_threshold_critical",
|
||||
Type: ConfigTypeFloat,
|
||||
Description: "Disk usage threshold for critical alerts (%)",
|
||||
Default: 95.0,
|
||||
Min: ptrFloat(0),
|
||||
Max: ptrFloat(100),
|
||||
},
|
||||
}
|
||||
|
||||
// ValidationError represents a validation error for a config key.
|
||||
type ValidationError struct {
|
||||
Key string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Key, e.Message)
|
||||
}
|
||||
|
||||
// ValidationResult holds the result of config validation.
|
||||
type ValidationResult struct {
|
||||
Valid bool
|
||||
Errors []ValidationError
|
||||
Warnings []ValidationError
|
||||
}
|
||||
|
||||
// ProfileValidator validates agent profile configurations.
|
||||
type ProfileValidator struct {
|
||||
keyDefs map[string]ConfigKeyDefinition
|
||||
}
|
||||
|
||||
// NewProfileValidator creates a new profile validator.
|
||||
func NewProfileValidator() *ProfileValidator {
|
||||
keyDefs := make(map[string]ConfigKeyDefinition)
|
||||
for _, def := range ValidConfigKeys {
|
||||
keyDefs[def.Key] = def
|
||||
}
|
||||
return &ProfileValidator{keyDefs: keyDefs}
|
||||
}
|
||||
|
||||
// Validate validates an agent profile configuration.
|
||||
func (v *ProfileValidator) Validate(config AgentConfigMap) ValidationResult {
|
||||
result := ValidationResult{Valid: true}
|
||||
|
||||
// Check for unknown keys
|
||||
for key := range config {
|
||||
if _, ok := v.keyDefs[key]; !ok {
|
||||
result.Warnings = append(result.Warnings, ValidationError{
|
||||
Key: key,
|
||||
Message: "Unknown configuration key (will be ignored by agent)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate known keys
|
||||
for key, value := range config {
|
||||
def, ok := v.keyDefs[key]
|
||||
if !ok {
|
||||
continue // Skip unknown keys (already warned)
|
||||
}
|
||||
|
||||
if err := v.validateValue(def, value); err != nil {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, ValidationError{
|
||||
Key: key,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for required keys
|
||||
for _, def := range v.keyDefs {
|
||||
if def.Required {
|
||||
if _, ok := config[def.Key]; !ok {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, ValidationError{
|
||||
Key: def.Key,
|
||||
Message: "Required configuration key is missing",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// validateValue validates a single configuration value against its definition.
|
||||
func (v *ProfileValidator) validateValue(def ConfigKeyDefinition, value interface{}) error {
|
||||
if value == nil {
|
||||
if def.Required {
|
||||
return fmt.Errorf("value cannot be null")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch def.Type {
|
||||
case ConfigTypeString:
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected string, got %T", value)
|
||||
}
|
||||
if def.Pattern != "" {
|
||||
re, err := regexp.Compile(def.Pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern in definition: %v", err)
|
||||
}
|
||||
if !re.MatchString(s) {
|
||||
return fmt.Errorf("value does not match pattern %s", def.Pattern)
|
||||
}
|
||||
}
|
||||
|
||||
case ConfigTypeBool:
|
||||
if _, ok := value.(bool); !ok {
|
||||
return fmt.Errorf("expected boolean, got %T", value)
|
||||
}
|
||||
|
||||
case ConfigTypeInt:
|
||||
var num float64
|
||||
switch n := value.(type) {
|
||||
case int:
|
||||
num = float64(n)
|
||||
case int64:
|
||||
num = float64(n)
|
||||
case float64:
|
||||
if n != float64(int64(n)) {
|
||||
return fmt.Errorf("expected integer, got float")
|
||||
}
|
||||
num = n
|
||||
default:
|
||||
return fmt.Errorf("expected integer, got %T", value)
|
||||
}
|
||||
if def.Min != nil && num < *def.Min {
|
||||
return fmt.Errorf("value %v is below minimum %v", num, *def.Min)
|
||||
}
|
||||
if def.Max != nil && num > *def.Max {
|
||||
return fmt.Errorf("value %v exceeds maximum %v", num, *def.Max)
|
||||
}
|
||||
|
||||
case ConfigTypeFloat:
|
||||
var num float64
|
||||
switch n := value.(type) {
|
||||
case int:
|
||||
num = float64(n)
|
||||
case int64:
|
||||
num = float64(n)
|
||||
case float64:
|
||||
num = n
|
||||
default:
|
||||
return fmt.Errorf("expected number, got %T", value)
|
||||
}
|
||||
if def.Min != nil && num < *def.Min {
|
||||
return fmt.Errorf("value %v is below minimum %v", num, *def.Min)
|
||||
}
|
||||
if def.Max != nil && num > *def.Max {
|
||||
return fmt.Errorf("value %v exceeds maximum %v", num, *def.Max)
|
||||
}
|
||||
|
||||
case ConfigTypeDuration:
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected duration string, got %T", value)
|
||||
}
|
||||
if _, err := time.ParseDuration(s); err != nil {
|
||||
return fmt.Errorf("invalid duration format: %v", err)
|
||||
}
|
||||
|
||||
case ConfigTypeEnum:
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected string, got %T", value)
|
||||
}
|
||||
found := false
|
||||
for _, allowed := range def.Enum {
|
||||
if strings.EqualFold(s, allowed) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("value must be one of: %s", strings.Join(def.Enum, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigKeyDefinitions returns all valid configuration key definitions.
|
||||
func GetConfigKeyDefinitions() []ConfigKeyDefinition {
|
||||
return ValidConfigKeys
|
||||
}
|
||||
|
||||
// GetConfigKeyDefinition returns the definition for a specific key.
|
||||
func GetConfigKeyDefinition(key string) (ConfigKeyDefinition, bool) {
|
||||
for _, def := range ValidConfigKeys {
|
||||
if def.Key == key {
|
||||
return def, true
|
||||
}
|
||||
}
|
||||
return ConfigKeyDefinition{}, false
|
||||
}
|
||||
|
||||
// ptrFloat returns a pointer to a float64.
|
||||
func ptrFloat(v float64) *float64 {
|
||||
return &v
|
||||
}
|
||||
494
internal/models/profile_validation_test.go
Normal file
494
internal/models/profile_validation_test.go
Normal file
@@ -0,0 +1,494 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfileValidator_ValidateStringType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid string",
|
||||
config: AgentConfigMap{"disk_paths": "/,/home"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid string type",
|
||||
config: AgentConfigMap{"disk_paths": 123},
|
||||
wantErr: true,
|
||||
errKey: "disk_paths",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
if tt.wantErr && len(result.Errors) > 0 {
|
||||
found := false
|
||||
for _, err := range result.Errors {
|
||||
if err.Key == tt.errKey {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected error for key %s, but got: %v", tt.errKey, result.Errors)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ValidateBoolType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid bool true",
|
||||
config: AgentConfigMap{"enable_docker": true},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid bool false",
|
||||
config: AgentConfigMap{"enable_docker": false},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid bool type - string",
|
||||
config: AgentConfigMap{"enable_docker": "true"},
|
||||
wantErr: true,
|
||||
errKey: "enable_docker",
|
||||
},
|
||||
{
|
||||
name: "invalid bool type - int",
|
||||
config: AgentConfigMap{"enable_docker": 1},
|
||||
wantErr: true,
|
||||
errKey: "enable_docker",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ValidateIntType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
config: AgentConfigMap{"metric_buffer_size": 100},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid int as float64 (JSON unmarshal)",
|
||||
config: AgentConfigMap{"metric_buffer_size": float64(100)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "int below minimum",
|
||||
config: AgentConfigMap{"metric_buffer_size": 5},
|
||||
wantErr: true,
|
||||
errKey: "metric_buffer_size",
|
||||
},
|
||||
{
|
||||
name: "int above maximum",
|
||||
config: AgentConfigMap{"metric_buffer_size": 20000},
|
||||
wantErr: true,
|
||||
errKey: "metric_buffer_size",
|
||||
},
|
||||
{
|
||||
name: "invalid int type - string",
|
||||
config: AgentConfigMap{"metric_buffer_size": "100"},
|
||||
wantErr: true,
|
||||
errKey: "metric_buffer_size",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ValidateFloatType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid float",
|
||||
config: AgentConfigMap{"cpu_threshold_warning": 80.5},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid float as int",
|
||||
config: AgentConfigMap{"cpu_threshold_warning": 80},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "float below minimum",
|
||||
config: AgentConfigMap{"cpu_threshold_warning": -10.0},
|
||||
wantErr: true,
|
||||
errKey: "cpu_threshold_warning",
|
||||
},
|
||||
{
|
||||
name: "float above maximum",
|
||||
config: AgentConfigMap{"cpu_threshold_warning": 150.0},
|
||||
wantErr: true,
|
||||
errKey: "cpu_threshold_warning",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ValidateDurationType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid duration seconds",
|
||||
config: AgentConfigMap{"interval": "30s"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid duration minutes",
|
||||
config: AgentConfigMap{"interval": "5m"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid duration hours",
|
||||
config: AgentConfigMap{"interval": "1h"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid duration complex",
|
||||
config: AgentConfigMap{"interval": "1h30m45s"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid duration format",
|
||||
config: AgentConfigMap{"interval": "30"},
|
||||
wantErr: true,
|
||||
errKey: "interval",
|
||||
},
|
||||
{
|
||||
name: "invalid duration - not a string",
|
||||
config: AgentConfigMap{"interval": 30},
|
||||
wantErr: true,
|
||||
errKey: "interval",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ValidateEnumType(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config AgentConfigMap
|
||||
wantErr bool
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
name: "valid enum debug",
|
||||
config: AgentConfigMap{"log_level": "debug"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid enum info",
|
||||
config: AgentConfigMap{"log_level": "info"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid enum warn",
|
||||
config: AgentConfigMap{"log_level": "warn"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid enum error",
|
||||
config: AgentConfigMap{"log_level": "error"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid enum value",
|
||||
config: AgentConfigMap{"log_level": "trace"},
|
||||
wantErr: true,
|
||||
errKey: "log_level",
|
||||
},
|
||||
{
|
||||
name: "invalid enum type",
|
||||
config: AgentConfigMap{"log_level": 1},
|
||||
wantErr: true,
|
||||
errKey: "log_level",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validator.Validate(tt.config)
|
||||
if tt.wantErr && result.Valid {
|
||||
t.Errorf("expected validation to fail, but it passed")
|
||||
}
|
||||
if !tt.wantErr && !result.Valid {
|
||||
t.Errorf("expected validation to pass, but it failed: %v", result.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_UnknownKeysWarning(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
config := AgentConfigMap{
|
||||
"interval": "30s",
|
||||
"unknown_key": "some_value",
|
||||
}
|
||||
|
||||
result := validator.Validate(config)
|
||||
|
||||
// Should still be valid (warnings don't fail validation)
|
||||
if !result.Valid {
|
||||
t.Errorf("expected validation to pass with warnings, but it failed: %v", result.Errors)
|
||||
}
|
||||
|
||||
// Should have a warning for unknown key
|
||||
if len(result.Warnings) == 0 {
|
||||
t.Errorf("expected a warning for unknown key, but got none")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, w := range result.Warnings {
|
||||
if w.Key == "unknown_key" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected warning for 'unknown_key', got: %v", result.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_ComplexConfig(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
config := AgentConfigMap{
|
||||
"interval": "30s",
|
||||
"enable_docker": true,
|
||||
"enable_system_metrics": true,
|
||||
"enable_process_metrics": false,
|
||||
"log_level": "info",
|
||||
"metric_buffer_size": 100,
|
||||
"cpu_threshold_warning": 80.0,
|
||||
"cpu_threshold_critical": 95.0,
|
||||
"disk_paths": "/,/home",
|
||||
}
|
||||
|
||||
result := validator.Validate(config)
|
||||
|
||||
if !result.Valid {
|
||||
t.Errorf("expected complex config to be valid, but got errors: %v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidator_NullValue(t *testing.T) {
|
||||
validator := NewProfileValidator()
|
||||
|
||||
config := AgentConfigMap{
|
||||
"interval": nil,
|
||||
}
|
||||
|
||||
result := validator.Validate(config)
|
||||
|
||||
// Null values should be valid for non-required keys
|
||||
if !result.Valid {
|
||||
t.Errorf("expected null value to be valid for non-required key, but got errors: %v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigKeyDefinitions(t *testing.T) {
|
||||
defs := GetConfigKeyDefinitions()
|
||||
|
||||
if len(defs) == 0 {
|
||||
t.Error("expected config key definitions to be non-empty")
|
||||
}
|
||||
|
||||
// Check some known keys exist
|
||||
expectedKeys := []string{"interval", "enable_docker", "log_level", "metric_buffer_size"}
|
||||
for _, key := range expectedKeys {
|
||||
found := false
|
||||
for _, def := range defs {
|
||||
if def.Key == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected key %s to be in definitions", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigKeyDefinition(t *testing.T) {
|
||||
def, found := GetConfigKeyDefinition("interval")
|
||||
if !found {
|
||||
t.Error("expected to find 'interval' definition")
|
||||
}
|
||||
if def.Type != ConfigTypeDuration {
|
||||
t.Errorf("expected 'interval' type to be Duration, got %s", def.Type)
|
||||
}
|
||||
|
||||
_, found = GetConfigKeyDefinition("nonexistent_key")
|
||||
if found {
|
||||
t.Error("expected not to find 'nonexistent_key' definition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProfile_MergedConfig(t *testing.T) {
|
||||
parentProfile := AgentProfile{
|
||||
ID: "parent-1",
|
||||
Name: "Parent Profile",
|
||||
Config: AgentConfigMap{
|
||||
"interval": "30s",
|
||||
"enable_docker": true,
|
||||
"log_level": "info",
|
||||
},
|
||||
}
|
||||
|
||||
childProfile := AgentProfile{
|
||||
ID: "child-1",
|
||||
Name: "Child Profile",
|
||||
ParentID: "parent-1",
|
||||
Config: AgentConfigMap{
|
||||
"interval": "10s", // Override parent
|
||||
"log_level": "debug", // Override parent
|
||||
},
|
||||
}
|
||||
|
||||
profiles := []AgentProfile{parentProfile, childProfile}
|
||||
|
||||
merged := childProfile.MergedConfig(profiles)
|
||||
|
||||
// Child's interval should override parent's
|
||||
if merged["interval"] != "10s" {
|
||||
t.Errorf("expected interval to be '10s', got %v", merged["interval"])
|
||||
}
|
||||
|
||||
// Child's log_level should override parent's
|
||||
if merged["log_level"] != "debug" {
|
||||
t.Errorf("expected log_level to be 'debug', got %v", merged["log_level"])
|
||||
}
|
||||
|
||||
// Parent's enable_docker should be inherited
|
||||
if merged["enable_docker"] != true {
|
||||
t.Errorf("expected enable_docker to be true, got %v", merged["enable_docker"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProfile_MergedConfigNoParent(t *testing.T) {
|
||||
profile := AgentProfile{
|
||||
ID: "profile-1",
|
||||
Name: "Standalone Profile",
|
||||
Config: AgentConfigMap{
|
||||
"interval": "30s",
|
||||
},
|
||||
}
|
||||
|
||||
profiles := []AgentProfile{profile}
|
||||
|
||||
merged := profile.MergedConfig(profiles)
|
||||
|
||||
if merged["interval"] != "30s" {
|
||||
t.Errorf("expected interval to be '30s', got %v", merged["interval"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProfile_MergedConfigParentNotFound(t *testing.T) {
|
||||
profile := AgentProfile{
|
||||
ID: "child-1",
|
||||
Name: "Child Profile",
|
||||
ParentID: "nonexistent-parent",
|
||||
Config: AgentConfigMap{
|
||||
"interval": "10s",
|
||||
},
|
||||
}
|
||||
|
||||
profiles := []AgentProfile{profile}
|
||||
|
||||
// Should return just the child's config if parent not found
|
||||
merged := profile.MergedConfig(profiles)
|
||||
|
||||
if merged["interval"] != "10s" {
|
||||
t.Errorf("expected interval to be '10s', got %v", merged["interval"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user