Files
logz/logz_test.go

289 lines
7.9 KiB
Go
Raw Permalink Normal View History

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)
}
}