Files
summercms/modules/diary/tracer_smoke_test.go
Jakub Zych e903cc32be feat(diary): component loggers through a redacting console and daily stack
- New diary module: For, Bootstrap, Setup, SetVerbosity, Reset, Close, ParseConfig
- Proxy component handler safe in package vars; styled console handler in bonfire's palette
- Daily writer: one file per day, 0640, lazy open, retention, disables itself on open failure
- Every channel leaf wrapped by sunscreen.Wrap; stack via slog.NewMultiHandler
- bonfire: ColorMode, Palette, DefaultPalette, DetectProfile (lipgloss v2, colorprofile)
- Generated main runs diary.Bootstrap first and diary.Setup after compass.Load
- Ignore /storage/ and /examples/hello/storage/
2026-10-21 11:41:48 +02:00

104 lines
3.0 KiB
Go

package diary_test
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/diary"
)
// earlyLogger is created before any Setup, the way modules create their
// component loggers in package var blocks.
var earlyLogger = diary.For("x")
// TestDiaryTracerSmoke: one record from a component logger flows from
// config/log.yaml through compass into a redacting stack of a console and a
// daily file.
func TestDiaryTracerSmoke(t *testing.T) {
tmp := t.TempDir()
configDir := filepath.Join(tmp, "config")
if err := os.MkdirAll(configDir, 0o755); err != nil {
t.Fatal(err)
}
logYAML := `default: stack
level: info
channels:
stack:
driver: stack
channels: [console, daily]
console:
driver: console
format: auto
daily:
driver: daily
path: storage/logs/summer.log
days: 14
format: json
level: info
`
if err := os.WriteFile(filepath.Join(configDir, "log.yaml"), []byte(logYAML), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{Dir: configDir, Environ: []string{}})
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
now := func() time.Time { return time.Date(2026, 10, 21, 12, 0, 0, 0, time.Local) }
t.Cleanup(diary.Reset)
if err := diary.Setup(cfg, diary.Options{Root: tmp, Stderr: &buf, Color: bonfire.ColorNever, Now: now}); err != nil {
t.Fatalf("Setup: %v", err)
}
diary.For("journal").Info("post saved", "password", "hunter2", "id", 7)
earlyLogger.Info("early logger reached")
console := buf.String()
for _, want := range []string{"component=journal", "post saved", "[REDACTED]", "component=x", "early logger reached"} {
if !strings.Contains(console, want) {
t.Errorf("console output lacks %q:\n%s", want, console)
}
}
if strings.Contains(console, "hunter2") {
t.Errorf("console output leaks the password:\n%s", console)
}
if err := diary.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
file := filepath.Join(tmp, "storage", "logs", "summer-2026-10-21.log")
info, err := os.Stat(file)
if err != nil {
t.Fatalf("daily file: %v", err)
}
if mode := info.Mode().Perm(); mode != 0o640 {
t.Errorf("daily file mode = %o, want 640", mode)
}
raw, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(raw, []byte("hunter2")) {
t.Errorf("daily file leaks the password:\n%s", raw)
}
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
if len(lines) != 2 {
t.Fatalf("daily file has %d lines, want 2:\n%s", len(lines), raw)
}
var rec map[string]any
if err := json.Unmarshal([]byte(lines[0]), &rec); err != nil {
t.Fatalf("daily line is not JSON: %v\n%s", err, lines[0])
}
if rec["component"] != "journal" || rec["password"] != "[REDACTED]" || rec["msg"] != "post saved" {
t.Errorf("daily record = %v, want component journal, password [REDACTED], msg post saved", rec)
}
}