package diary_test import ( "bytes" "log/slog" "os" "path/filepath" "reflect" "strings" "testing" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/diary" ) // openConfig opens a compass config from an empty config directory with // only the given environment. func openConfig(t *testing.T, environ ...string) *compass.Config { t.Helper() dir := filepath.Join(t.TempDir(), "config") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } cfg, err := compass.Open(compass.Options{Dir: dir, Environ: append([]string{}, environ...)}) if err != nil { t.Fatal(err) } return cfg } // TestConfigEnvSmoke: SUMMER_LOG__ overrides arrive as strings, parse, and // overlay the framework defaults key by key; every invalid value fails with // an error naming its key. func TestConfigEnvSmoke(t *testing.T) { cfg := openConfig(t, "SUMMER_LOG__CHANNELS__DAILY__DAYS=30", "SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily", "SUMMER_LOG__COMPONENTS__RIVER=error", ) got, err := diary.ParseConfig(cfg) if err != nil { t.Fatalf("ParseConfig: %v", err) } want := diary.DefaultConfig() daily := want.Channels["daily"] daily.Days = 30 want.Channels["daily"] = daily want.Components["river"] = slog.LevelError if !reflect.DeepEqual(got, want) { t.Errorf("parsed config:\n got %+v\nwant %+v", got, want) } if ch := got.Channels["stack"].Channels; !reflect.DeepEqual(ch, []string{"console", "daily"}) { t.Errorf("stack children = %q, want console, daily", ch) } errorCases := []struct { name string environ []string key string }{ {"level", []string{"SUMMER_LOG__LEVEL=loud"}, "log.level"}, {"default", []string{"SUMMER_LOG__DEFAULT=missing"}, "log.default"}, {"driver", []string{"SUMMER_LOG__CHANNELS__DAILY__DRIVER=syslog"}, "log.channels.daily.driver"}, {"driver required", []string{"SUMMER_LOG__CHANNELS__EXTRA__LEVEL=info"}, "log.channels.extra.driver"}, {"format", []string{"SUMMER_LOG__CHANNELS__CONSOLE__FORMAT=xml"}, "log.channels.console.format"}, {"auto outside console", []string{"SUMMER_LOG__CHANNELS__DAILY__FORMAT=auto"}, "log.channels.daily.format"}, {"channel level", []string{"SUMMER_LOG__CHANNELS__CONSOLE__LEVEL=verbose"}, "log.channels.console.level"}, {"days", []string{"SUMMER_LOG__CHANNELS__DAILY__DAYS=-1"}, "log.channels.daily.days"}, {"days text", []string{"SUMMER_LOG__CHANNELS__DAILY__DAYS=two"}, "log.channels.daily.days"}, {"path escapes root", []string{"SUMMER_LOG__CHANNELS__DAILY__PATH=../outside/summer.log"}, "log.channels.daily.path"}, {"stack child", []string{"SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,nowhere"}, "log.channels.stack.channels"}, {"stack cycle", []string{ "SUMMER_LOG__DEFAULT=a", "SUMMER_LOG__CHANNELS__A__DRIVER=stack", "SUMMER_LOG__CHANNELS__A__CHANNELS=console,b", "SUMMER_LOG__CHANNELS__B__DRIVER=stack", "SUMMER_LOG__CHANNELS__B__CHANNELS=a", }, "log.channels.a.channels"}, {"component level", []string{"SUMMER_LOG__COMPONENTS__RIVER=chatty"}, "log.components.river"}, } for _, tc := range errorCases { _, err := diary.ParseConfig(openConfig(t, tc.environ...)) if err == nil { t.Errorf("%s: ParseConfig accepted %q", tc.name, tc.environ) continue } if !strings.Contains(err.Error(), tc.key) { t.Errorf("%s: error %q does not name %s", tc.name, err, tc.key) } } } // TestDailyDegradesSmoke: a daily channel whose directory cannot be created // warns once on the console, is disabled, and never fails Setup or stops // later records from reaching the console. func TestDailyDegradesSmoke(t *testing.T) { if os.Geteuid() == 0 { t.Skip("root ignores directory permissions") } root := t.TempDir() readOnly := filepath.Join(root, "readonly") if err := os.Mkdir(readOnly, 0o555); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(readOnly, 0o755) }) cfg := openConfig(t, "SUMMER_LOG__CHANNELS__DAILY__PATH=readonly/logs/summer.log") var buf bytes.Buffer t.Cleanup(diary.Reset) if err := diary.Setup(cfg, diary.Options{Root: root, Stderr: &buf, Color: bonfire.ColorNever}); err != nil { t.Fatalf("Setup failed because of the log file: %v", err) } logger := diary.For("journal") logger.Warn("first warning") logger.Warn("second warning") logger.Info("still logging") out := buf.String() if n := strings.Count(out, "daily log file disabled"); n != 1 { t.Errorf("daily warning appears %d times, want 1:\n%s", n, out) } if !strings.Contains(out, filepath.Join(readOnly, "logs")) { t.Errorf("warning does not name the path:\n%s", out) } if !strings.Contains(out, "component=diary") { t.Errorf("warning does not come from component diary:\n%s", out) } for _, want := range []string{"first warning", "second warning", "still logging"} { if !strings.Contains(out, want) { t.Errorf("console lacks %q after the daily channel failed:\n%s", want, out) } } }