Pulse/cmd/pulse-agent/main_test.go
2025-12-17 13:28:17 +00:00

522 lines
11 KiB
Go

package main
import (
"reflect"
"testing"
"github.com/rs/zerolog"
)
func TestGatherTags(t *testing.T) {
tests := []struct {
name string
env string
flags []string
expected []string
}{
// Empty inputs
{
name: "empty env and flags returns empty slice",
env: "",
flags: nil,
expected: []string{},
},
{
name: "empty env and empty flags returns empty slice",
env: "",
flags: []string{},
expected: []string{},
},
// Environment only
{
name: "single env tag",
env: "prod",
flags: nil,
expected: []string{"prod"},
},
{
name: "multiple env tags comma separated",
env: "prod,us-west",
flags: nil,
expected: []string{"prod", "us-west"},
},
{
name: "env tags with whitespace trimmed",
env: " prod , us-west ",
flags: nil,
expected: []string{"prod", "us-west"},
},
{
name: "env empty items filtered",
env: "prod,,us-west,",
flags: nil,
expected: []string{"prod", "us-west"},
},
{
name: "env whitespace-only items filtered",
env: "prod, ,us-west",
flags: nil,
expected: []string{"prod", "us-west"},
},
// Flags only
{
name: "single flag tag",
env: "",
flags: []string{"staging"},
expected: []string{"staging"},
},
{
name: "multiple flag tags",
env: "",
flags: []string{"staging", "eu-central"},
expected: []string{"staging", "eu-central"},
},
{
name: "flag tags with whitespace trimmed",
env: "",
flags: []string{" staging ", " eu-central "},
expected: []string{"staging", "eu-central"},
},
{
name: "flag empty items filtered",
env: "",
flags: []string{"staging", "", "eu-central"},
expected: []string{"staging", "eu-central"},
},
{
name: "flag whitespace-only items filtered",
env: "",
flags: []string{"staging", " ", "eu-central"},
expected: []string{"staging", "eu-central"},
},
// Both env and flags (env first, then flags)
{
name: "env tags come before flags",
env: "prod",
flags: []string{"app1"},
expected: []string{"prod", "app1"},
},
{
name: "multiple env and multiple flags",
env: "prod,us-west",
flags: []string{"app1", "critical"},
expected: []string{"prod", "us-west", "app1", "critical"},
},
{
name: "duplicates preserved (no dedup)",
env: "prod,prod",
flags: []string{"prod"},
expected: []string{"prod", "prod", "prod"},
},
// Edge cases
{
name: "only commas in env",
env: ",,,",
flags: nil,
expected: []string{},
},
{
name: "single comma",
env: ",",
flags: nil,
expected: []string{},
},
{
name: "env with tabs",
env: "\tprod\t,\tstaging\t",
flags: nil,
expected: []string{"prod", "staging"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := gatherTags(tt.env, tt.flags)
if !reflect.DeepEqual(got, tt.expected) {
t.Fatalf("expected %v, got %v", tt.expected, got)
}
})
}
}
func TestGatherCSV(t *testing.T) {
tests := []struct {
name string
env string
flags []string
expected []string
}{
{"empty", "", nil, []string{}},
{"env only", "a,b", nil, []string{"a", "b"}},
{"env trims", " a , b ", nil, []string{"a", "b"}},
{"flags only", "", []string{"x", " y "}, []string{"x", "y"}},
{"both", "a", []string{"b"}, []string{"a", "b"}},
{"filters empties", "a,,", []string{"", "b", " "}, []string{"a", "b"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := gatherCSV(tc.env, tc.flags)
if !reflect.DeepEqual(got, tc.expected) {
t.Fatalf("expected %v, got %v", tc.expected, got)
}
})
}
}
func TestDefaultInt(t *testing.T) {
tests := []struct {
name string
value string
fallback int
expected int
}{
{"empty uses fallback", "", 5, 5},
{"whitespace uses fallback", " ", 5, 5},
{"valid int", "12", 5, 12},
{"invalid uses fallback", "nope", 5, 5},
{"leading whitespace", " 7", 5, 7},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := defaultInt(tc.value, tc.fallback)
if got != tc.expected {
t.Fatalf("expected %d, got %d", tc.expected, got)
}
})
}
}
func TestParseLogLevel(t *testing.T) {
tests := []struct {
name string
input string
wantLevel zerolog.Level
wantErr bool
}{
// Valid levels
{
name: "debug level",
input: "debug",
wantLevel: zerolog.DebugLevel,
},
{
name: "info level",
input: "info",
wantLevel: zerolog.InfoLevel,
},
{
name: "warn level",
input: "warn",
wantLevel: zerolog.WarnLevel,
},
{
name: "error level",
input: "error",
wantLevel: zerolog.ErrorLevel,
},
{
name: "trace level (accepted in unified agent)",
input: "trace",
wantLevel: zerolog.TraceLevel,
},
{
name: "fatal level (accepted in unified agent)",
input: "fatal",
wantLevel: zerolog.FatalLevel,
},
{
name: "panic level (accepted in unified agent)",
input: "panic",
wantLevel: zerolog.PanicLevel,
},
// Case insensitivity
{
name: "uppercase DEBUG",
input: "DEBUG",
wantLevel: zerolog.DebugLevel,
},
{
name: "mixed case Info",
input: "Info",
wantLevel: zerolog.InfoLevel,
},
{
name: "uppercase WARN",
input: "WARN",
wantLevel: zerolog.WarnLevel,
},
{
name: "uppercase ERROR",
input: "ERROR",
wantLevel: zerolog.ErrorLevel,
},
{
name: "uppercase TRACE",
input: "TRACE",
wantLevel: zerolog.TraceLevel,
},
// Whitespace handling
{
name: "leading whitespace",
input: " debug",
wantLevel: zerolog.DebugLevel,
},
{
name: "trailing whitespace",
input: "warn ",
wantLevel: zerolog.WarnLevel,
},
{
name: "both whitespace",
input: " error ",
wantLevel: zerolog.ErrorLevel,
},
{
name: "tabs",
input: "\tinfo\t",
wantLevel: zerolog.InfoLevel,
},
// Empty string defaults to info
{
name: "empty string defaults to info",
input: "",
wantLevel: zerolog.InfoLevel,
},
{
name: "whitespace only defaults to info",
input: " ",
wantLevel: zerolog.InfoLevel,
},
{
name: "tabs only defaults to info",
input: "\t\t",
wantLevel: zerolog.InfoLevel,
},
// Numeric levels (zerolog supports these)
{
name: "numeric -1 maps to trace level",
input: "-1",
wantLevel: zerolog.TraceLevel,
},
{
name: "numeric 0 maps to debug level",
input: "0",
wantLevel: zerolog.DebugLevel,
},
{
name: "numeric 1 maps to info level",
input: "1",
wantLevel: zerolog.InfoLevel,
},
{
name: "numeric 2 maps to warn level",
input: "2",
wantLevel: zerolog.WarnLevel,
},
{
name: "numeric 3 maps to error level",
input: "3",
wantLevel: zerolog.ErrorLevel,
},
{
name: "numeric 4 maps to fatal level",
input: "4",
wantLevel: zerolog.FatalLevel,
},
{
name: "numeric 5 maps to panic level",
input: "5",
wantLevel: zerolog.PanicLevel,
},
// Invalid levels
{
name: "invalid level returns error",
input: "invalid",
wantLevel: zerolog.NoLevel, // zerolog.ParseLevel returns NoLevel on error
wantErr: true,
},
{
name: "typo returns error",
input: "debuf",
wantLevel: zerolog.NoLevel,
wantErr: true,
},
{
name: "verbose returns error",
input: "verbose",
wantLevel: zerolog.NoLevel,
wantErr: true,
},
{
name: "numeric out of range accepted (zerolog accepts any int)",
input: "99",
wantLevel: zerolog.Level(99),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
level, err := parseLogLevel(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if level != tt.wantLevel {
t.Fatalf("expected level %v, got %v", tt.wantLevel, level)
}
})
}
}
func TestDefaultLogLevel(t *testing.T) {
tests := []struct {
name string
envValue string
expected string
}{
// Empty returns "info"
{
name: "empty string returns info",
envValue: "",
expected: "info",
},
{
name: "whitespace only returns info",
envValue: " ",
expected: "info",
},
{
name: "tabs only returns info",
envValue: "\t\t",
expected: "info",
},
{
name: "newline only returns info",
envValue: "\n",
expected: "info",
},
// Non-empty returns as-is (no validation)
{
name: "debug returns debug",
envValue: "debug",
expected: "debug",
},
{
name: "error returns error",
envValue: "error",
expected: "error",
},
{
name: "trace returns trace",
envValue: "trace",
expected: "trace",
},
{
name: "invalid value passed through",
envValue: "invalid",
expected: "invalid",
},
{
name: "mixed case passed through",
envValue: "DEBUG",
expected: "DEBUG",
},
{
name: "value with surrounding whitespace NOT trimmed",
envValue: " debug ",
expected: " debug ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := defaultLogLevel(tt.envValue)
if got != tt.expected {
t.Fatalf("expected %q, got %q", tt.expected, got)
}
})
}
}
func TestMultiValue(t *testing.T) {
t.Run("String joins with comma", func(t *testing.T) {
mv := multiValue{"a", "b", "c"}
if got := mv.String(); got != "a,b,c" {
t.Fatalf("expected %q, got %q", "a,b,c", got)
}
})
t.Run("String empty slice returns empty string", func(t *testing.T) {
mv := multiValue{}
if got := mv.String(); got != "" {
t.Fatalf("expected %q, got %q", "", got)
}
})
t.Run("String single item no comma", func(t *testing.T) {
mv := multiValue{"single"}
if got := mv.String(); got != "single" {
t.Fatalf("expected %q, got %q", "single", got)
}
})
t.Run("Set appends values", func(t *testing.T) {
mv := multiValue{}
if err := mv.Set("first"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := mv.Set("second"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := mv.Set("third"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := multiValue{"first", "second", "third"}
if !reflect.DeepEqual(mv, expected) {
t.Fatalf("expected %v, got %v", expected, mv)
}
})
t.Run("Set preserves empty strings", func(t *testing.T) {
mv := multiValue{}
_ = mv.Set("")
_ = mv.Set("value")
_ = mv.Set("")
if len(mv) != 3 {
t.Fatalf("expected 3 items, got %d", len(mv))
}
})
t.Run("Set always returns nil error", func(t *testing.T) {
mv := multiValue{}
// Set always returns nil, testing various inputs
inputs := []string{"", "normal", "with spaces", "special!@#$%", "unicode日本語"}
for _, input := range inputs {
if err := mv.Set(input); err != nil {
t.Fatalf("expected nil error for input %q, got %v", input, err)
}
}
})
}