package observability
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"testing"
"sourcecraft.dev/bigbes/lethe/internal/config"
)
// TestLoggerInitTintAndJSON exercises Init for both supported formats and
// ensures the package-default logger is replaced.
func TestLoggerInitTintAndJSON(t *testing.T) {
prev := slog.Default()
t.Cleanup(func() { slog.SetDefault(prev) })
for _, format := range []string{"tint", "json"} {
l := &Logger{Cfg: config.LoggingConfig{Level: "info", Format: format}}
if err := l.Init(context.Background()); err != nil {
t.Fatalf("Init(%s): %v", format, err)
}
if l.L == nil {
t.Fatalf("Init(%s): L is nil", format)
}
if slog.Default() != l.L {
t.Fatalf("Init(%s): slog.Default not replaced", format)
}
}
}
func TestLoggerInitRejectsUnknownLevelAndFormat(t *testing.T) {
l := &Logger{Cfg: config.LoggingConfig{Level: "verbose", Format: "json"}}
if err := l.Init(context.Background()); err == nil {
t.Fatalf("Init: expected error for unknown level")
}
l = &Logger{Cfg: config.LoggingConfig{Level: "info", Format: "yaml"}}
if err := l.Init(context.Background()); err == nil {
t.Fatalf("Init: expected error for unknown format")
}
}
// TestContextHandlerAddsRequestIDAndUser proves the contextHandler stamp
// fires for both keys when present, and is silent when absent.
func TestContextHandlerAddsRequestIDAndUser(t *testing.T) {
var buf bytes.Buffer
inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
logger := slog.New(&contextHandler{inner: inner})
ctx := WithUser(WithRequestID(context.Background(), "req-42"), "alice")
logger.InfoContext(ctx, "hello")
var rec map[string]any
if err := json.Unmarshal(buf.Bytes(), &rec); err != nil {
t.Fatalf("unmarshal log line: %v", err)
}
if rec["request_id"] != "req-42" {
t.Fatalf("request_id = %v; want req-42", rec["request_id"])
}
if rec["user"] != "alice" {
t.Fatalf("user = %v; want alice", rec["user"])
}
// Empty context: no request_id/user fields.
buf.Reset()
logger.InfoContext(context.Background(), "anon")
rec = nil
if err := json.Unmarshal(buf.Bytes(), &rec); err != nil {
t.Fatalf("unmarshal anon log line: %v", err)
}
if _, ok := rec["request_id"]; ok {
t.Errorf("anon: request_id present unexpectedly")
}
if _, ok := rec["user"]; ok {
t.Errorf("anon: user present unexpectedly")
}
}
// TestJSONMaskingRedactsSensitiveKeys covers the JSON ReplaceAttr path. The
// scribe tint path is library-tested by go.bigb.es/auxilia.
func TestJSONMaskingRedactsSensitiveKeys(t *testing.T) {
var buf bytes.Buffer
h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{ReplaceAttr: maskAttrJSON})
logger := slog.New(h)
logger.Info("login", slog.String("password", "hunter2"), slog.String("authorization", "Bearer xyz"), slog.String("user", "alice"))
var rec map[string]any
if err := json.Unmarshal(buf.Bytes(), &rec); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if rec["password"] != "***" {
t.Errorf("password = %v; want ***", rec["password"])
}
if rec["authorization"] != "***" {
t.Errorf("authorization = %v; want ***", rec["authorization"])
}
if rec["user"] != "alice" {
t.Errorf("user = %v; want alice (not masked)", rec["user"])
}
}