- 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/
104 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|