package journal import ( "bytes" "log/slog" "strings" "sync" "testing" "git.golem15.com/golem15/sm-journal-plugin/classes" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/cooler" ) const siteCacheKeysToken = "journal-site-cache-keys-token-0123456789" type keysLogBuffer struct { mu sync.Mutex buf bytes.Buffer } func (b *keysLogBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.buf.Write(p) } func (b *keysLogBuffer) lines() []string { b.mu.Lock() defer b.mu.Unlock() var out []string for _, l := range strings.Split(strings.TrimSpace(b.buf.String()), "\n") { if l != "" { out = append(out, l) } } return out } // siteCacheApp returns an app on a config without a database holding values, // with slog.Default captured for the test. func siteCacheApp(t *testing.T, values map[string]string) (*backpack.App, *keysLogBuffer) { t.Helper() cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{}}) if err != nil { t.Fatal(err) } for k, v := range values { if err := cfg.Set(k, v); err != nil { t.Fatal(err) } } buf := &keysLogBuffer{} prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(buf, nil))) t.Cleanup(func() { slog.SetDefault(prev) }) return backpack.New(cfg), buf } func assertNoPurgeToken(t *testing.T, lines []string) { t.Helper() for _, l := range lines { if strings.Contains(l, siteCacheKeysToken) { t.Fatalf("log line carries the token: %s", l) } } } func TestSiteCacheFrameworkKeysWin(t *testing.T) { framework := map[string]string{ cooler.SiteCachePurgeURLKey: "http://127.0.0.1:3000/api/__site__/cache/purge", cooler.SiteCachePurgeTokenKey: siteCacheKeysToken, } t.Run("framework keys only: the Journal publishes nothing and stays quiet", func(t *testing.T) { app, logs := siteCacheApp(t, framework) if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatal(err) } if p, ok := app.Lookup[*cooler.SiteCachePurger](); ok && p != nil { t.Fatal("the Journal published a purger although the framework keys are set") } if lines := logs.lines(); len(lines) != 0 { t.Fatalf("log = %q, want nothing", lines) } // The framework builds the purger from its own keys. if p := cooler.SiteCacheFromApp(app); p == nil || p.Endpoint() != "127.0.0.1:3000/api/__site__/cache/purge" { t.Fatalf("cooler.SiteCacheFromApp = %v", p) } }) t.Run("alias keys too: one warning that they are ignored", func(t *testing.T) { values := map[string]string{ classes.SiteCachePurgeURLKey: "http://127.0.0.1:4000/purge", classes.SiteCachePurgeTokenKey: "x", // invalid, but ignored } for k, v := range framework { values[k] = v } app, logs := siteCacheApp(t, values) if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatalf("ignored alias keys must not fail boot: %v", err) } if p, ok := app.Lookup[*cooler.SiteCachePurger](); ok && p != nil { t.Fatal("the Journal published a purger from ignored keys") } lines := logs.lines() if len(lines) != 1 || !strings.Contains(lines[0], "level=WARN") || !strings.Contains(lines[0], "ignored") || !strings.Contains(lines[0], cooler.SiteCachePurgeURLKey) { t.Fatalf("log = %q, want one warning naming the framework keys", lines) } assertNoPurgeToken(t, lines) }) t.Run("only the framework token set still counts as framework keys", func(t *testing.T) { app, _ := siteCacheApp(t, map[string]string{ cooler.SiteCachePurgeTokenKey: siteCacheKeysToken, classes.SiteCachePurgeURLKey: "http://127.0.0.1:4000/purge", classes.SiteCachePurgeTokenKey: siteCacheKeysToken, }) if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatal(err) } if p, ok := app.Lookup[*cooler.SiteCachePurger](); ok && p != nil { t.Fatal("alias keys were used although a framework key is set") } }) } func TestSiteCacheAliasKeysDeprecated(t *testing.T) { t.Run("valid alias keys publish a purger with one deprecation warning", func(t *testing.T) { app, logs := siteCacheApp(t, map[string]string{ classes.SiteCachePurgeURLKey: "http://127.0.0.1:4000/api/__site__/cache/purge", classes.SiteCachePurgeTokenKey: siteCacheKeysToken, }) if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatal(err) } p, ok := app.Lookup[*cooler.SiteCachePurger]() if !ok || p == nil || p.Endpoint() != "127.0.0.1:4000/api/__site__/cache/purge" { t.Fatalf("published purger = %v", p) } if got := cooler.SiteCacheFromApp(app); got != p { t.Fatal("cooler.SiteCacheFromApp did not return the alias purger") } lines := logs.lines() if len(lines) != 1 || !strings.Contains(lines[0], "level=WARN") || !strings.Contains(lines[0], "deprecated") || !strings.Contains(lines[0], cooler.SiteCachePurgeURLKey) || !strings.Contains(lines[0], cooler.SiteCachePurgeTokenKey) { t.Fatalf("log = %q, want one deprecation warning naming both framework keys", lines) } assertNoPurgeToken(t, lines) }) for _, tc := range []struct { name, url, token, key string }{ {"only the alias URL", "http://127.0.0.1:4000/purge", "", classes.SiteCachePurgeTokenKey}, {"only the alias token", "", siteCacheKeysToken, classes.SiteCachePurgeURLKey}, {"http to a remote host", "http://example.test/purge", siteCacheKeysToken, classes.SiteCachePurgeURLKey}, {"a short token", "http://127.0.0.1:4000/purge", "short", classes.SiteCachePurgeTokenKey}, } { t.Run("invalid alias keys fail boot: "+tc.name, func(t *testing.T) { app, _ := siteCacheApp(t, map[string]string{ classes.SiteCachePurgeURLKey: tc.url, classes.SiteCachePurgeTokenKey: tc.token, }) err := (&Plugin{}).wireSiteCache(app) if err == nil || !strings.Contains(err.Error(), tc.key) { t.Fatalf("wireSiteCache = %v, want an error naming %s", err, tc.key) } if tc.token != "" && strings.Contains(err.Error(), tc.token) { t.Fatalf("error %q carries the token", err) } if p, ok := app.Lookup[*cooler.SiteCachePurger](); ok && p != nil { t.Fatal("an invalid alias configuration published a purger") } }) } t.Run("an already published purger is kept", func(t *testing.T) { app, _ := siteCacheApp(t, map[string]string{ classes.SiteCachePurgeURLKey: "http://127.0.0.1:4000/purge", classes.SiteCachePurgeTokenKey: siteCacheKeysToken, }) first, err := cooler.NewSiteCachePurger("http://127.0.0.1:5000/purge", siteCacheKeysToken) if err != nil { t.Fatal(err) } if err := app.Publish(first); err != nil { t.Fatal(err) } if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatal(err) } if got, _ := app.Lookup[*cooler.SiteCachePurger](); got != first { t.Fatal("wireSiteCache replaced a published purger") } }) } func TestSiteCacheNoKeys(t *testing.T) { app, logs := siteCacheApp(t, nil) if err := (&Plugin{}).wireSiteCache(app); err != nil { t.Fatal(err) } if p, ok := app.Lookup[*cooler.SiteCachePurger](); ok && p != nil { t.Fatal("no keys published a purger") } if p := cooler.SiteCacheFromApp(app); p != nil { t.Fatalf("cooler.SiteCacheFromApp = %v, want nil", p) } if lines := logs.lines(); len(lines) != 0 { t.Fatalf("log = %q, want nothing", lines) } if err := (&Plugin{}).wireSiteCache(nil); err != nil { t.Fatalf("wireSiteCache(nil) = %v", err) } if err := (&Plugin{}).wireSiteCache(backpack.New(nil)); err != nil { t.Fatalf("wireSiteCache(no config) = %v", err) } }