package logging import ( "bytes" "encoding/json" "log" "strings" "testing" "ja4sentinel/api" ) func TestIsLogLevelEnabled(t *testing.T) { tests := []struct { name string loggerLevel string messageLevel string want bool }{ {name: "debug logger accepts debug", loggerLevel: "debug", messageLevel: "debug", want: true}, {name: "debug logger accepts info", loggerLevel: "debug", messageLevel: "info", want: true}, {name: "info logger rejects debug", loggerLevel: "info", messageLevel: "debug", want: false}, {name: "info logger accepts info", loggerLevel: "info", messageLevel: "info", want: true}, {name: "warn logger rejects info", loggerLevel: "warn", messageLevel: "info", want: false}, {name: "warn logger accepts error", loggerLevel: "warn", messageLevel: "error", want: true}, {name: "error logger accepts only error", loggerLevel: "error", messageLevel: "error", want: true}, {name: "error logger rejects warn", loggerLevel: "error", messageLevel: "warn", want: false}, {name: "invalid level rejects all", loggerLevel: "invalid", messageLevel: "info", want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger := NewServiceLogger(tt.loggerLevel) if got := logger.isLogLevelEnabled(tt.messageLevel); got != tt.want { t.Fatalf("isLogLevelEnabled(%q) = %v, want %v", tt.messageLevel, got, tt.want) } }) } } func TestDebug_NotEmittedWhenLoggerLevelInfo(t *testing.T) { logger := NewServiceLogger("info") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Debug("service", "debug message", map[string]string{"k": "v"}) if buf.Len() != 0 { t.Fatalf("expected no output for debug at info level, got: %s", buf.String()) } } func TestLog_UppercaseDebug_NotEmittedWhenLoggerLevelInfo(t *testing.T) { logger := NewServiceLogger("info") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Log("service", "DEBUG", "debug message", nil) if strings.TrimSpace(buf.String()) != "" { t.Fatalf("expected no output for uppercase DEBUG at info level, got: %s", buf.String()) } } func TestInfo_EmitedWhenLoggerLevelInfo(t *testing.T) { logger := NewServiceLogger("info") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Info("service", "info message", map[string]string{"key": "value"}) if buf.Len() == 0 { t.Fatal("expected output for info at info level") } // Verify JSON format var got map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("output is not valid JSON: %v", err) } if got["level"] != "INFO" { t.Errorf("level = %v, want INFO", got["level"]) } if got["component"] != "service" { t.Errorf("component = %v, want service", got["component"]) } if got["message"] != "info message" { t.Errorf("message = %v, want info message", got["message"]) } if got["key"] != "value" { t.Errorf("key = %v, want value", got["key"]) } } func TestWarn_EmitedWhenLoggerLevelWarn(t *testing.T) { logger := NewServiceLogger("warn") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Warn("service", "warn message", nil) if buf.Len() == 0 { t.Fatal("expected output for warn at warn level") } } func TestError_AlwaysEmitted(t *testing.T) { levels := []string{"debug", "info", "warn", "error"} for _, level := range levels { t.Run(level, func(t *testing.T) { logger := NewServiceLogger(level) var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Error("service", "error message", map[string]string{"error": "test"}) if buf.Len() == 0 { t.Fatalf("expected output for error at %s level", level) } }) } } func TestLog_EmptyDetails(t *testing.T) { logger := NewServiceLogger("debug") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Info("service", "test message", nil) if buf.Len() == 0 { t.Fatal("expected output") } var got map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("output is not valid JSON: %v", err) } // Details should not be present when nil/empty if _, ok := got["details"]; ok { t.Error("details should not be present when nil") } } func TestLog_WithDetails(t *testing.T) { logger := NewServiceLogger("debug") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) details := map[string]string{ "error": "test error", "trace_id": "abc123", } logger.Info("service", "test message", details) var got map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("output is not valid JSON: %v", err) } if got["error"] != "test error" { t.Errorf("error = %v, want test error", got["error"]) } if got["trace_id"] != "abc123" { t.Errorf("trace_id = %v, want abc123", got["trace_id"]) } } func TestLog_TimestampPresent(t *testing.T) { logger := NewServiceLogger("debug") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) logger.Info("service", "test", nil) var got map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("output is not valid JSON: %v", err) } if _, ok := got["timestamp"]; !ok { t.Error("timestamp should be present") } } func TestLoggerFactory(t *testing.T) { factory := &LoggerFactory{} // Test NewLogger with different levels levels := []string{"debug", "info", "warn", "error"} for _, level := range levels { t.Run(level, func(t *testing.T) { logger := factory.NewLogger(level) if logger == nil { t.Fatalf("NewLogger(%q) returned nil", level) } }) } // Test NewDefaultLogger logger := factory.NewDefaultLogger() if logger == nil { t.Fatal("NewDefaultLogger() returned nil") } } func TestServiceLogger_ImplementsApiLogger(t *testing.T) { logger := NewServiceLogger("debug") // Verify it implements the interface var _ api.Logger = logger } func TestServiceLogger_ConcurrentLogging(t *testing.T) { logger := NewServiceLogger("debug") var buf bytes.Buffer logger.out = log.New(&buf, "", 0) done := make(chan bool) for i := 0; i < 10; i++ { go func(id int) { logger.Info("service", "concurrent message", map[string]string{"id": string(rune(id))}) done <- true }(i) } for i := 0; i < 10; i++ { <-done } // Should have 10 lines lines := strings.Split(strings.TrimSpace(buf.String()), "\n") if len(lines) != 10 { t.Errorf("expected 10 lines, got %d", len(lines)) } }