test(diary): config, channels, rotation, redaction and slogtest
- config rules name their key; env strings overlay defaults; D-08 defaults - component floors, verbosity 1-3, quiet on the console only - daily rotation, retention, 0640 files, unwritable dir, lazy open, path confinement - redaction on every driver, console control-character stripping and prefix - loggers before Setup, Reset, HideOnTTY, 100 concurrent writers - testing/slogtest for the console handler and the JSON pipeline - sunscreen InstallDefault still installs a redacting logfmt handler
This commit is contained in:
202
modules/diary/config_test.go
Normal file
202
modules/diary/config_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package diary_test
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/diary"
|
||||
)
|
||||
|
||||
// TestConfigRules: every invalid value in config/log.yaml fails with an
|
||||
// error naming its exact key; YAML lists and comma strings parse alike.
|
||||
func TestConfigRules(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
yaml string
|
||||
key string
|
||||
}{
|
||||
{"level", "level: loud\n", "log.level"},
|
||||
{"default undefined", "default: nowhere\n", "log.default"},
|
||||
{"default empty", "default: \"\"\n", "log.default"},
|
||||
{"driver", "channels:\n daily:\n driver: syslog\n", "log.channels.daily.driver"},
|
||||
{"driver missing", "channels:\n extra:\n level: info\n", "log.channels.extra.driver"},
|
||||
{"format", "channels:\n console:\n format: xml\n", "log.channels.console.format"},
|
||||
{"auto outside console", "channels:\n stderr:\n format: auto\n", "log.channels.stderr.format"},
|
||||
{"channel level", "channels:\n daily:\n level: chatty\n", "log.channels.daily.level"},
|
||||
{"days negative", "channels:\n daily:\n days: -3\n", "log.channels.daily.days"},
|
||||
{"days fraction", "channels:\n daily:\n days: 1.5\n", "log.channels.daily.days"},
|
||||
{"days text", "channels:\n daily:\n days: often\n", "log.channels.daily.days"},
|
||||
{"unknown setting", "channels:\n daily:\n rotate: hourly\n", "log.channels.daily.rotate"},
|
||||
{"channel not a map", "channels:\n daily: yes\n", "log.channels.daily"},
|
||||
{"daily path missing", "channels:\n daily:\n path: \"\"\n", "log.channels.daily.path"},
|
||||
{"path escapes", "channels:\n daily:\n path: ../outside/summer.log\n", "log.channels.daily.path"},
|
||||
{"path escapes after clean", "channels:\n daily:\n path: storage/../../outside.log\n", "log.channels.daily.path"},
|
||||
{"stack empty", "channels:\n stack:\n channels: []\n", "log.channels.stack.channels"},
|
||||
{"stack child", "channels:\n stack:\n channels: [console, nowhere]\n", "log.channels.stack.channels"},
|
||||
{"stack self", "channels:\n stack:\n channels: [console, stack]\n", "log.channels.stack.channels"},
|
||||
{"stack cycle", "default: a\nchannels:\n a:\n driver: stack\n channels: [console, b]\n b:\n driver: stack\n channels: [a]\n", "log.channels.a.channels"},
|
||||
{"component level", "components:\n river: chatty\n", "log.components.river"},
|
||||
{"component nested", "components:\n golem15:\n blog: info\n", "log.components.golem15"},
|
||||
{"components not a map", "components: warn\n", "log.components"},
|
||||
{"channels not a map", "channels: console\n", "log.channels"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := diary.ParseConfig(openYAMLConfig(t, tc.yaml))
|
||||
if err == nil {
|
||||
t.Fatalf("ParseConfig accepted:\n%s", tc.yaml)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.key) {
|
||||
t.Errorf("error %q does not name %s", err, tc.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A cycle error names the path it found.
|
||||
_, err := diary.ParseConfig(openYAMLConfig(t, cases[19].yaml))
|
||||
if err == nil || !strings.Contains(err.Error(), "a -> b -> a") {
|
||||
t.Errorf("cycle error = %v, want the path a -> b -> a", err)
|
||||
}
|
||||
|
||||
// A YAML list, a comma string in YAML and a comma string from the
|
||||
// environment give the same stack.
|
||||
list, err := diary.ParseConfig(openYAMLConfig(t, "channels:\n stack:\n channels: [console, daily]\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
comma, err := diary.ParseConfig(openYAMLConfig(t, "channels:\n stack:\n channels: \" console , daily \"\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env, err := diary.ParseConfig(openConfig(t, "SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(list, comma) || !reflect.DeepEqual(list, env) {
|
||||
t.Errorf("list, comma and env stacks differ:\n list %+v\ncomma %+v\n env %+v", list.Channels["stack"], comma.Channels["stack"], env.Channels["stack"])
|
||||
}
|
||||
|
||||
// Levels accept warning as warn and any case; days 0 keeps every file.
|
||||
got, err := diary.ParseConfig(openYAMLConfig(t, "level: WARNING\nchannels:\n daily:\n days: 0\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Level != slog.LevelWarn || got.Channels["daily"].Days != 0 {
|
||||
t.Errorf("level %v days %d, want WARN and 0", got.Level, got.Channels["daily"].Days)
|
||||
}
|
||||
|
||||
// compass keeps a dotted YAML key literal, so it does not nest; only
|
||||
// the nested form (above, or from SUMMER_LOG__ variables) is refused.
|
||||
got, err = diary.ParseConfig(openYAMLConfig(t, "components:\n golem15.blog: debug\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Components["golem15.blog"] != slog.LevelDebug {
|
||||
t.Errorf("literal dotted component = %v", got.Components)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigEnvStrings: SUMMER_LOG__ overrides arrive as strings and
|
||||
// overlay the defaults key by key, also on top of config/log.yaml.
|
||||
func TestConfigEnvStrings(t *testing.T) {
|
||||
got, err := diary.ParseConfig(openConfig(t,
|
||||
"SUMMER_LOG__LEVEL=debug",
|
||||
"SUMMER_LOG__CHANNELS__DAILY__LEVEL=error",
|
||||
"SUMMER_LOG__CHANNELS__DAILY__DAYS=7",
|
||||
"SUMMER_LOG__COMPONENTS__JOURNAL=debug",
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConfig: %v", err)
|
||||
}
|
||||
want := diary.DefaultConfig()
|
||||
want.Level = slog.LevelDebug
|
||||
daily := want.Channels["daily"]
|
||||
daily.Level, daily.HasLevel, daily.Days = slog.LevelError, true, 7
|
||||
want.Channels["daily"] = daily
|
||||
want.Components["journal"] = slog.LevelDebug
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("env overlay:\n got %+v\nwant %+v", got, want)
|
||||
}
|
||||
|
||||
// The environment wins over the file, and keys the environment does
|
||||
// not set keep the file's values.
|
||||
got, err = diary.ParseConfig(openYAMLConfig(t,
|
||||
"channels:\n daily:\n path: var/log/app.log\n days: 3\n",
|
||||
"SUMMER_LOG__CHANNELS__DAILY__DAYS=9",
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d := got.Channels["daily"]; d.Path != "var/log/app.log" || d.Days != 9 || d.Format != diary.FormatJSON || d.Level != slog.LevelWarn {
|
||||
t.Errorf("daily channel = %+v, want path var/log/app.log, days 9 and the default format and level", d)
|
||||
}
|
||||
|
||||
// A nil config gives the defaults.
|
||||
none, err := diary.ParseConfig(nil)
|
||||
if err != nil || !reflect.DeepEqual(none, diary.DefaultConfig()) {
|
||||
t.Errorf("ParseConfig(nil) = %+v, %v; want the defaults", none, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultConfig: the defaults equal the D-08 example (components river
|
||||
// warn), and the config/log.yaml that summer new writes parses to the
|
||||
// defaults plus its http component.
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
example := `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: warn
|
||||
components:
|
||||
river: warn
|
||||
`
|
||||
got, err := diary.ParseConfig(openYAMLConfig(t, example))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, diary.DefaultConfig()) {
|
||||
t.Errorf("D-08 example:\n got %+v\nwant %+v", got, diary.DefaultConfig())
|
||||
}
|
||||
def := diary.DefaultConfig()
|
||||
if def.Default != "stack" || def.Level != slog.LevelInfo || def.Components["river"] != slog.LevelWarn || len(def.Components) != 1 {
|
||||
t.Errorf("DefaultConfig head = %q %v %v", def.Default, def.Level, def.Components)
|
||||
}
|
||||
for _, name := range []string{"stack", "console", "daily", "stderr", "null"} {
|
||||
if _, ok := def.Channels[name]; !ok {
|
||||
t.Errorf("DefaultConfig lacks channel %q", name)
|
||||
}
|
||||
}
|
||||
// The defaults are a fresh copy each time.
|
||||
def.Components["river"] = slog.LevelDebug
|
||||
if diary.DefaultConfig().Components["river"] != slog.LevelWarn {
|
||||
t.Error("DefaultConfig returns shared maps")
|
||||
}
|
||||
|
||||
generated, err := os.ReadFile(filepath.Join("..", "..", "internal", "build", "testdata", "new", "config__log.yaml.golden"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err = diary.ParseConfig(openYAMLConfig(t, string(generated)))
|
||||
if err != nil {
|
||||
t.Fatalf("generated config/log.yaml: %v", err)
|
||||
}
|
||||
want := diary.DefaultConfig()
|
||||
want.Components["http"] = slog.LevelInfo
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("generated config/log.yaml:\n got %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user