Files
kportal/internal/logger/logger_test.go
T
lukaszraczylo f41c316b2b Add configuration wizard. (#2)
* Add configuration wizard.
2025-11-24 02:28:08 +00:00

522 lines
13 KiB
Go

package logger
import (
"bytes"
"encoding/json"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoggerTextFormat(t *testing.T) {
tests := []struct {
name string
level Level
logLevel Level
message string
fields map[string]interface{}
expectOutput bool
expectContains []string
}{
{
name: "info logged at info level",
level: LevelInfo,
logLevel: LevelInfo,
message: "test message",
fields: nil,
expectOutput: true,
expectContains: []string{"[INFO]", "test message"},
},
{
name: "debug filtered at info level",
level: LevelInfo,
logLevel: LevelDebug,
message: "debug message",
fields: nil,
expectOutput: false,
expectContains: []string{},
},
{
name: "error logged at info level",
level: LevelInfo,
logLevel: LevelError,
message: "error message",
fields: nil,
expectOutput: true,
expectContains: []string{"[ERROR]", "error message"},
},
{
name: "info with fields",
level: LevelInfo,
logLevel: LevelInfo,
message: "test message",
fields: map[string]interface{}{
"key1": "value1",
"key2": 123,
},
expectOutput: true,
expectContains: []string{"[INFO]", "test message", "key1", "value1"},
},
{
name: "warn logged at warn level",
level: LevelWarn,
logLevel: LevelWarn,
message: "warning message",
fields: nil,
expectOutput: true,
expectContains: []string{"[WARN]", "warning message"},
},
{
name: "info filtered at warn level",
level: LevelWarn,
logLevel: LevelInfo,
message: "info message",
fields: nil,
expectOutput: false,
expectContains: []string{},
},
{
name: "debug logged at debug level",
level: LevelDebug,
logLevel: LevelDebug,
message: "debug message",
fields: nil,
expectOutput: true,
expectContains: []string{"[DEBUG]", "debug message"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
logger := New(tt.level, FormatText, buf)
// Log at the specified level
switch tt.logLevel {
case LevelDebug:
if tt.fields != nil {
logger.Debug(tt.message, tt.fields)
} else {
logger.Debug(tt.message)
}
case LevelInfo:
if tt.fields != nil {
logger.Info(tt.message, tt.fields)
} else {
logger.Info(tt.message)
}
case LevelWarn:
if tt.fields != nil {
logger.Warn(tt.message, tt.fields)
} else {
logger.Warn(tt.message)
}
case LevelError:
if tt.fields != nil {
logger.Error(tt.message, tt.fields)
} else {
logger.Error(tt.message)
}
}
output := buf.String()
if tt.expectOutput {
assert.NotEmpty(t, output, "Expected log output but got none")
for _, expected := range tt.expectContains {
assert.Contains(t, output, expected, "Expected output to contain: %s", expected)
}
} else {
assert.Empty(t, output, "Expected no log output but got: %s", output)
}
})
}
}
func TestLoggerJSONFormat(t *testing.T) {
tests := []struct {
name string
level Level
logLevel Level
message string
fields map[string]interface{}
expectOutput bool
expectLevel string
}{
{
name: "info logged at info level",
level: LevelInfo,
logLevel: LevelInfo,
message: "test message",
fields: nil,
expectOutput: true,
expectLevel: "INFO",
},
{
name: "debug filtered at info level",
level: LevelInfo,
logLevel: LevelDebug,
message: "debug message",
fields: nil,
expectOutput: false,
expectLevel: "",
},
{
name: "error logged at debug level",
level: LevelDebug,
logLevel: LevelError,
message: "error message",
fields: nil,
expectOutput: true,
expectLevel: "ERROR",
},
{
name: "info with fields",
level: LevelInfo,
logLevel: LevelInfo,
message: "test message",
fields: map[string]interface{}{
"context": "production",
"port": 8080,
"retry": 3,
},
expectOutput: true,
expectLevel: "INFO",
},
{
name: "warn at warn level",
level: LevelWarn,
logLevel: LevelWarn,
message: "warning message",
fields: nil,
expectOutput: true,
expectLevel: "WARN",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
logger := New(tt.level, FormatJSON, buf)
// Log at the specified level
switch tt.logLevel {
case LevelDebug:
if tt.fields != nil {
logger.Debug(tt.message, tt.fields)
} else {
logger.Debug(tt.message)
}
case LevelInfo:
if tt.fields != nil {
logger.Info(tt.message, tt.fields)
} else {
logger.Info(tt.message)
}
case LevelWarn:
if tt.fields != nil {
logger.Warn(tt.message, tt.fields)
} else {
logger.Warn(tt.message)
}
case LevelError:
if tt.fields != nil {
logger.Error(tt.message, tt.fields)
} else {
logger.Error(tt.message)
}
}
output := buf.String()
if tt.expectOutput {
assert.NotEmpty(t, output, "Expected log output but got none")
// Parse JSON
var entry logEntry
err := json.Unmarshal([]byte(strings.TrimSpace(output)), &entry)
require.NoError(t, err, "Failed to parse JSON output: %s", output)
// Validate fields
assert.Equal(t, tt.expectLevel, entry.Level)
assert.Equal(t, tt.message, entry.Message)
assert.NotEmpty(t, entry.Time, "Time field should not be empty")
// Validate custom fields if provided
if tt.fields != nil {
require.NotNil(t, entry.Fields, "Expected fields in JSON output")
for key, expectedValue := range tt.fields {
actualValue, exists := entry.Fields[key]
assert.True(t, exists, "Expected field %s not found in output", key)
// JSON unmarshaling converts numbers to float64
if floatVal, ok := expectedValue.(int); ok {
assert.Equal(t, float64(floatVal), actualValue)
} else {
assert.Equal(t, expectedValue, actualValue)
}
}
}
} else {
assert.Empty(t, output, "Expected no log output but got: %s", output)
}
})
}
}
func TestGlobalLogger(t *testing.T) {
tests := []struct {
name string
initLevel Level
initFormat Format
logFunc func(string, ...map[string]interface{})
message string
expectContains string
}{
{
name: "global info logger text",
initLevel: LevelInfo,
initFormat: FormatText,
logFunc: Info,
message: "global info message",
expectContains: "[INFO]",
},
{
name: "global error logger text",
initLevel: LevelInfo,
initFormat: FormatText,
logFunc: Error,
message: "global error message",
expectContains: "[ERROR]",
},
{
name: "global warn logger json",
initLevel: LevelWarn,
initFormat: FormatJSON,
logFunc: Warn,
message: "global warn message",
expectContains: `"level":"WARN"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stderr by replacing globalLogger's output
buf := &bytes.Buffer{}
Init(tt.initLevel, tt.initFormat)
globalLogger.output = buf
// Call the global log function
tt.logFunc(tt.message)
output := buf.String()
assert.Contains(t, output, tt.expectContains)
assert.Contains(t, output, tt.message)
})
}
}
func TestLogLevelsFiltering(t *testing.T) {
tests := []struct {
name string
loggerLevel Level
logAtLevels []Level
expectOutputs []bool
}{
{
name: "debug level logs everything",
loggerLevel: LevelDebug,
logAtLevels: []Level{LevelDebug, LevelInfo, LevelWarn, LevelError},
expectOutputs: []bool{true, true, true, true},
},
{
name: "info level filters debug",
loggerLevel: LevelInfo,
logAtLevels: []Level{LevelDebug, LevelInfo, LevelWarn, LevelError},
expectOutputs: []bool{false, true, true, true},
},
{
name: "warn level filters debug and info",
loggerLevel: LevelWarn,
logAtLevels: []Level{LevelDebug, LevelInfo, LevelWarn, LevelError},
expectOutputs: []bool{false, false, true, true},
},
{
name: "error level only logs errors",
loggerLevel: LevelError,
logAtLevels: []Level{LevelDebug, LevelInfo, LevelWarn, LevelError},
expectOutputs: []bool{false, false, false, true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
logger := New(tt.loggerLevel, FormatText, buf)
for i, logLevel := range tt.logAtLevels {
buf.Reset()
switch logLevel {
case LevelDebug:
logger.Debug("test")
case LevelInfo:
logger.Info("test")
case LevelWarn:
logger.Warn("test")
case LevelError:
logger.Error("test")
}
hasOutput := buf.Len() > 0
assert.Equal(t, tt.expectOutputs[i], hasOutput,
"Level %v at logger level %v: expected output=%v, got=%v",
logLevel, tt.loggerLevel, tt.expectOutputs[i], hasOutput)
}
})
}
}
func TestLoggerNilOutput(t *testing.T) {
// Test that logger defaults to os.Stderr when output is nil
logger := New(LevelInfo, FormatText, nil)
assert.NotNil(t, logger.output, "Logger output should not be nil")
}
func TestLevelToString(t *testing.T) {
tests := []struct {
level Level
expected string
}{
{LevelDebug, "DEBUG"},
{LevelInfo, "INFO"},
{LevelWarn, "WARN"},
{LevelError, "ERROR"},
{Level(999), "UNKNOWN"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := levelToString(tt.level)
assert.Equal(t, tt.expected, result)
})
}
}
func TestJSONFieldTypes(t *testing.T) {
tests := []struct {
name string
fields map[string]interface{}
}{
{
name: "string fields",
fields: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
{
name: "numeric fields",
fields: map[string]interface{}{
"port": 8080,
"timeout": 30,
"retry": 3,
},
},
{
name: "boolean fields",
fields: map[string]interface{}{
"enabled": true,
"running": false,
},
},
{
name: "mixed types",
fields: map[string]interface{}{
"context": "production",
"port": 8080,
"enabled": true,
"namespace": "default",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
logger := New(LevelInfo, FormatJSON, buf)
logger.Info("test message", tt.fields)
var entry logEntry
err := json.Unmarshal([]byte(strings.TrimSpace(buf.String())), &entry)
require.NoError(t, err)
assert.Equal(t, len(tt.fields), len(entry.Fields),
"Field count mismatch")
for key := range tt.fields {
_, exists := entry.Fields[key]
assert.True(t, exists, "Field %s not found in JSON output", key)
}
})
}
}
func TestInitWithCustomOutput(t *testing.T) {
tests := []struct {
name string
output io.Writer
expectDiscard bool
description string
}{
{
name: "init with custom buffer",
output: &bytes.Buffer{},
expectDiscard: false,
description: "Should use provided buffer",
},
{
name: "init with io.Discard",
output: io.Discard,
expectDiscard: true,
description: "Should use io.Discard to silence output",
},
{
name: "init without output defaults to stderr",
output: nil,
expectDiscard: false,
description: "Should default to stderr when no output provided",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.output != nil {
Init(LevelInfo, FormatText, tt.output)
} else {
Init(LevelInfo, FormatText)
}
// Verify global logger was initialized
assert.NotNil(t, globalLogger, "Global logger should be initialized")
if tt.output != nil && !tt.expectDiscard {
// For buffer, verify output works
if buf, ok := tt.output.(*bytes.Buffer); ok {
Info("test message")
output := buf.String()
assert.Contains(t, output, "test message")
assert.Contains(t, output, "[INFO]")
}
} else if tt.expectDiscard {
// For io.Discard, verify no output appears (we can't really test this directly,
// but we can verify the logger was set with the right output)
assert.Equal(t, io.Discard, globalLogger.output)
}
})
}
}