Files
logcorrelator/internal/config/config_test.go
Jacquin Antoine 24aa84bd9c
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / docker (push) Has been cancelled
test: add comprehensive tests to improve coverage
- observability: added tests for LogLevel.String(), Warn(), Warnf(), Infof(),
  Debugf(), Error(), WithFields(), and concurrent access patterns
- file: added tests for Reopen(), Close(), empty/whitespace paths,
  validateFilePath allowed/rejected paths, concurrent writes, Flush(),
  and marshal errors
- config: added tests for TimeWindowConfig.GetDuration(),
  CorrelationConfig getters, validation scenarios (no inputs, no outputs,
  duplicate sockets, ClickHouse validation), and LogConfig.GetLevel()

Coverage improvements:
- observability: 57.7% → 79.5%
- file: 68.6% → 78.6%
- config: 69.8% → 97.7%
- total: 68.6% → 74.4%

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 22:52:09 +01:00

941 lines
20 KiB
Go

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: 0666,
},
{
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: 0666,
},
}
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")
}
}
func TestTimeWindowConfig_GetDuration(t *testing.T) {
tests := []struct {
name string
config TimeWindowConfig
expected time.Duration
}{
{
name: "seconds",
config: TimeWindowConfig{Value: 5, Unit: "s"},
expected: 5 * time.Second,
},
{
name: "milliseconds",
config: TimeWindowConfig{Value: 500, Unit: "ms"},
expected: 500 * time.Millisecond,
},
{
name: "default on zero value",
config: TimeWindowConfig{Value: 0, Unit: "s"},
expected: 1 * time.Second,
},
{
name: "unknown unit defaults to seconds",
config: TimeWindowConfig{Value: 5, Unit: "unknown"},
expected: 5 * time.Second,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.GetDuration()
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
func TestCorrelationConfig_GetTimeWindow(t *testing.T) {
cfg := CorrelationConfig{
TimeWindow: TimeWindowConfig{Value: 2, Unit: "s"},
}
result := cfg.GetTimeWindow()
if result != 2*time.Second {
t.Errorf("expected 2s, got %v", result)
}
}
func TestCorrelationConfig_GetTimeWindow_Default(t *testing.T) {
cfg := CorrelationConfig{
TimeWindow: TimeWindowConfig{Value: 0, Unit: "s"},
}
result := cfg.GetTimeWindow()
if result != 1*time.Second {
t.Errorf("expected default 1s, got %v", result)
}
}
func TestCorrelationConfig_GetMatchingMode(t *testing.T) {
tests := []struct {
name string
config CorrelationConfig
expected string
}{
{
name: "one_to_many",
config: CorrelationConfig{Matching: MatchingConfig{Mode: "one_to_many"}},
expected: "one_to_many",
},
{
name: "one_to_one",
config: CorrelationConfig{Matching: MatchingConfig{Mode: "one_to_one"}},
expected: "one_to_one",
},
{
name: "default empty",
config: CorrelationConfig{Matching: MatchingConfig{Mode: ""}},
expected: "one_to_many",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.config.GetMatchingMode()
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}
func TestCorrelationConfig_GetApacheAlwaysEmit(t *testing.T) {
cfg := CorrelationConfig{
OrphanPolicy: OrphanPolicyConfig{ApacheAlwaysEmit: true},
}
if !cfg.GetApacheAlwaysEmit() {
t.Error("expected true")
}
}
func TestCorrelationConfig_GetApacheAlwaysEmit_Deprecated(t *testing.T) {
cfg := CorrelationConfig{
EmitOrphans: true, // deprecated field
}
if !cfg.GetApacheAlwaysEmit() {
t.Error("expected true from deprecated field")
}
}
func TestCorrelationConfig_GetNetworkTTLS(t *testing.T) {
cfg := CorrelationConfig{
TTL: TTLConfig{NetworkTTLS: 60},
}
result := cfg.GetNetworkTTLS()
if result != 60 {
t.Errorf("expected 60, got %d", result)
}
}
func TestCorrelationConfig_GetNetworkTTLS_Default(t *testing.T) {
cfg := CorrelationConfig{
TTL: TTLConfig{NetworkTTLS: 0},
}
result := cfg.GetNetworkTTLS()
if result != 30 {
t.Errorf("expected default 30, got %d", result)
}
}
func TestCorrelationConfig_GetMaxHTTPBufferSize(t *testing.T) {
cfg := CorrelationConfig{
Buffers: BuffersConfig{MaxHTTPItems: 5000},
}
result := cfg.GetMaxHTTPBufferSize()
if result != 5000 {
t.Errorf("expected 5000, got %d", result)
}
}
func TestCorrelationConfig_GetMaxHTTPBufferSize_Default(t *testing.T) {
cfg := CorrelationConfig{
Buffers: BuffersConfig{MaxHTTPItems: 0},
}
result := cfg.GetMaxHTTPBufferSize()
if result != 10000 {
t.Errorf("expected default 10000, got %d", result)
}
}
func TestCorrelationConfig_GetMaxNetworkBufferSize(t *testing.T) {
cfg := CorrelationConfig{
Buffers: BuffersConfig{MaxNetworkItems: 15000},
}
result := cfg.GetMaxNetworkBufferSize()
if result != 15000 {
t.Errorf("expected 15000, got %d", result)
}
}
func TestCorrelationConfig_GetMaxNetworkBufferSize_Default(t *testing.T) {
cfg := CorrelationConfig{
Buffers: BuffersConfig{MaxNetworkItems: 0},
}
result := cfg.GetMaxNetworkBufferSize()
if result != 20000 {
t.Errorf("expected default 20000, got %d", result)
}
}
func TestUnixSocketConfig_GetSocketPermissions_Invalid(t *testing.T) {
cfg := UnixSocketConfig{
SocketPermissions: "invalid",
}
result := cfg.GetSocketPermissions()
if result != 0666 {
t.Errorf("expected default 0666 for invalid value, got %o", result)
}
}
func TestLoadConfig_Validation(t *testing.T) {
// Test config with no inputs
content := `
outputs:
file:
path: /var/log/test.log
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for config with no inputs")
}
}
func TestLoadConfig_Validation_NoOutputs(t *testing.T) {
// Test config with no outputs
content := `
inputs:
unix_sockets:
- name: http
path: /var/run/logcorrelator/http.socket
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for config with no outputs")
}
}
func TestLoadConfig_Validation_DuplicateSocketNames(t *testing.T) {
content := `
inputs:
unix_sockets:
- name: http
path: /var/run/logcorrelator/http.socket
- name: http
path: /var/run/logcorrelator/other.socket
outputs:
file:
path: /var/log/test.log
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for duplicate socket names")
}
}
func TestLoadConfig_Validation_DuplicateSocketPaths(t *testing.T) {
content := `
inputs:
unix_sockets:
- name: http
path: /var/run/logcorrelator/http.socket
- name: network
path: /var/run/logcorrelator/http.socket
outputs:
file:
path: /var/log/test.log
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for duplicate socket paths")
}
}
func TestLoadConfig_Validation_ClickHouseEnabled(t *testing.T) {
content := `
inputs:
unix_sockets:
- name: http
path: /var/run/logcorrelator/http.socket
outputs:
clickhouse:
enabled: true
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for ClickHouse enabled without DSN")
}
}
func TestLoadConfig_Validation_ClickHouseEnabled_NoTable(t *testing.T) {
content := `
inputs:
unix_sockets:
- name: http
path: /var/run/logcorrelator/http.socket
outputs:
clickhouse:
enabled: true
dsn: clickhouse://localhost:9000/db
correlation:
time_window_s: 1
`
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)
}
_, err := Load(configPath)
if err == nil {
t.Error("expected error for ClickHouse enabled without table")
}
}