mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
1b49e133da
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
1614 lines
48 KiB
Go
1614 lines
48 KiB
Go
package logger
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestLogLevel tests the LogLevel constants and parsing
|
|
func TestLogLevel(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected LogLevel
|
|
}{
|
|
{"debug", LogLevelDebug},
|
|
{"DEBUG", LogLevelDebug},
|
|
{"info", LogLevelInfo},
|
|
{"INFO", LogLevelInfo},
|
|
{"error", LogLevelError},
|
|
{"ERROR", LogLevelError},
|
|
{"none", LogLevelNone},
|
|
{"NONE", LogLevelNone},
|
|
{"unknown", LogLevelInfo}, // default
|
|
{"", LogLevelInfo}, // default
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("ParseLogLevel_%s", test.input), func(t *testing.T) {
|
|
result := ParseLogLevel(test.input)
|
|
if result != test.expected {
|
|
t.Errorf("ParseLogLevel(%q) = %v, want %v", test.input, result, test.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_LogLevels tests logging at different levels
|
|
func TestStandardLogger_LogLevels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
level LogLevel
|
|
shouldLog map[string]bool
|
|
loggerLevel string
|
|
}{
|
|
{
|
|
name: "Debug level logs everything",
|
|
level: LogLevelDebug,
|
|
loggerLevel: "debug",
|
|
shouldLog: map[string]bool{
|
|
"debug": true,
|
|
"info": true,
|
|
"error": true,
|
|
},
|
|
},
|
|
{
|
|
name: "Info level logs info and error",
|
|
level: LogLevelInfo,
|
|
loggerLevel: "info",
|
|
shouldLog: map[string]bool{
|
|
"debug": false,
|
|
"info": true,
|
|
"error": true,
|
|
},
|
|
},
|
|
{
|
|
name: "Error level logs only error",
|
|
level: LogLevelError,
|
|
loggerLevel: "error",
|
|
shouldLog: map[string]bool{
|
|
"debug": false,
|
|
"info": false,
|
|
"error": true,
|
|
},
|
|
},
|
|
{
|
|
name: "None level logs nothing",
|
|
level: LogLevelNone,
|
|
loggerLevel: "none",
|
|
shouldLog: map[string]bool{
|
|
"debug": false,
|
|
"info": false,
|
|
"error": false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
logger := NewStandardLogger(test.loggerLevel, &errorBuf, &infoBuf, &debugBuf)
|
|
|
|
// Test basic logging methods
|
|
logger.Debug("debug message")
|
|
logger.Info("info message")
|
|
logger.Error("error message")
|
|
|
|
// Check debug output
|
|
debugOutput := debugBuf.String()
|
|
if test.shouldLog["debug"] && !strings.Contains(debugOutput, "debug message") {
|
|
t.Errorf("Expected debug message to be logged at level %v", test.level)
|
|
}
|
|
if !test.shouldLog["debug"] && strings.Contains(debugOutput, "debug message") {
|
|
t.Errorf("Debug message should not be logged at level %v", test.level)
|
|
}
|
|
|
|
// Check info output
|
|
infoOutput := infoBuf.String()
|
|
if test.shouldLog["info"] && !strings.Contains(infoOutput, "info message") {
|
|
t.Errorf("Expected info message to be logged at level %v", test.level)
|
|
}
|
|
if !test.shouldLog["info"] && strings.Contains(infoOutput, "info message") {
|
|
t.Errorf("Info message should not be logged at level %v", test.level)
|
|
}
|
|
|
|
// Check error output
|
|
errorOutput := errorBuf.String()
|
|
if test.shouldLog["error"] && !strings.Contains(errorOutput, "error message") {
|
|
t.Errorf("Expected error message to be logged at level %v", test.level)
|
|
}
|
|
if !test.shouldLog["error"] && strings.Contains(errorOutput, "error message") {
|
|
t.Errorf("Error message should not be logged at level %v", test.level)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_FormattedLogging tests formatted logging methods
|
|
func TestStandardLogger_FormattedLogging(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", &errorBuf, &infoBuf, &debugBuf)
|
|
|
|
// Test formatted methods
|
|
logger.Debugf("debug %s %d", "test", 123)
|
|
logger.Infof("info %s %d", "test", 456)
|
|
logger.Errorf("error %s %d", "test", 789)
|
|
logger.Printf("printf %s %d", "test", 999)
|
|
|
|
// Check outputs
|
|
if !strings.Contains(debugBuf.String(), "debug test 123") {
|
|
t.Error("Debugf output not found")
|
|
}
|
|
if !strings.Contains(infoBuf.String(), "info test 456") {
|
|
t.Error("Infof output not found")
|
|
}
|
|
if !strings.Contains(infoBuf.String(), "printf test 999") {
|
|
t.Error("Printf output not found (should go to info)")
|
|
}
|
|
if !strings.Contains(errorBuf.String(), "error test 789") {
|
|
t.Error("Errorf output not found")
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_Println tests the Println method
|
|
func TestStandardLogger_Println(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &infoBuf, nil)
|
|
|
|
logger.Println("test", "message", 123)
|
|
|
|
output := infoBuf.String()
|
|
// Just check that the essential content is there, ignoring formatting differences
|
|
if !strings.Contains(output, "test") || !strings.Contains(output, "message") || !strings.Contains(output, "123") {
|
|
t.Errorf("Println output missing expected content: %s", output)
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_Fatalf tests the Fatalf method (should panic)
|
|
func TestStandardLogger_Fatalf(t *testing.T) {
|
|
var errorBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", &errorBuf, nil, nil)
|
|
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Error("Fatalf should have panicked")
|
|
}
|
|
// Check that error was logged before panic
|
|
if !strings.Contains(errorBuf.String(), "fatal test") {
|
|
t.Error("Fatalf should log error before panicking")
|
|
}
|
|
}()
|
|
|
|
logger.Fatalf("fatal %s", "test")
|
|
}
|
|
|
|
// TestStandardLogger_WithField tests structured logging with single field
|
|
func TestStandardLogger_WithField(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &infoBuf, nil)
|
|
|
|
fieldLogger := logger.WithField("key", "value")
|
|
fieldLogger.Info("test message")
|
|
|
|
output := infoBuf.String()
|
|
if !strings.Contains(output, "test message [key=value]") {
|
|
t.Errorf("WithField output incorrect: %s", output)
|
|
}
|
|
|
|
// Test that original logger is unchanged
|
|
infoBuf.Reset()
|
|
logger.Info("original message")
|
|
output = infoBuf.String()
|
|
if strings.Contains(output, "[key=value]") {
|
|
t.Error("Original logger should not have fields")
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_WithFields tests structured logging with multiple fields
|
|
func TestStandardLogger_WithFields(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &infoBuf, nil)
|
|
|
|
fields := map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
"key3": true,
|
|
}
|
|
fieldLogger := logger.WithFields(fields)
|
|
fieldLogger.Info("test message")
|
|
|
|
output := infoBuf.String()
|
|
// Check that message contains all fields (order may vary)
|
|
if !strings.Contains(output, "test message [") {
|
|
t.Error("WithFields should format message with fields")
|
|
}
|
|
if !strings.Contains(output, "key1=value1") {
|
|
t.Error("Missing key1=value1 in output")
|
|
}
|
|
if !strings.Contains(output, "key2=42") {
|
|
t.Error("Missing key2=42 in output")
|
|
}
|
|
if !strings.Contains(output, "key3=true") {
|
|
t.Error("Missing key3=true in output")
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_NestedFields tests chaining WithField calls
|
|
func TestStandardLogger_NestedFields(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &infoBuf, nil)
|
|
|
|
chainedLogger := logger.WithField("key1", "value1").WithField("key2", "value2")
|
|
chainedLogger.Info("test message")
|
|
|
|
output := infoBuf.String()
|
|
if !strings.Contains(output, "key1=value1") || !strings.Contains(output, "key2=value2") {
|
|
t.Errorf("Chained fields not found in output: %s", output)
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_ConcurrentSafety tests concurrent access to logger
|
|
func TestStandardLogger_ConcurrentSafety(t *testing.T) {
|
|
// Use separate buffers for each log level to avoid race conditions in the test
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
var bufMutex sync.Mutex // Protect the buffers in test
|
|
|
|
// Wrap buffers with mutex protection for test
|
|
safeErrorBuf := &safeBuffer{buf: &errorBuf, mu: &bufMutex}
|
|
safeInfoBuf := &safeBuffer{buf: &infoBuf, mu: &bufMutex}
|
|
safeDebugBuf := &safeBuffer{buf: &debugBuf, mu: &bufMutex}
|
|
|
|
logger := NewStandardLogger("debug", safeErrorBuf, safeInfoBuf, safeDebugBuf)
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10 // Reduced for faster test
|
|
messagesPerGoroutine := 5
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < messagesPerGoroutine; j++ {
|
|
logger.Infof("goroutine %d message %d", id, j)
|
|
fieldLogger := logger.WithField("goroutine", id)
|
|
fieldLogger.Debugf("field message %d", j)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Just verify no panic occurred and some output was generated
|
|
bufMutex.Lock()
|
|
totalLen := errorBuf.Len() + infoBuf.Len() + debugBuf.Len()
|
|
bufMutex.Unlock()
|
|
|
|
if totalLen == 0 {
|
|
t.Error("Expected some log output from concurrent operations")
|
|
}
|
|
}
|
|
|
|
// safeBuffer wraps bytes.Buffer with mutex for testing
|
|
type safeBuffer struct {
|
|
buf *bytes.Buffer
|
|
mu *sync.Mutex
|
|
}
|
|
|
|
func (sb *safeBuffer) Write(p []byte) (n int, err error) {
|
|
sb.mu.Lock()
|
|
defer sb.mu.Unlock()
|
|
return sb.buf.Write(p)
|
|
}
|
|
|
|
// TestNewStandardLogger_NilOutputs tests logger creation with nil outputs
|
|
func TestNewStandardLogger_NilOutputs(t *testing.T) {
|
|
logger := NewStandardLogger("debug", nil, nil, nil)
|
|
|
|
// Should not panic when logging to nil outputs
|
|
logger.Debug("debug message")
|
|
logger.Info("info message")
|
|
logger.Error("error message")
|
|
}
|
|
|
|
// TestNoOpLogger tests the NoOpLogger implementation
|
|
func TestNoOpLogger(t *testing.T) {
|
|
logger := &NoOpLogger{}
|
|
|
|
// None of these should panic or produce output
|
|
logger.Debug("debug")
|
|
logger.Debugf("debug %s", "formatted")
|
|
logger.Info("info")
|
|
logger.Infof("info %s", "formatted")
|
|
logger.Error("error")
|
|
logger.Errorf("error %s", "formatted")
|
|
logger.Printf("printf %s", "formatted")
|
|
logger.Println("println", "args")
|
|
logger.Fatalf("fatalf %s", "formatted") // Should NOT panic
|
|
|
|
// Test chaining
|
|
fieldLogger := logger.WithField("key", "value")
|
|
if fieldLogger != logger {
|
|
t.Error("WithField should return same NoOpLogger instance")
|
|
}
|
|
|
|
fieldsLogger := logger.WithFields(map[string]interface{}{"key": "value"})
|
|
if fieldsLogger != logger {
|
|
t.Error("WithFields should return same NoOpLogger instance")
|
|
}
|
|
}
|
|
|
|
// TestNoOpLogger_DirectInstantiation tests NoOpLogger methods through direct instantiation
|
|
func TestNoOpLogger_DirectInstantiation(t *testing.T) {
|
|
// Create NoOpLogger instance directly to ensure methods are called
|
|
logger := &NoOpLogger{}
|
|
|
|
// Verify these methods exist and can be called without panic
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("NoOpLogger methods should not panic: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Call each method explicitly to ensure coverage
|
|
logger.Debug("test debug")
|
|
logger.Debugf("test debugf %s", "arg")
|
|
logger.Info("test info")
|
|
logger.Infof("test infof %s", "arg")
|
|
logger.Error("test error")
|
|
logger.Errorf("test errorf %s", "arg")
|
|
logger.Printf("test printf %s", "arg")
|
|
logger.Println("test", "println")
|
|
logger.Fatalf("test fatalf %s", "arg") // Critical: should NOT panic
|
|
|
|
// Test field methods
|
|
result1 := logger.WithField("key", "value")
|
|
if result1 != logger {
|
|
t.Error("WithField should return same instance")
|
|
}
|
|
|
|
result2 := logger.WithFields(map[string]interface{}{"key": "value"})
|
|
if result2 != logger {
|
|
t.Error("WithFields should return same instance")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Enhanced NoOpLogger Tests (lines 256-280 coverage)
|
|
// =============================================================================
|
|
|
|
// TestNoOpLogger_AllMethods tests all NoOpLogger methods comprehensively
|
|
func TestNoOpLogger_AllMethods(t *testing.T) {
|
|
logger := &NoOpLogger{}
|
|
|
|
// Test all methods don't panic with various inputs
|
|
testCases := []struct {
|
|
name string
|
|
fn func()
|
|
}{
|
|
{"Debug empty", func() { logger.Debug("") }},
|
|
{"Debug normal", func() { logger.Debug("debug message") }},
|
|
{"Debug long", func() { logger.Debug(strings.Repeat("long ", 1000)) }},
|
|
{"Debug special chars", func() { logger.Debug("Debug with \n\t special chars: \\u00e9") }},
|
|
|
|
{"Debugf empty", func() { logger.Debugf("") }},
|
|
{"Debugf no args", func() { logger.Debugf("debug message") }},
|
|
{"Debugf with args", func() { logger.Debugf("debug %s %d", "test", 42) }},
|
|
{"Debugf many args", func() { logger.Debugf("debug %v %v %v %v", 1, 2, 3, 4) }},
|
|
{"Debugf nil args", func() { logger.Debugf("debug %v", nil) }},
|
|
|
|
{"Info empty", func() { logger.Info("") }},
|
|
{"Info normal", func() { logger.Info("info message") }},
|
|
{"Info special chars", func() { logger.Info("Info with unicode: ü ñ é") }},
|
|
|
|
{"Infof empty", func() { logger.Infof("") }},
|
|
{"Infof no args", func() { logger.Infof("info message") }},
|
|
{"Infof with args", func() { logger.Infof("info %s %d", "test", 123) }},
|
|
{"Infof complex", func() { logger.Infof("complex %+v", map[string]int{"key": 42}) }},
|
|
|
|
{"Error empty", func() { logger.Error("") }},
|
|
{"Error normal", func() { logger.Error("error message") }},
|
|
{"Error long", func() { logger.Error(strings.Repeat("error ", 500)) }},
|
|
|
|
{"Errorf empty", func() { logger.Errorf("") }},
|
|
{"Errorf no args", func() { logger.Errorf("error message") }},
|
|
{"Errorf with args", func() { logger.Errorf("error %s %d", "test", 456) }},
|
|
{"Errorf with error", func() { logger.Errorf("error: %v", fmt.Errorf("test error")) }},
|
|
|
|
{"Printf empty", func() { logger.Printf("") }},
|
|
{"Printf no args", func() { logger.Printf("printf message") }},
|
|
{"Printf with args", func() { logger.Printf("printf %s %d", "test", 789) }},
|
|
{"Printf percent", func() { logger.Printf("100%% complete") }},
|
|
|
|
{"Println empty", func() { logger.Println() }},
|
|
{"Println single", func() { logger.Println("single") }},
|
|
{"Println multiple", func() { logger.Println("multiple", "args", 123, true) }},
|
|
{"Println nil", func() { logger.Println(nil, nil) }},
|
|
{"Println mixed", func() { logger.Println("string", 42, true, 3.14, []int{1, 2, 3}) }},
|
|
|
|
{"Fatalf empty", func() { logger.Fatalf("") }},
|
|
{"Fatalf no args", func() { logger.Fatalf("fatal message") }},
|
|
{"Fatalf with args", func() { logger.Fatalf("fatal %s %d", "test", 999) }},
|
|
{"Fatalf should not panic", func() { logger.Fatalf("this should not cause panic") }},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Ensure no panic occurs
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("NoOpLogger.%s panicked: %v", tc.name, r)
|
|
}
|
|
}()
|
|
|
|
tc.fn()
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNoOpLogger_WithField_EdgeCases tests WithField with edge cases
|
|
func TestNoOpLogger_WithField_EdgeCases(t *testing.T) {
|
|
logger := &NoOpLogger{}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
key string
|
|
value interface{}
|
|
}{
|
|
{"empty key", "", "value"},
|
|
{"empty value", "key", ""},
|
|
{"nil value", "key", nil},
|
|
{"complex value", "key", map[string]interface{}{"nested": []int{1, 2, 3}}},
|
|
{"function value", "key", func() string { return "test" }},
|
|
{"channel value", "key", make(chan int)},
|
|
{"large string", "key", strings.Repeat("large ", 1000)},
|
|
{"unicode key", "ключ", "значение"},
|
|
{"unicode value", "key", "値 💻 🌟"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := logger.WithField(tc.key, tc.value)
|
|
|
|
if result != logger {
|
|
t.Error("WithField should always return the same NoOpLogger instance")
|
|
}
|
|
|
|
// Should be able to chain calls
|
|
chained := result.WithField("another", "value")
|
|
if chained != logger {
|
|
t.Error("Chained WithField should return the same NoOpLogger instance")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNoOpLogger_WithFields_EdgeCases tests WithFields with edge cases
|
|
func TestNoOpLogger_WithFields_EdgeCases(t *testing.T) {
|
|
logger := &NoOpLogger{}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
fields map[string]interface{}
|
|
}{
|
|
{"nil map", nil},
|
|
{"empty map", map[string]interface{}{}},
|
|
{"single field", map[string]interface{}{"key": "value"}},
|
|
{"multiple fields", map[string]interface{}{
|
|
"string": "value",
|
|
"int": 42,
|
|
"bool": true,
|
|
"float": 3.14,
|
|
}},
|
|
{"nil values", map[string]interface{}{
|
|
"nil1": nil,
|
|
"nil2": nil,
|
|
}},
|
|
{"complex values", map[string]interface{}{
|
|
"map": map[string]int{"nested": 42},
|
|
"slice": []string{"a", "b", "c"},
|
|
"function": func() {},
|
|
"channel": make(chan string),
|
|
}},
|
|
{"large map", func() map[string]interface{} {
|
|
large := make(map[string]interface{})
|
|
for i := 0; i < 1000; i++ {
|
|
large[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
|
}
|
|
return large
|
|
}()},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := logger.WithFields(tc.fields)
|
|
|
|
if result != logger {
|
|
t.Error("WithFields should always return the same NoOpLogger instance")
|
|
}
|
|
|
|
// Should be able to chain calls
|
|
chained := result.WithFields(map[string]interface{}{"another": "value"})
|
|
if chained != logger {
|
|
t.Error("Chained WithFields should return the same NoOpLogger instance")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNoOpLogger_Concurrent tests concurrent access to NoOpLogger
|
|
func TestNoOpLogger_Concurrent(t *testing.T) {
|
|
logger := &NoOpLogger{}
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
operationsPerGoroutine := 100
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
// Test various operations concurrently
|
|
logger.Debug(fmt.Sprintf("debug %d-%d", id, j))
|
|
logger.Debugf("debugf %d-%d", id, j)
|
|
logger.Info(fmt.Sprintf("info %d-%d", id, j))
|
|
logger.Infof("infof %d-%d", id, j)
|
|
logger.Error(fmt.Sprintf("error %d-%d", id, j))
|
|
logger.Errorf("errorf %d-%d", id, j)
|
|
logger.Printf("printf %d-%d", id, j)
|
|
logger.Println("println", id, j)
|
|
logger.Fatalf("fatalf %d-%d", id, j)
|
|
|
|
// Test field operations
|
|
fieldLogger := logger.WithField(fmt.Sprintf("key%d", id), j)
|
|
fieldLogger.Info("test")
|
|
|
|
fieldsLogger := logger.WithFields(map[string]interface{}{
|
|
"goroutine": id,
|
|
"operation": j,
|
|
})
|
|
fieldsLogger.Debug("test")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// If we reach here without deadlock or panic, the test passes
|
|
}
|
|
|
|
// TestNoOpLogger_Singleton_Consistency tests singleton behavior
|
|
func TestNoOpLogger_Singleton_Consistency(t *testing.T) {
|
|
// Get multiple instances through different paths
|
|
logger1 := &NoOpLogger{}
|
|
logger2 := GetNoOpLogger()
|
|
logger3 := GetFactory().GetNoOpLogger()
|
|
|
|
// Test that WithField/WithFields always return the same type
|
|
field1 := logger1.WithField("key", "value")
|
|
field2 := logger2.WithField("key", "value")
|
|
field3 := logger3.WithField("key", "value")
|
|
|
|
// All should be NoOpLoggers
|
|
if _, ok := field1.(*NoOpLogger); !ok {
|
|
t.Error("WithField should return NoOpLogger")
|
|
}
|
|
if _, ok := field2.(*NoOpLogger); !ok {
|
|
t.Error("WithField should return NoOpLogger")
|
|
}
|
|
if _, ok := field3.(*NoOpLogger); !ok {
|
|
t.Error("WithField should return NoOpLogger")
|
|
}
|
|
|
|
// Test WithFields
|
|
fields1 := logger1.WithFields(map[string]interface{}{"key": "value"})
|
|
fields2 := logger2.WithFields(map[string]interface{}{"key": "value"})
|
|
fields3 := logger3.WithFields(map[string]interface{}{"key": "value"})
|
|
|
|
if _, ok := fields1.(*NoOpLogger); !ok {
|
|
t.Error("WithFields should return NoOpLogger")
|
|
}
|
|
if _, ok := fields2.(*NoOpLogger); !ok {
|
|
t.Error("WithFields should return NoOpLogger")
|
|
}
|
|
if _, ok := fields3.(*NoOpLogger); !ok {
|
|
t.Error("WithFields should return NoOpLogger")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Additional Edge Cases and Error Scenarios
|
|
// =============================================================================
|
|
|
|
// TestStandardLogger_NilFieldValues tests handling of nil field values
|
|
func TestStandardLogger_NilFieldValues(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
// Test nil field values
|
|
fieldLogger := logger.WithField("nil_value", nil)
|
|
fieldLogger.Info("test message")
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "test message [nil_value=<nil>]") {
|
|
t.Errorf("Expected nil value to be formatted as '<nil>', got: %s", output)
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_LargeMessages tests handling of very large messages
|
|
func TestStandardLogger_LargeMessages(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
// Test very large message
|
|
largeMessage := strings.Repeat("This is a very long message. ", 1000)
|
|
logger.Info(largeMessage)
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, largeMessage) {
|
|
t.Error("Large message should be handled correctly")
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_UnicodeMessages tests handling of unicode characters
|
|
func TestStandardLogger_UnicodeMessages(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
unicodeMessage := "Unicode test: 中文 日本語 한글 العربية ελληνικά русский ⚡️ 🌟 💻"
|
|
logger.Info(unicodeMessage)
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, unicodeMessage) {
|
|
t.Error("Unicode characters should be preserved in log output")
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_ZeroLengthMessages tests zero-length message handling
|
|
func TestStandardLogger_ZeroLengthMessages(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
// Test empty messages
|
|
logger.Debug("")
|
|
logger.Info("")
|
|
logger.Error("")
|
|
|
|
// Should write something (timestamp, etc.) even with empty messages
|
|
if buf.Len() == 0 {
|
|
t.Error("Empty messages should still produce some output")
|
|
}
|
|
}
|
|
|
|
// TestLogLevel_AllValues tests all log level values
|
|
func TestLogLevel_AllValues(t *testing.T) {
|
|
levelMap := map[LogLevel]string{
|
|
LogLevelDebug: "debug",
|
|
LogLevelInfo: "info",
|
|
LogLevelError: "error",
|
|
LogLevelNone: "none",
|
|
}
|
|
|
|
for level, levelStr := range levelMap {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
logger := NewStandardLogger(levelStr, &errorBuf, &infoBuf, &debugBuf)
|
|
|
|
// Test that logger was created successfully with each level
|
|
if logger == nil {
|
|
t.Errorf("NewStandardLogger should not return nil for level %v", level)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStandardLogger_FormattingEdgeCases tests edge cases in formatting
|
|
func TestStandardLogger_FormattingEdgeCases(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
// Test format strings with various argument types
|
|
logger.Infof("format %v %v %v", "string", 42, true)
|
|
|
|
// Test percent signs in format strings
|
|
logger.Infof("Progress: 100%% complete")
|
|
|
|
// Test with nil arguments
|
|
logger.Infof("nil value: %v", nil)
|
|
|
|
// Should not panic and produce output
|
|
if buf.Len() == 0 {
|
|
t.Error("Should produce output from formatting tests")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_ConcurrentAccess tests concurrent access to adapter
|
|
func TestLegacyLoggerAdapter_ConcurrentAccess(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
var bufMutex sync.Mutex
|
|
|
|
// Thread-safe buffer wrappers
|
|
safeErrorBuf := &safeBuffer{buf: &errorBuf, mu: &bufMutex}
|
|
safeInfoBuf := &safeBuffer{buf: &infoBuf, mu: &bufMutex}
|
|
safeDebugBuf := &safeBuffer{buf: &debugBuf, mu: &bufMutex}
|
|
|
|
errorLogger := log.New(safeErrorBuf, "", 0)
|
|
infoLogger := log.New(safeInfoBuf, "", 0)
|
|
debugLogger := log.New(safeDebugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger)
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
messagesPerGoroutine := 10
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < messagesPerGoroutine; j++ {
|
|
adapter.Debug(fmt.Sprintf("debug %d-%d", id, j))
|
|
adapter.Info(fmt.Sprintf("info %d-%d", id, j))
|
|
adapter.Error(fmt.Sprintf("error %d-%d", id, j))
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify some output was generated
|
|
bufMutex.Lock()
|
|
totalLen := errorBuf.Len() + infoBuf.Len() + debugBuf.Len()
|
|
bufMutex.Unlock()
|
|
|
|
if totalLen == 0 {
|
|
t.Error("Expected some log output from concurrent operations")
|
|
}
|
|
}
|
|
|
|
// TestGetNoOpLogger tests the singleton no-op logger
|
|
func TestGetNoOpLogger(t *testing.T) {
|
|
logger1 := GetNoOpLogger()
|
|
logger2 := GetNoOpLogger()
|
|
|
|
if logger1 != logger2 {
|
|
t.Error("GetNoOpLogger should return the same instance (singleton)")
|
|
}
|
|
|
|
// Verify it's actually a NoOpLogger
|
|
if _, ok := logger1.(*NoOpLogger); !ok {
|
|
t.Error("GetNoOpLogger should return a NoOpLogger instance")
|
|
}
|
|
}
|
|
|
|
// TestDefaultLogger tests the DefaultLogger function
|
|
func TestDefaultLogger(t *testing.T) {
|
|
logger := DefaultLogger("info")
|
|
|
|
// Should be a StandardLogger
|
|
if _, ok := logger.(*StandardLogger); !ok {
|
|
t.Error("DefaultLogger should return a StandardLogger instance")
|
|
}
|
|
|
|
// Test that it actually logs (to default outputs)
|
|
logger.Info("test message") // Should not panic
|
|
}
|
|
|
|
// TestStandardLogger_formatWithFields tests the private formatWithFields method indirectly
|
|
func TestStandardLogger_formatWithFields(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("debug", nil, &buf, nil)
|
|
|
|
// Test empty fields
|
|
logger.Info("no fields")
|
|
output := buf.String()
|
|
if strings.Contains(output, "[") {
|
|
t.Error("Message without fields should not contain brackets")
|
|
}
|
|
|
|
buf.Reset()
|
|
|
|
// Test single field
|
|
fieldLogger := logger.WithField("key", "value")
|
|
fieldLogger.Info("one field")
|
|
output = buf.String()
|
|
if !strings.Contains(output, "one field [key=value]") {
|
|
t.Errorf("Single field formatting incorrect: %s", output)
|
|
}
|
|
|
|
buf.Reset()
|
|
|
|
// Test multiple fields (order may vary, so check components)
|
|
fieldsLogger := logger.WithFields(map[string]interface{}{
|
|
"a": 1,
|
|
"b": 2,
|
|
})
|
|
fieldsLogger.Info("two fields")
|
|
output = buf.String()
|
|
if !strings.Contains(output, "two fields [") {
|
|
t.Error("Multiple fields should start with message and bracket")
|
|
}
|
|
if !strings.Contains(output, "a=1") || !strings.Contains(output, "b=2") {
|
|
t.Error("Multiple fields should contain all key=value pairs")
|
|
}
|
|
}
|
|
|
|
// Benchmark tests for performance critical paths
|
|
func BenchmarkStandardLogger_Info(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("info", nil, &buf, nil)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
logger.Info("benchmark message")
|
|
}
|
|
}
|
|
|
|
func BenchmarkStandardLogger_InfoWithField(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("info", nil, &buf, nil)
|
|
fieldLogger := logger.WithField("key", "value")
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
fieldLogger.Info("benchmark message")
|
|
}
|
|
}
|
|
|
|
func BenchmarkStandardLogger_DebugDisabled(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("info", nil, &buf, nil) // Debug disabled
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
logger.Debug("benchmark message") // Should be fast when disabled
|
|
}
|
|
}
|
|
|
|
func BenchmarkNoOpLogger(b *testing.B) {
|
|
logger := GetNoOpLogger()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
logger.Info("benchmark message")
|
|
}
|
|
}
|
|
|
|
func BenchmarkWithField(b *testing.B) {
|
|
var buf bytes.Buffer
|
|
logger := NewStandardLogger("info", nil, &buf, nil)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
logger.WithField("iteration", i)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// LegacyLoggerAdapter Tests (adapter.go - 0% coverage)
|
|
// =============================================================================
|
|
|
|
// TestNewLegacyAdapter tests creating a new legacy adapter
|
|
func TestNewLegacyAdapter(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "ERROR: ", log.LstdFlags)
|
|
infoLogger := log.New(&infoBuf, "INFO: ", log.LstdFlags)
|
|
debugLogger := log.New(&debugBuf, "DEBUG: ", log.LstdFlags)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger)
|
|
|
|
if adapter == nil {
|
|
t.Error("NewLegacyAdapter should not return nil")
|
|
}
|
|
|
|
// Verify it's the correct type
|
|
if _, ok := adapter.(*LegacyLoggerAdapter); !ok {
|
|
t.Error("NewLegacyAdapter should return a LegacyLoggerAdapter")
|
|
}
|
|
}
|
|
|
|
// TestNewLegacyAdapter_WithNilLoggers tests creating adapter with nil loggers
|
|
func TestNewLegacyAdapter_WithNilLoggers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
errorLogger *log.Logger
|
|
infoLogger *log.Logger
|
|
debugLogger *log.Logger
|
|
}{
|
|
{"nil error logger", nil, log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0)},
|
|
{"nil info logger", log.New(&bytes.Buffer{}, "", 0), nil, log.New(&bytes.Buffer{}, "", 0)},
|
|
{"nil debug logger", log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0), nil},
|
|
{"all nil loggers", nil, nil, nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter := NewLegacyAdapter(test.errorLogger, test.infoLogger, test.debugLogger)
|
|
|
|
// Should return NoOpLogger when any logger is nil
|
|
if _, ok := adapter.(*NoOpLogger); !ok {
|
|
t.Error("NewLegacyAdapter with nil loggers should return NoOpLogger")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Debug tests debug logging
|
|
func TestLegacyLoggerAdapter_Debug(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
debugLogger := log.New(&debugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Debug("debug message")
|
|
|
|
if !strings.Contains(debugBuf.String(), "debug message") {
|
|
t.Error("Debug message not found in debug buffer")
|
|
}
|
|
|
|
// Verify other buffers are empty
|
|
if errorBuf.Len() > 0 || infoBuf.Len() > 0 {
|
|
t.Error("Debug should only write to debug buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Debugf tests formatted debug logging
|
|
func TestLegacyLoggerAdapter_Debugf(t *testing.T) {
|
|
var debugBuf bytes.Buffer
|
|
debugLogger := log.New(&debugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0), debugLogger).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Debugf("debug %s %d", "test", 42)
|
|
|
|
if !strings.Contains(debugBuf.String(), "debug test 42") {
|
|
t.Error("Debugf formatted message not found in debug buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Info tests info logging
|
|
func TestLegacyLoggerAdapter_Info(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
debugLogger := log.New(&debugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Info("info message")
|
|
|
|
if !strings.Contains(infoBuf.String(), "info message") {
|
|
t.Error("Info message not found in info buffer")
|
|
}
|
|
|
|
// Verify other buffers are empty
|
|
if errorBuf.Len() > 0 || debugBuf.Len() > 0 {
|
|
t.Error("Info should only write to info buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Infof tests formatted info logging
|
|
func TestLegacyLoggerAdapter_Infof(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), infoLogger, log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Infof("info %s %d", "test", 123)
|
|
|
|
if !strings.Contains(infoBuf.String(), "info test 123") {
|
|
t.Error("Infof formatted message not found in info buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Error tests error logging
|
|
func TestLegacyLoggerAdapter_Error(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
debugLogger := log.New(&debugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Error("error message")
|
|
|
|
if !strings.Contains(errorBuf.String(), "error message") {
|
|
t.Error("Error message not found in error buffer")
|
|
}
|
|
|
|
// Verify other buffers are empty
|
|
if infoBuf.Len() > 0 || debugBuf.Len() > 0 {
|
|
t.Error("Error should only write to error buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Errorf tests formatted error logging
|
|
func TestLegacyLoggerAdapter_Errorf(t *testing.T) {
|
|
var errorBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Errorf("error %s %d", "test", 456)
|
|
|
|
if !strings.Contains(errorBuf.String(), "error test 456") {
|
|
t.Error("Errorf formatted message not found in error buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Printf tests printf logging (should go to info)
|
|
func TestLegacyLoggerAdapter_Printf(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), infoLogger, log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Printf("printf %s %d", "test", 789)
|
|
|
|
if !strings.Contains(infoBuf.String(), "printf test 789") {
|
|
t.Error("Printf formatted message not found in info buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Println tests println logging (should go to info)
|
|
func TestLegacyLoggerAdapter_Println(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), infoLogger, log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
adapter.Println("println", "test", 999)
|
|
|
|
output := infoBuf.String()
|
|
if !strings.Contains(output, "println") || !strings.Contains(output, "test") || !strings.Contains(output, "999") {
|
|
t.Errorf("Println output missing expected content: %s", output)
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_Fatalf tests fatalf logging (should log and panic)
|
|
func TestLegacyLoggerAdapter_Fatalf(t *testing.T) {
|
|
var errorBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Error("Fatalf should have panicked")
|
|
}
|
|
// Check that error was logged before panic
|
|
if !strings.Contains(errorBuf.String(), "fatal test 123") {
|
|
t.Error("Fatalf should log error before panicking")
|
|
}
|
|
}()
|
|
|
|
adapter.Fatalf("fatal %s %d", "test", 123)
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_WithField tests structured logging (should return same adapter)
|
|
func TestLegacyLoggerAdapter_WithField(t *testing.T) {
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0))
|
|
|
|
fieldLogger := adapter.WithField("key", "value")
|
|
|
|
if fieldLogger != adapter {
|
|
t.Error("WithField should return the same adapter instance (no structured logging support)")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_WithFields tests structured logging with multiple fields
|
|
func TestLegacyLoggerAdapter_WithFields(t *testing.T) {
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0), log.New(&bytes.Buffer{}, "", 0))
|
|
|
|
fields := map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
}
|
|
fieldsLogger := adapter.WithFields(fields)
|
|
|
|
if fieldsLogger != adapter {
|
|
t.Error("WithFields should return the same adapter instance (no structured logging support)")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_EmptyMessages tests logging empty messages
|
|
func TestLegacyLoggerAdapter_EmptyMessages(t *testing.T) {
|
|
var errorBuf, infoBuf, debugBuf bytes.Buffer
|
|
errorLogger := log.New(&errorBuf, "", 0)
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
debugLogger := log.New(&debugBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(errorLogger, infoLogger, debugLogger).(*LegacyLoggerAdapter)
|
|
|
|
// Test empty messages
|
|
adapter.Debug("")
|
|
adapter.Info("")
|
|
adapter.Error("")
|
|
|
|
// Should not crash, buffers should have some content (even if just newlines)
|
|
if debugBuf.Len() == 0 {
|
|
t.Error("Debug with empty message should still write to buffer")
|
|
}
|
|
if infoBuf.Len() == 0 {
|
|
t.Error("Info with empty message should still write to buffer")
|
|
}
|
|
if errorBuf.Len() == 0 {
|
|
t.Error("Error with empty message should still write to buffer")
|
|
}
|
|
}
|
|
|
|
// TestLegacyLoggerAdapter_SpecialCharacters tests logging with special characters
|
|
func TestLegacyLoggerAdapter_SpecialCharacters(t *testing.T) {
|
|
var infoBuf bytes.Buffer
|
|
infoLogger := log.New(&infoBuf, "", 0)
|
|
|
|
adapter := NewLegacyAdapter(log.New(&bytes.Buffer{}, "", 0), infoLogger, log.New(&bytes.Buffer{}, "", 0)).(*LegacyLoggerAdapter)
|
|
|
|
specialMsg := "Message with \n newlines \t tabs and unicode: \u00e9\u00f1\u00fc"
|
|
adapter.Info(specialMsg)
|
|
|
|
if !strings.Contains(infoBuf.String(), specialMsg) {
|
|
t.Error("Special characters should be preserved in log output")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Factory Tests (factory.go - 0% coverage)
|
|
// =============================================================================
|
|
|
|
// TestGetFactory tests the singleton factory
|
|
func TestGetFactory(t *testing.T) {
|
|
factory1 := GetFactory()
|
|
factory2 := GetFactory()
|
|
|
|
if factory1 == nil {
|
|
t.Error("GetFactory should not return nil")
|
|
}
|
|
|
|
if factory1 != factory2 {
|
|
t.Error("GetFactory should return the same instance (singleton)")
|
|
}
|
|
}
|
|
|
|
// TestFactory_SetDefaultLogLevel tests setting default log level
|
|
func TestFactory_SetDefaultLogLevel(t *testing.T) {
|
|
factory := GetFactory()
|
|
|
|
// Clear factory state for clean test
|
|
factory.Clear()
|
|
|
|
factory.SetDefaultLogLevel("debug")
|
|
|
|
// Create a logger and verify it uses the new default level
|
|
logger := factory.createLogger("test")
|
|
|
|
// Test by checking if debug logging works
|
|
var buf bytes.Buffer
|
|
if stdLogger, ok := logger.(*StandardLogger); ok {
|
|
// Create a new logger with our buffer to test the level
|
|
testLogger := NewStandardLogger("debug", nil, nil, &buf)
|
|
testLogger.Debug("test debug")
|
|
|
|
if buf.Len() == 0 {
|
|
t.Error("Debug level should be active when default is set to debug")
|
|
}
|
|
|
|
// Verify the logger is a StandardLogger (not NoOp)
|
|
if stdLogger == nil {
|
|
t.Error("Expected StandardLogger when level is debug")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFactory_GetLogger tests logger creation and caching
|
|
func TestFactory_GetLogger(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.Clear() // Clean state
|
|
|
|
// Test creating a new logger
|
|
logger1 := factory.GetLogger("test-logger")
|
|
if logger1 == nil {
|
|
t.Error("GetLogger should not return nil")
|
|
}
|
|
|
|
// Test that getting the same logger returns cached instance
|
|
logger2 := factory.GetLogger("test-logger")
|
|
if logger1 != logger2 {
|
|
t.Error("GetLogger should return cached instance for same name")
|
|
}
|
|
|
|
// Test creating a different logger
|
|
logger3 := factory.GetLogger("different-logger")
|
|
if logger3 == logger1 {
|
|
t.Error("Different logger names should create different instances")
|
|
}
|
|
}
|
|
|
|
// TestFactory_GetLogger_NoOp tests creating no-op loggers
|
|
func TestFactory_GetLogger_NoOp(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.Clear()
|
|
|
|
noOpNames := []string{"noop", "no-op", "discard"}
|
|
|
|
for _, name := range noOpNames {
|
|
t.Run(name, func(t *testing.T) {
|
|
logger := factory.GetLogger(name)
|
|
|
|
if _, ok := logger.(*NoOpLogger); !ok {
|
|
t.Errorf("GetLogger(%q) should return NoOpLogger", name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFactory_createLogger tests logger creation logic
|
|
func TestFactory_createLogger(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.SetDefaultLogLevel("info")
|
|
|
|
// Test normal logger creation
|
|
logger := factory.createLogger("normal")
|
|
if _, ok := logger.(*StandardLogger); !ok {
|
|
t.Error("createLogger should return StandardLogger for normal names")
|
|
}
|
|
|
|
// Test no-op logger creation
|
|
noOpLogger := factory.createLogger("noop")
|
|
if _, ok := noOpLogger.(*NoOpLogger); !ok {
|
|
t.Error("createLogger should return NoOpLogger for 'noop'")
|
|
}
|
|
}
|
|
|
|
// TestFactory_createLogger_WithEnvironment tests logger creation with environment variables
|
|
func TestFactory_createLogger_WithEnvironment(t *testing.T) {
|
|
// Save original environment
|
|
originalLogToFile := os.Getenv("OIDC_LOG_TO_FILE")
|
|
originalLogDir := os.Getenv("OIDC_LOG_DIR")
|
|
|
|
defer func() {
|
|
// Restore original environment
|
|
os.Setenv("OIDC_LOG_TO_FILE", originalLogToFile)
|
|
os.Setenv("OIDC_LOG_DIR", originalLogDir)
|
|
}()
|
|
|
|
// Create temporary directory for test
|
|
tempDir := t.TempDir()
|
|
|
|
// Set environment to use file logging
|
|
os.Setenv("OIDC_LOG_TO_FILE", "true")
|
|
os.Setenv("OIDC_LOG_DIR", tempDir)
|
|
|
|
factory := GetFactory()
|
|
logger := factory.createLogger("file-test")
|
|
|
|
if _, ok := logger.(*StandardLogger); !ok {
|
|
t.Error("createLogger should return StandardLogger even with file logging")
|
|
}
|
|
|
|
// Test that log files are created when logging
|
|
logger.Info("test message")
|
|
logger.Error("test error")
|
|
logger.Debug("test debug")
|
|
|
|
// Give a moment for file operations
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Check if log files were created (they might be, depending on implementation)
|
|
// This tests the file creation path even if files aren't immediately visible
|
|
expectedFiles := []string{"info.log", "error.log", "debug.log"}
|
|
for _, filename := range expectedFiles {
|
|
filepath := filepath.Join(tempDir, filename)
|
|
if _, err := os.Stat(filepath); err == nil {
|
|
// File exists, which is good - the file creation worked
|
|
t.Logf("Log file created successfully: %s", filepath)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFactory_GetDefaultLogger tests default logger creation and caching
|
|
func TestFactory_GetDefaultLogger(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.Clear()
|
|
|
|
// Test creating default logger
|
|
logger1 := factory.GetDefaultLogger()
|
|
if logger1 == nil {
|
|
t.Error("GetDefaultLogger should not return nil")
|
|
}
|
|
|
|
// Test that getting default logger again returns cached instance
|
|
logger2 := factory.GetDefaultLogger()
|
|
if logger1 != logger2 {
|
|
t.Error("GetDefaultLogger should return cached instance")
|
|
}
|
|
|
|
// Should be a StandardLogger
|
|
if _, ok := logger1.(*StandardLogger); !ok {
|
|
t.Error("GetDefaultLogger should return StandardLogger")
|
|
}
|
|
}
|
|
|
|
// TestFactory_GetNoOpLogger tests no-op logger singleton
|
|
func TestFactory_GetNoOpLogger(t *testing.T) {
|
|
factory := GetFactory()
|
|
|
|
// Test getting no-op logger
|
|
logger1 := factory.GetNoOpLogger()
|
|
if logger1 == nil {
|
|
t.Error("GetNoOpLogger should not return nil")
|
|
}
|
|
|
|
// Test that getting no-op logger again returns same instance
|
|
logger2 := factory.GetNoOpLogger()
|
|
if logger1 != logger2 {
|
|
t.Error("GetNoOpLogger should return same instance")
|
|
}
|
|
|
|
// Should be a NoOpLogger
|
|
if _, ok := logger1.(*NoOpLogger); !ok {
|
|
t.Error("GetNoOpLogger should return NoOpLogger")
|
|
}
|
|
}
|
|
|
|
// TestFactory_Clear tests clearing factory cache
|
|
func TestFactory_Clear(t *testing.T) {
|
|
factory := GetFactory()
|
|
|
|
// Create some loggers
|
|
logger1 := factory.GetLogger("test1")
|
|
defaultLogger1 := factory.GetDefaultLogger()
|
|
|
|
// Clear the factory
|
|
factory.Clear()
|
|
|
|
// Get loggers again - should be new instances
|
|
logger2 := factory.GetLogger("test1")
|
|
defaultLogger2 := factory.GetDefaultLogger()
|
|
|
|
if logger1 == logger2 {
|
|
t.Error("Clear should remove cached loggers")
|
|
}
|
|
|
|
if defaultLogger1 == defaultLogger2 {
|
|
t.Error("Clear should remove cached default logger")
|
|
}
|
|
|
|
// NoOp logger should still be the same (singleton not cleared)
|
|
noOp1 := factory.GetNoOpLogger()
|
|
factory.Clear()
|
|
noOp2 := factory.GetNoOpLogger()
|
|
|
|
if noOp1 != noOp2 {
|
|
t.Error("Clear should not affect NoOp logger singleton")
|
|
}
|
|
}
|
|
|
|
// TestGetOrCreateLogFile tests file creation functionality
|
|
func TestGetOrCreateLogFile(t *testing.T) {
|
|
// Save original environment
|
|
originalLogDir := os.Getenv("OIDC_LOG_DIR")
|
|
defer os.Setenv("OIDC_LOG_DIR", originalLogDir)
|
|
|
|
// Test with custom log directory
|
|
tempDir := t.TempDir()
|
|
os.Setenv("OIDC_LOG_DIR", tempDir)
|
|
|
|
// Test file creation
|
|
writer := getOrCreateLogFile("test.log")
|
|
if writer == nil {
|
|
t.Error("getOrCreateLogFile should not return nil")
|
|
}
|
|
|
|
// Should be able to write to it
|
|
n, err := writer.Write([]byte("test message\n"))
|
|
if err != nil {
|
|
t.Errorf("Should be able to write to log file: %v", err)
|
|
}
|
|
if n == 0 {
|
|
t.Error("Should write some bytes")
|
|
}
|
|
|
|
// Check file was created
|
|
filepath := filepath.Join(tempDir, "test.log")
|
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
|
t.Error("Log file should be created")
|
|
}
|
|
}
|
|
|
|
// TestGetOrCreateLogFile_InvalidDirectory tests fallback behavior
|
|
func TestGetOrCreateLogFile_InvalidDirectory(t *testing.T) {
|
|
// Save original environment
|
|
originalLogDir := os.Getenv("OIDC_LOG_DIR")
|
|
defer os.Setenv("OIDC_LOG_DIR", originalLogDir)
|
|
|
|
// Set invalid directory (file instead of directory)
|
|
tempDir := t.TempDir()
|
|
invalidPath := filepath.Join(tempDir, "not-a-directory.txt")
|
|
|
|
// Create a file where we want a directory
|
|
err := os.WriteFile(invalidPath, []byte("content"), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
os.Setenv("OIDC_LOG_DIR", invalidPath)
|
|
|
|
// Should fall back to stderr
|
|
writer := getOrCreateLogFile("test.log")
|
|
|
|
// Should return stderr (or some valid writer)
|
|
if writer == nil {
|
|
t.Error("getOrCreateLogFile should return stderr as fallback")
|
|
}
|
|
|
|
// Should be able to write (even if it's stderr)
|
|
n, err := writer.Write([]byte("test message\n"))
|
|
if err != nil {
|
|
t.Errorf("Should be able to write to fallback writer: %v", err)
|
|
}
|
|
if n == 0 {
|
|
t.Error("Should write some bytes to fallback")
|
|
}
|
|
}
|
|
|
|
// TestGetOrCreateLogFile_DefaultDirectory tests default directory behavior
|
|
func TestGetOrCreateLogFile_DefaultDirectory(t *testing.T) {
|
|
// Save and clear environment
|
|
originalLogDir := os.Getenv("OIDC_LOG_DIR")
|
|
os.Unsetenv("OIDC_LOG_DIR")
|
|
defer os.Setenv("OIDC_LOG_DIR", originalLogDir)
|
|
|
|
// This should use default directory /var/log/traefik-oidc
|
|
// It will likely fail to create the directory due to permissions,
|
|
// so it should fall back to stderr
|
|
writer := getOrCreateLogFile("test.log")
|
|
|
|
if writer == nil {
|
|
t.Error("getOrCreateLogFile should return a writer (likely stderr as fallback)")
|
|
}
|
|
|
|
// Should be able to write
|
|
n, err := writer.Write([]byte("test message\n"))
|
|
if err != nil {
|
|
t.Errorf("Should be able to write to writer: %v", err)
|
|
}
|
|
if n == 0 {
|
|
t.Error("Should write some bytes")
|
|
}
|
|
}
|
|
|
|
// TestGlobalConvenienceFunctions tests the global convenience functions
|
|
func TestGlobalConvenienceFunctions(t *testing.T) {
|
|
// Clear factory state
|
|
GetFactory().Clear()
|
|
|
|
// Test New function
|
|
logger1 := New("info")
|
|
if logger1 == nil {
|
|
t.Error("New should not return nil")
|
|
}
|
|
|
|
// Test Default function
|
|
defaultLogger := Default()
|
|
if defaultLogger == nil {
|
|
t.Error("Default should not return nil")
|
|
}
|
|
|
|
// Test NoOp function
|
|
noOpLogger := NoOp()
|
|
if noOpLogger == nil {
|
|
t.Error("NoOp should not return nil")
|
|
}
|
|
if _, ok := noOpLogger.(*NoOpLogger); !ok {
|
|
t.Error("NoOp should return NoOpLogger")
|
|
}
|
|
|
|
// Test WithLevel function
|
|
levelLogger := WithLevel("debug")
|
|
if levelLogger == nil {
|
|
t.Error("WithLevel should not return nil")
|
|
}
|
|
if _, ok := levelLogger.(*StandardLogger); !ok {
|
|
t.Error("WithLevel should return StandardLogger")
|
|
}
|
|
}
|
|
|
|
// TestFactory_ConcurrentAccess tests concurrent access to factory
|
|
func TestFactory_ConcurrentAccess(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.Clear()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
loggerMap := make(map[int]Logger)
|
|
var mapMutex sync.Mutex
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
// Test concurrent logger creation
|
|
logger := factory.GetLogger(fmt.Sprintf("concurrent-%d", id))
|
|
|
|
mapMutex.Lock()
|
|
loggerMap[id] = logger
|
|
mapMutex.Unlock()
|
|
|
|
// Test concurrent default logger access
|
|
defaultLogger := factory.GetDefaultLogger()
|
|
if defaultLogger == nil {
|
|
t.Errorf("GetDefaultLogger returned nil in goroutine %d", id)
|
|
}
|
|
|
|
// Test concurrent no-op logger access
|
|
noOpLogger := factory.GetNoOpLogger()
|
|
if noOpLogger == nil {
|
|
t.Errorf("GetNoOpLogger returned nil in goroutine %d", id)
|
|
}
|
|
|
|
// Test concurrent logging
|
|
logger.Info(fmt.Sprintf("message from goroutine %d", id))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all loggers were created
|
|
mapMutex.Lock()
|
|
if len(loggerMap) != numGoroutines {
|
|
t.Errorf("Expected %d loggers, got %d", numGoroutines, len(loggerMap))
|
|
}
|
|
|
|
// Verify all loggers are different (different names should create different instances)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
logger := loggerMap[i]
|
|
if logger == nil {
|
|
t.Errorf("Logger %d is nil", i)
|
|
}
|
|
|
|
// Check it's the right type
|
|
if _, ok := logger.(*StandardLogger); !ok {
|
|
t.Errorf("Logger %d is not StandardLogger", i)
|
|
}
|
|
}
|
|
mapMutex.Unlock()
|
|
}
|
|
|
|
// TestFactory_ConcurrentSameLogger tests concurrent access to same logger
|
|
func TestFactory_ConcurrentSameLogger(t *testing.T) {
|
|
factory := GetFactory()
|
|
factory.Clear()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
loggers := make([]Logger, numGoroutines)
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
// All goroutines request the same logger
|
|
loggers[id] = factory.GetLogger("shared-logger")
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// All should be the same instance (cached)
|
|
firstLogger := loggers[0]
|
|
for i := 1; i < numGoroutines; i++ {
|
|
if loggers[i] != firstLogger {
|
|
t.Errorf("Logger %d should be same instance as first logger", i)
|
|
}
|
|
}
|
|
}
|