| package config |
|
|
| import ( |
| "strings" |
| "testing" |
| ) |
|
|
| func TestValidateFileConfig_Valid(t *testing.T) { |
| maxTabs := 20 |
| fc := &FileConfig{ |
| Server: ServerConfig{ |
| Port: "9867", |
| Bind: "127.0.0.1", |
| }, |
| InstanceDefaults: InstanceDefaultsConfig{ |
| Mode: "headless", |
| MaxTabs: &maxTabs, |
| StealthLevel: "light", |
| TabEvictionPolicy: "reject", |
| }, |
| MultiInstance: MultiInstanceConfig{ |
| Strategy: "simple", |
| AllocationPolicy: "fcfs", |
| Restart: MultiInstanceRestartConfig{ |
| MaxRestarts: intPtr(20), |
| InitBackoffSec: intPtr(2), |
| MaxBackoffSec: intPtr(60), |
| StableAfterSec: intPtr(300), |
| }, |
| }, |
| Timeouts: TimeoutsConfig{ |
| ActionSec: 30, |
| NavigateSec: 60, |
| }, |
| } |
|
|
| errs := ValidateFileConfig(fc) |
| if len(errs) > 0 { |
| t.Errorf("expected no errors for valid config, got: %v", errs) |
| } |
| } |
|
|
| func TestValidateFileConfig_RestartPolicy(t *testing.T) { |
| tests := []struct { |
| name string |
| restart MultiInstanceRestartConfig |
| wantErr bool |
| }{ |
| { |
| name: "bounded", |
| restart: MultiInstanceRestartConfig{ |
| MaxRestarts: intPtr(10), |
| InitBackoffSec: intPtr(2), |
| MaxBackoffSec: intPtr(60), |
| StableAfterSec: intPtr(300), |
| }, |
| wantErr: false, |
| }, |
| { |
| name: "unlimited", |
| restart: MultiInstanceRestartConfig{ |
| MaxRestarts: intPtr(-1), |
| InitBackoffSec: intPtr(2), |
| MaxBackoffSec: intPtr(60), |
| StableAfterSec: intPtr(300), |
| }, |
| wantErr: false, |
| }, |
| { |
| name: "zero max restarts valid (no restarts)", |
| restart: MultiInstanceRestartConfig{ |
| MaxRestarts: intPtr(0), |
| }, |
| wantErr: false, |
| }, |
| { |
| name: "negative max restarts invalid (except -1)", |
| restart: MultiInstanceRestartConfig{ |
| MaxRestarts: intPtr(-2), |
| }, |
| wantErr: true, |
| }, |
| { |
| name: "max backoff lower than init invalid", |
| restart: MultiInstanceRestartConfig{ |
| InitBackoffSec: intPtr(10), |
| MaxBackoffSec: intPtr(5), |
| }, |
| wantErr: true, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| fc := &FileConfig{ |
| MultiInstance: MultiInstanceConfig{Restart: tt.restart}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Fatalf("got error=%v, want %v (errs: %v)", hasErr, tt.wantErr, errs) |
| } |
| }) |
| } |
| } |
|
|
| func intPtr(v int) *int { return &v } |
|
|
| func TestValidateFileConfig_InvalidPort(t *testing.T) { |
| tests := []struct { |
| port string |
| wantErr bool |
| }{ |
| {"9867", false}, |
| {"1", false}, |
| {"65535", false}, |
| {"0", true}, |
| {"65536", true}, |
| {"-1", true}, |
| {"abc", true}, |
| {"", false}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| Server: ServerConfig{Port: tt.port}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("port=%q: got error=%v, want error=%v (errs: %v)", tt.port, hasErr, tt.wantErr, errs) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidStealthLevel(t *testing.T) { |
| tests := []struct { |
| level string |
| wantErr bool |
| }{ |
| {"light", false}, |
| {"full", false}, |
| {"", false}, |
| {"medium", true}, |
| {"none", true}, |
| {"max", true}, |
| {"LIGHT", true}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| InstanceDefaults: InstanceDefaultsConfig{StealthLevel: tt.level}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("stealthLevel=%q: got error=%v, want error=%v", tt.level, hasErr, tt.wantErr) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidEvictionPolicy(t *testing.T) { |
| tests := []struct { |
| policy string |
| wantErr bool |
| }{ |
| {"reject", false}, |
| {"close_oldest", false}, |
| {"close_lru", false}, |
| {"", false}, |
| {"drop", true}, |
| {"lru", true}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| InstanceDefaults: InstanceDefaultsConfig{TabEvictionPolicy: tt.policy}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("tabEvictionPolicy=%q: got error=%v, want error=%v", tt.policy, hasErr, tt.wantErr) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidStrategy(t *testing.T) { |
| tests := []struct { |
| strategy string |
| wantErr bool |
| }{ |
| {"simple", false}, |
| {"explicit", false}, |
| {"simple-autorestart", false}, |
| {"always-on", false}, |
| {"", false}, |
| {"auto", true}, |
| {"default", true}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| MultiInstance: MultiInstanceConfig{Strategy: tt.strategy}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("strategy=%q: got error=%v, want error=%v", tt.strategy, hasErr, tt.wantErr) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidAllocationPolicy(t *testing.T) { |
| tests := []struct { |
| policy string |
| wantErr bool |
| }{ |
| {"fcfs", false}, |
| {"round_robin", false}, |
| {"random", false}, |
| {"", false}, |
| {"fifo", true}, |
| {"roundrobin", true}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| MultiInstance: MultiInstanceConfig{AllocationPolicy: tt.policy}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("allocationPolicy=%q: got error=%v, want error=%v", tt.policy, hasErr, tt.wantErr) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidAttachScheme(t *testing.T) { |
| tests := []struct { |
| schemes []string |
| wantErr bool |
| }{ |
| {[]string{"ws"}, false}, |
| {[]string{"wss"}, false}, |
| {[]string{"ws", "wss"}, false}, |
| {[]string{"http"}, false}, |
| {[]string{"https"}, false}, |
| {[]string{"ws", "https"}, false}, |
| {[]string{"ftp"}, true}, |
| {[]string{"ws", "tcp"}, true}, |
| } |
|
|
| for _, tt := range tests { |
| fc := &FileConfig{ |
| Security: SecurityConfig{ |
| Attach: AttachConfig{AllowSchemes: tt.schemes}, |
| }, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("allowSchemes=%v: got error=%v, want error=%v", tt.schemes, hasErr, tt.wantErr) |
| } |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidMaxTabs(t *testing.T) { |
| zero := 0 |
| negative := -1 |
| positive := 10 |
|
|
| tests := []struct { |
| name string |
| maxTabs *int |
| wantErr bool |
| }{ |
| {"nil", nil, false}, |
| {"positive", &positive, false}, |
| {"zero", &zero, true}, |
| {"negative", &negative, true}, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| fc := &FileConfig{ |
| InstanceDefaults: InstanceDefaultsConfig{MaxTabs: tt.maxTabs}, |
| } |
| errs := ValidateFileConfig(fc) |
| hasErr := len(errs) > 0 |
| if hasErr != tt.wantErr { |
| t.Errorf("maxTabs=%v: got error=%v, want error=%v", tt.maxTabs, hasErr, tt.wantErr) |
| } |
| }) |
| } |
| } |
|
|
| func TestValidateFileConfig_InvalidTimeouts(t *testing.T) { |
| fc := &FileConfig{ |
| Timeouts: TimeoutsConfig{ |
| ActionSec: -1, |
| NavigateSec: -1, |
| ShutdownSec: -1, |
| WaitNavMs: -1, |
| }, |
| } |
|
|
| errs := ValidateFileConfig(fc) |
| if len(errs) != 4 { |
| t.Errorf("expected 4 timeout errors, got %d: %v", len(errs), errs) |
| } |
| } |
|
|
| func TestValidateFileConfig_InstancePortRange(t *testing.T) { |
| start := 9900 |
| end := 9800 |
|
|
| fc := &FileConfig{ |
| Server: ServerConfig{}, |
| MultiInstance: MultiInstanceConfig{ |
| InstancePortStart: &start, |
| InstancePortEnd: &end, |
| }, |
| } |
|
|
| errs := ValidateFileConfig(fc) |
| if len(errs) != 1 { |
| t.Errorf("expected 1 error for invalid port range, got %d: %v", len(errs), errs) |
| } |
| if len(errs) > 0 && !strings.Contains(errs[0].Error(), "start port") { |
| t.Errorf("expected port range error, got: %v", errs[0]) |
| } |
| } |
|
|
| func TestValidateFileConfig_MultipleErrors(t *testing.T) { |
| zero := 0 |
| fc := &FileConfig{ |
| Server: ServerConfig{ |
| Port: "99999", |
| }, |
| InstanceDefaults: InstanceDefaultsConfig{ |
| MaxTabs: &zero, |
| StealthLevel: "superstealth", |
| TabEvictionPolicy: "delete_oldest", |
| }, |
| MultiInstance: MultiInstanceConfig{ |
| Strategy: "magical", |
| AllocationPolicy: "balanced", |
| }, |
| } |
|
|
| errs := ValidateFileConfig(fc) |
| if len(errs) < 5 { |
| t.Errorf("expected at least 5 errors, got %d: %v", len(errs), errs) |
| } |
| } |
|
|
| func TestValidationError_Error(t *testing.T) { |
| err := ValidationError{ |
| Field: "server.port", |
| Message: "port out of range", |
| } |
| expected := "server.port: port out of range" |
| if err.Error() != expected { |
| t.Errorf("got %q, want %q", err.Error(), expected) |
| } |
| } |
|
|
| func TestValidEnumValues(t *testing.T) { |
| |
| for _, level := range ValidStealthLevels() { |
| if !isValidStealthLevel(level) { |
| t.Errorf("ValidStealthLevels contains %q but isValidStealthLevel returns false", level) |
| } |
| } |
|
|
| for _, policy := range ValidEvictionPolicies() { |
| if !isValidEvictionPolicy(policy) { |
| t.Errorf("ValidEvictionPolicies contains %q but isValidEvictionPolicy returns false", policy) |
| } |
| } |
|
|
| for _, strategy := range ValidStrategies() { |
| if !isValidStrategy(strategy) { |
| t.Errorf("ValidStrategies contains %q but isValidStrategy returns false", strategy) |
| } |
| } |
|
|
| for _, policy := range ValidAllocationPolicies() { |
| if !isValidAllocationPolicy(policy) { |
| t.Errorf("ValidAllocationPolicies contains %q but isValidAllocationPolicy returns false", policy) |
| } |
| } |
|
|
| for _, scheme := range ValidAttachSchemes() { |
| if !isValidAttachScheme(scheme) { |
| t.Errorf("ValidAttachSchemes contains %q but isValidAttachScheme returns false", scheme) |
| } |
| } |
| } |
|
|
| |
|
|
| |
| |
| func TestValidateIDPIConfig_Disabled(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: false, |
| AllowedDomains: []string{"", " ", "file:///etc/passwd"}, |
| CustomPatterns: []string{"", " "}, |
| }) |
| if len(errs) != 0 { |
| t.Errorf("expected no errors when IDPI disabled, got: %v", errs) |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_ValidConfig(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{"example.com", "*.example.com", "*"}, |
| CustomPatterns: []string{"exfiltrate this", "data leak"}, |
| }) |
| if len(errs) != 0 { |
| t.Errorf("expected no errors for valid IDPI config, got: %v", errs) |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_EmptyDomain(t *testing.T) { |
| cases := []struct { |
| name string |
| domain string |
| }{ |
| {"empty string", ""}, |
| {"spaces only", " "}, |
| {"tab only", "\t"}, |
| } |
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{tc.domain}, |
| }) |
| if len(errs) == 0 { |
| t.Errorf("expected error for empty domain %q, got none", tc.domain) |
| } |
| if len(errs) > 0 && !strings.Contains(errs[0].Error(), "security.idpi.allowedDomains") { |
| t.Errorf("expected field name in error, got: %v", errs[0]) |
| } |
| }) |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_DomainWithInternalWhitespace(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{"example .com"}, |
| }) |
| if len(errs) == 0 { |
| t.Error("expected error for domain with internal whitespace, got none") |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_FileSchemeBlocked(t *testing.T) { |
| for _, pattern := range []string{ |
| "file:///etc/passwd", |
| "file://localhost/etc/passwd", |
| } { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{pattern}, |
| }) |
| if len(errs) == 0 { |
| t.Errorf("expected error for file:// pattern %q, got none", pattern) |
| } |
| if len(errs) > 0 && !strings.Contains(errs[0].Error(), "file://") { |
| t.Errorf("expected file:// mention in error, got: %v", errs[0]) |
| } |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_EmptyCustomPattern(t *testing.T) { |
| cases := []string{"", " ", "\t"} |
| for _, p := range cases { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| CustomPatterns: []string{p}, |
| }) |
| if len(errs) == 0 { |
| t.Errorf("expected error for empty custom pattern %q, got none", p) |
| } |
| if len(errs) > 0 && !strings.Contains(errs[0].Error(), "customPatterns") { |
| t.Errorf("expected customPatterns field in error, got: %v", errs[0]) |
| } |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_MultipleErrors(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{"", "file:///bad"}, |
| CustomPatterns: []string{"", " "}, |
| }) |
| if len(errs) < 4 { |
| t.Errorf("expected at least 4 IDPI errors, got %d: %v", len(errs), errs) |
| } |
| } |
|
|
| |
| |
| func TestValidateFileConfig_IDPIPassthrough(t *testing.T) { |
| fc := &FileConfig{ |
| Security: SecurityConfig{ |
| IDPI: IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: []string{""}, |
| CustomPatterns: []string{" "}, |
| }, |
| }, |
| } |
| errs := ValidateFileConfig(fc) |
| if len(errs) < 2 { |
| t.Errorf("expected at least 2 IDPI errors via ValidateFileConfig, got %d: %v", len(errs), errs) |
| } |
| } |
|
|
| |
| |
| func TestValidateIDPIConfig_ScanTimeoutSec(t *testing.T) { |
| t.Run("NegativeIsInvalid", func(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| ScanTimeoutSec: -1, |
| }) |
| if len(errs) == 0 { |
| t.Error("expected error for negative scanTimeoutSec, got none") |
| } |
| if len(errs) > 0 && !strings.Contains(errs[0].Error(), "scanTimeoutSec") { |
| t.Errorf("expected scanTimeoutSec field in error, got: %v", errs[0]) |
| } |
| }) |
|
|
| t.Run("ZeroIsValid", func(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| ScanTimeoutSec: 0, |
| }) |
| if len(errs) != 0 { |
| t.Errorf("expected no error for scanTimeoutSec=0, got: %v", errs) |
| } |
| }) |
|
|
| t.Run("PositiveIsValid", func(t *testing.T) { |
| errs := validateIDPIConfig(IDPIConfig{ |
| Enabled: true, |
| ScanTimeoutSec: 10, |
| }) |
| if len(errs) != 0 { |
| t.Errorf("expected no error for scanTimeoutSec=10, got: %v", errs) |
| } |
| }) |
| } |
|
|