package config import ( "os" "path/filepath" "testing" "time" ) func TestLoad_ValidConfig(t *testing.T) { content := ` inputs: unix_sockets: - name: http path: /var/run/logcorrelator/http.socket format: json - name: network path: /var/run/logcorrelator/network.socket format: json outputs: file: path: /var/log/logcorrelator/correlated.log clickhouse: dsn: clickhouse://user:pass@localhost:9000/db table: correlated_logs correlation: time_window_s: 1 emit_orphans: true ` tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yml") if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } cfg, err := Load(configPath) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Inputs.UnixSockets) != 2 { t.Errorf("expected 2 unix sockets, got %d", len(cfg.Inputs.UnixSockets)) } if cfg.Outputs.File.Path != "/var/log/logcorrelator/correlated.log" { t.Errorf("expected file path /var/log/logcorrelator/correlated.log, got %s", cfg.Outputs.File.Path) } } func TestLoad_InvalidPath(t *testing.T) { _, err := Load("/nonexistent/path/config.yml") if err == nil { t.Error("expected error for nonexistent path") } } func TestLoad_InvalidYAML(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yml") content := `invalid: yaml: content: [` if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } _, err := Load(configPath) if err == nil { t.Error("expected error for invalid YAML") } } func TestLoad_DefaultValues(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yml") content := ` inputs: unix_sockets: - name: a path: /tmp/a.sock - name: b path: /tmp/b.sock outputs: file: path: /var/log/test.log ` if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } cfg, err := Load(configPath) if err != nil { t.Fatalf("unexpected error: %v", err) } // Check defaults if cfg.Correlation.TimeWindowS != 1 { t.Errorf("expected default time window value 1, got %d", cfg.Correlation.TimeWindowS) } if cfg.Correlation.EmitOrphans != true { t.Error("expected default emit_orphans to be true") } } func TestValidate_MinimumInputs(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "only_one", Path: "/tmp/test.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for less than 2 inputs") } } func TestValidate_AtLeastOneOutput(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{}, ClickHouse: ClickHouseOutputConfig{Enabled: false}, Stdout: StdoutOutputConfig{Enabled: false}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for no outputs enabled") } } func TestGetTimeWindow(t *testing.T) { tests := []struct { name string config CorrelationConfig expected time.Duration }{ { name: "1 second", config: CorrelationConfig{ TimeWindowS: 1, }, expected: time.Second, }, { name: "5 seconds", config: CorrelationConfig{ TimeWindowS: 5, }, expected: 5 * time.Second, }, { name: "default", config: CorrelationConfig{ TimeWindowS: 0, }, expected: time.Second, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.config.GetTimeWindow() if result != tt.expected { t.Errorf("expected %v, got %v", tt.expected, result) } }) } } func TestValidate_DuplicateNames(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "same", Path: "/tmp/a.sock"}, {Name: "same", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for duplicate names") } } func TestValidate_DuplicatePaths(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/same.sock"}, {Name: "b", Path: "/tmp/same.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for duplicate paths") } } func TestValidate_EmptyName(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for empty name") } } func TestValidate_EmptyPath(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: ""}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for empty path") } } func TestValidate_EmptyFilePath(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, }, } err := cfg.Validate() if err == nil { t.Error("expected error for empty file path") } } func TestValidate_ClickHouseMissingDSN(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, ClickHouse: ClickHouseOutputConfig{ Enabled: true, DSN: "", Table: "test", }, }, } err := cfg.Validate() if err == nil { t.Error("expected error for missing ClickHouse DSN") } } func TestValidate_ClickHouseMissingTable(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, ClickHouse: ClickHouseOutputConfig{ Enabled: true, DSN: "clickhouse://localhost:9000/db", Table: "", }, }, } err := cfg.Validate() if err == nil { t.Error("expected error for missing ClickHouse table") } } func TestValidate_ClickHouseInvalidBatchSize(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, ClickHouse: ClickHouseOutputConfig{ Enabled: true, DSN: "clickhouse://localhost:9000/db", Table: "test", BatchSize: 0, }, }, } err := cfg.Validate() if err == nil { t.Error("expected error for invalid batch size") } } func TestValidate_ClickHouseInvalidMaxBufferSize(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, ClickHouse: ClickHouseOutputConfig{ Enabled: true, DSN: "clickhouse://localhost:9000/db", Table: "test", BatchSize: 100, MaxBufferSize: 0, }, }, } err := cfg.Validate() if err == nil { t.Error("expected error for invalid max buffer size") } } func TestValidate_ClickHouseInvalidTimeout(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: ""}, ClickHouse: ClickHouseOutputConfig{ Enabled: true, DSN: "clickhouse://localhost:9000/db", Table: "test", BatchSize: 100, TimeoutMs: 0, }, }, } err := cfg.Validate() if err == nil { t.Error("expected error for invalid timeout") } } func TestValidate_EmptyCorrelationKey(t *testing.T) { cfg := &Config{ Inputs: InputsConfig{ UnixSockets: []UnixSocketConfig{ {Name: "a", Path: "/tmp/a.sock"}, {Name: "b", Path: "/tmp/b.sock"}, }, }, Outputs: OutputsConfig{ File: FileOutputConfig{Path: "/var/log/test.log"}, }, Correlation: CorrelationConfig{ TimeWindowS: 0, }, } err := cfg.Validate() if err == nil { t.Error("expected error for invalid time window") } } func TestGetSocketPermissions(t *testing.T) { tests := []struct { name string config UnixSocketConfig expected os.FileMode }{ { name: "default", config: UnixSocketConfig{ SocketPermissions: "", }, expected: 0660, }, { name: "explicit 0660", config: UnixSocketConfig{ SocketPermissions: "0660", }, expected: 0660, }, { name: "explicit 0666", config: UnixSocketConfig{ SocketPermissions: "0666", }, expected: 0666, }, { name: "without leading zero", config: UnixSocketConfig{ SocketPermissions: "660", }, expected: 0660, }, { name: "invalid value", config: UnixSocketConfig{ SocketPermissions: "invalid", }, expected: 0660, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.config.GetSocketPermissions() if result != tt.expected { t.Errorf("expected %o, got %o", tt.expected, result) } }) } } func TestLogConfig_GetLevel(t *testing.T) { tests := []struct { name string config LogConfig expected string }{ { name: "default", config: LogConfig{Level: ""}, expected: "INFO", }, { name: "DEBUG uppercase", config: LogConfig{Level: "DEBUG"}, expected: "DEBUG", }, { name: "debug lowercase", config: LogConfig{Level: "debug"}, expected: "DEBUG", }, { name: "WARN", config: LogConfig{Level: "WARN"}, expected: "WARN", }, { name: "ERROR", config: LogConfig{Level: "ERROR"}, expected: "ERROR", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.config.GetLevel() if result != tt.expected { t.Errorf("GetLevel() = %v, want %v", result, tt.expected) } }) } } func TestLoad_LogLevel(t *testing.T) { content := ` log: level: DEBUG inputs: unix_sockets: - name: http path: /var/run/logcorrelator/http.socket - name: network path: /var/run/logcorrelator/network.socket outputs: file: path: /var/log/test.log correlation: time_window_s: 1 emit_orphans: true ` tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yml") if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } cfg, err := Load(configPath) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Log.GetLevel() != "DEBUG" { t.Errorf("expected log level DEBUG, got %s", cfg.Log.GetLevel()) } } func TestLoad_StdoutEnabledObject(t *testing.T) { content := ` inputs: unix_sockets: - name: http path: /var/run/logcorrelator/http.socket - name: network path: /var/run/logcorrelator/network.socket outputs: stdout: enabled: true correlation: time_window_s: 1 emit_orphans: true ` tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yml") if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("failed to write config: %v", err) } cfg, err := Load(configPath) if err != nil { t.Fatalf("unexpected error: %v", err) } if !cfg.Outputs.Stdout.Enabled { t.Error("expected outputs.stdout.enabled to be true") } }