From 3962cb7ac3d75c201cbd5891526e6aa85956ebaa Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 05:27:49 +0000 Subject: [PATCH] refactor(ax): complete logger docs and safe-default regression coverage --- log.go | 9 +++++ log_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/log.go b/log.go index 5f5f4f5..eb4c667 100644 --- a/log.go +++ b/log.go @@ -128,6 +128,12 @@ type Options struct { var RotationWriterFactory func(RotationOptions) goio.WriteCloser // New creates a new Logger with the given options. +// +// logger := log.New(log.Options{ +// Level: log.LevelInfo, +// Output: os.Stdout, +// RedactKeys: []string{"password", "token"}, +// }) func New(opts Options) *Logger { level := normaliseLevel(opts.Level) @@ -359,6 +365,8 @@ func (l *Logger) Security(msg string, keyvals ...any) { // Username returns the current system username. // It uses os/user for reliability and falls back to environment variables. +// +// user := log.Username() func Username() string { if u, err := user.Current(); err == nil { return u.Username @@ -398,6 +406,7 @@ func Default() *Logger { } // SetDefault sets the default logger. +// Passing nil is ignored to preserve the current default logger. // // log.SetDefault(customLogger) func SetDefault(l *Logger) { diff --git a/log_test.go b/log_test.go index 35227bd..5a573b2 100644 --- a/log_test.go +++ b/log_test.go @@ -3,6 +3,7 @@ package log import ( "bytes" goio "io" + "os" "strings" "testing" ) @@ -120,6 +121,22 @@ func TestLogger_Redaction_Good(t *testing.T) { } } +func TestLogger_Redaction_Good_CaseInsensitiveKeys(t *testing.T) { + var buf bytes.Buffer + l := New(Options{ + Level: LevelInfo, + Output: &buf, + RedactKeys: []string{"password"}, + }) + + l.Info("login", "PASSWORD", "secret123") + + output := buf.String() + if !strings.Contains(output, "PASSWORD=\"[REDACTED]\"") { + t.Errorf("expected case-insensitive redaction, got %q", output) + } +} + func TestLogger_InjectionPrevention_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelInfo, Output: &buf}) @@ -248,6 +265,17 @@ func TestLogger_SetOutput_Good(t *testing.T) { } } +func TestLogger_SetOutput_Bad_NilFallsBackToStderr(t *testing.T) { + var buf bytes.Buffer + l := New(Options{Level: LevelInfo, Output: &buf}) + + l.SetOutput(nil) + + if l.output != os.Stderr { + t.Errorf("expected nil output to fallback to os.Stderr, got %T", l.output) + } +} + func TestLogger_SetRedactKeys_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelInfo, Output: &buf}) @@ -333,6 +361,32 @@ func TestNew_RotationFactory_Good(t *testing.T) { } } +func TestNew_RotationFactory_Good_DefaultRetentionValues(t *testing.T) { + original := RotationWriterFactory + defer func() { RotationWriterFactory = original }() + + var captured RotationOptions + RotationWriterFactory = func(opts RotationOptions) goio.WriteCloser { + captured = opts + return nopWriteCloser{goio.Discard} + } + + _ = New(Options{ + Level: LevelInfo, + Rotation: &RotationOptions{Filename: "test.log"}, + }) + + if captured.MaxSize != defaultRotationMaxSize { + t.Errorf("expected default MaxSize=%d, got %d", defaultRotationMaxSize, captured.MaxSize) + } + if captured.MaxAge != defaultRotationMaxAge { + t.Errorf("expected default MaxAge=%d, got %d", defaultRotationMaxAge, captured.MaxAge) + } + if captured.MaxBackups != defaultRotationMaxBackups { + t.Errorf("expected default MaxBackups=%d, got %d", defaultRotationMaxBackups, captured.MaxBackups) + } +} + func TestNew_DefaultOutput_Good(t *testing.T) { // No output or rotation — should default to stderr (not nil) l := New(Options{Level: LevelInfo}) @@ -341,6 +395,13 @@ func TestNew_DefaultOutput_Good(t *testing.T) { } } +func TestNew_Bad_InvalidLevelDefaultsToInfo(t *testing.T) { + l := New(Options{Level: Level(99)}) + if l.Level() != LevelInfo { + t.Errorf("expected invalid level to default to info, got %v", l.Level()) + } +} + func TestUsername_Good(t *testing.T) { name := Username() if name == "" { @@ -379,3 +440,47 @@ func TestDefault_Good(t *testing.T) { } } } + +func TestDefault_Bad_SetDefaultNilIgnored(t *testing.T) { + original := Default() + var buf bytes.Buffer + custom := New(Options{Level: LevelInfo, Output: &buf}) + SetDefault(custom) + defer SetDefault(original) + + SetDefault(nil) + + if Default() != custom { + t.Error("expected SetDefault(nil) to preserve the current default logger") + } +} + +func TestLogger_StyleHooks_Bad_NilHooksDoNotPanic(t *testing.T) { + var buf bytes.Buffer + l := New(Options{Level: LevelDebug, Output: &buf}) + l.StyleTimestamp = nil + l.StyleDebug = nil + l.StyleInfo = nil + l.StyleWarn = nil + l.StyleError = nil + l.StyleSecurity = nil + + defer func() { + if r := recover(); r != nil { + t.Fatalf("expected nil style hooks not to panic, got panic: %v", r) + } + }() + + l.Debug("debug") + l.Info("info") + l.Warn("warn") + l.Error("error") + l.Security("security") + + output := buf.String() + for _, tag := range []string{"[DBG]", "[INF]", "[WRN]", "[ERR]", "[SEC]"} { + if !strings.Contains(output, tag) { + t.Errorf("expected %s in output, got %q", tag, output) + } + } +}