package logz import ( "bytes" "context" "encoding/json" "log/slog" "strings" "testing" ) // --------------------------------------------------------------- // Duck-type test helpers // --------------------------------------------------------------- type errWithCode struct{ code string } func (e *errWithCode) Error() string { return e.code } func (e *errWithCode) ErrorCode() string { return e.code } type errWithCtx struct{ fields map[string]any } func (e *errWithCtx) Error() string { return "ctx-err" } func (e *errWithCtx) ErrorContext() map[string]any { return e.fields } type errFull struct { code string fields map[string]any } func (e *errFull) Error() string { return e.code } func (e *errFull) ErrorCode() string { return e.code } func (e *errFull) ErrorContext() map[string]any { return e.fields } // --------------------------------------------------------------- // Helper: logger that writes to a buffer for inspection // --------------------------------------------------------------- func newTestLogger(buf *bytes.Buffer, level slog.Level) *slogLogger { handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: level}) return &slogLogger{logger: slog.New(handler)} } // --------------------------------------------------------------- // Constructor tests // --------------------------------------------------------------- func TestNew_ZeroOptions(t *testing.T) { logger := New(Options{}) if logger == nil { t.Fatal("New(Options{}) returned nil") } // Zero value level is INFO — calling Info should not panic. logger.Info("zero options test") } func TestNew_JSONFormat(t *testing.T) { var buf bytes.Buffer sl := &slogLogger{ logger: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})), } sl.Info("json test", "key", "value") var out map[string]any if err := json.Unmarshal(buf.Bytes(), &out); err != nil { t.Fatalf("expected JSON output, got: %s", buf.String()) } if out["msg"] != "json test" { t.Errorf("unexpected msg: %v", out["msg"]) } } func TestNew_StaticArgs(t *testing.T) { var buf bytes.Buffer handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) base := slog.New(handler).With("service", "test-svc") sl := &slogLogger{logger: base} sl.Info("static args test") if !strings.Contains(buf.String(), "test-svc") { t.Errorf("static arg not found in output: %s", buf.String()) } } // --------------------------------------------------------------- // Logger method tests // --------------------------------------------------------------- func TestLogger_Debug_Info_Warn(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelDebug) // None of these should panic. l.Debug("debug msg") l.Info("info msg") l.Warn("warn msg") } func TestLogger_Error_StandardError(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelError) err := &errWithCode{code: "PLAIN"} l.Error("std error", err) if !strings.Contains(buf.String(), "std error") { t.Errorf("message not found in output: %s", buf.String()) } } func TestLogger_Error_NilError(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelError) // Should not panic. l.Error("nil error test", nil) } func TestLogger_Error_DuckTypeCode(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelError) err := &errWithCode{code: "NOT_FOUND"} l.Error("duck code test", err) if !strings.Contains(buf.String(), "NOT_FOUND") { t.Errorf("error_code not found in output: %s", buf.String()) } } func TestLogger_Error_DuckTypeContext(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelError) err := &errWithCtx{fields: map[string]any{"user_id": "abc"}} l.Error("duck ctx test", err) if !strings.Contains(buf.String(), "abc") { t.Errorf("context field not found in output: %s", buf.String()) } } func TestLogger_Error_DuckTypeBoth(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelError) err := &errFull{code: "INTERNAL", fields: map[string]any{"op": "db.query"}} l.Error("full duck test", err) output := buf.String() if !strings.Contains(output, "INTERNAL") { t.Errorf("error_code not found in output: %s", output) } if !strings.Contains(output, "db.query") { t.Errorf("context field not found in output: %s", output) } } func TestLogger_With(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelInfo) child := l.With("component", "auth") child.Info("with test") if !strings.Contains(buf.String(), "auth") { t.Errorf("With attr not found in output: %s", buf.String()) } // Original logger must not have the attribute. buf.Reset() l.Info("original") if strings.Contains(buf.String(), "auth") { t.Errorf("With mutated original logger: %s", buf.String()) } } func TestLogger_WithContext_RequestID(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelInfo) ctx := WithRequestID(context.Background(), "req-123") child := l.WithContext(ctx) child.Info("ctx request id test") if !strings.Contains(buf.String(), "req-123") { t.Errorf("request_id not found in output: %s", buf.String()) } } func TestLogger_WithContext_ExtraFields(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelInfo) ctx := WithField(context.Background(), "tenant", "acme") child := l.WithContext(ctx) child.Info("ctx fields test") if !strings.Contains(buf.String(), "acme") { t.Errorf("context field not found in output: %s", buf.String()) } } func TestLogger_WithContext_EmptyContext(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelInfo) child := l.WithContext(context.Background()) // Empty context — same pointer returned. if child != Logger(l) { t.Error("WithContext with empty context should return the same logger") } } func TestLogger_WithContext_NilContext(t *testing.T) { var buf bytes.Buffer l := newTestLogger(&buf, slog.LevelInfo) // Should not panic. child := l.WithContext(nil) if child != Logger(l) { t.Error("WithContext with nil context should return the same logger") } } // --------------------------------------------------------------- // Context helper tests // --------------------------------------------------------------- func TestWithRequestID_GetRequestID(t *testing.T) { ctx := WithRequestID(context.Background(), "abc-123") if got := GetRequestID(ctx); got != "abc-123" { t.Errorf("GetRequestID = %q, want %q", got, "abc-123") } } func TestGetRequestID_NilContext(t *testing.T) { if got := GetRequestID(nil); got != "" { t.Errorf("GetRequestID(nil) = %q, want empty", got) } } func TestWithField(t *testing.T) { ctx := WithField(context.Background(), "key", "val") fields, ok := ctx.Value(ctxExtraFieldsKey{}).(map[string]any) if !ok { t.Fatal("no fields in context") } if fields["key"] != "val" { t.Errorf("field key = %v, want val", fields["key"]) } } func TestWithFields(t *testing.T) { ctx := WithFields(context.Background(), map[string]any{"a": 1, "b": 2}) fields, ok := ctx.Value(ctxExtraFieldsKey{}).(map[string]any) if !ok { t.Fatal("no fields in context") } if fields["a"] != 1 || fields["b"] != 2 { t.Errorf("fields = %v", fields) } } func TestWithFields_MergePreservesExisting(t *testing.T) { ctx := WithField(context.Background(), "first", "yes") ctx = WithFields(ctx, map[string]any{"second": "yes"}) fields, _ := ctx.Value(ctxExtraFieldsKey{}).(map[string]any) if fields["first"] != "yes" { t.Error("first field was lost after second WithFields call") } if fields["second"] != "yes" { t.Error("second field not present after WithFields call") } } // --------------------------------------------------------------- // enrichErrorAttrs tests // --------------------------------------------------------------- func TestEnrichErrorAttrs_NilError(t *testing.T) { attrs := []any{"existing", "value"} result := enrichErrorAttrs(nil, attrs) if len(result) != len(attrs) { t.Errorf("enrichErrorAttrs(nil) modified attrs: %v", result) } }