Pulse/cmd/pulse-agent/main_test.go
rcourtman 5c90fb102b Add unit tests for pulse-agent utility functions (unified agent)
53 test cases covering 4 functions:
- gatherTags: environment/flag tag merging (18 cases)
- parseLogLevel: log level parsing (30 cases)
- defaultLogLevel: default value resolution (10 cases)
- multiValue: flag.Value interface for repeatable flags (6 cases)

Key difference from pulse-host-agent: parseLogLevel in unified agent delegates
directly to zerolog.ParseLevel without range validation, so trace/fatal/panic
levels are accepted (unlike pulse-host-agent which restricts to debug-error).

First test file for cmd/pulse-agent package.
2025-11-30 08:34:09 +00:00

473 lines
10 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 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)
}
}
})
}