diff --git a/site_cache_admin_test.go b/site_cache_admin_test.go index 68d7ee4..0df67a8 100644 --- a/site_cache_admin_test.go +++ b/site_cache_admin_test.go @@ -330,10 +330,10 @@ func TestPostsAdminPurgesSiteCache(t *testing.T) { }) } -// TestSiteCacheFrameworkKeysWin proves the framework keys site_cache.* take +// TestCategoriesAdminFrameworkKeysWin proves the framework keys site_cache.* take // precedence over the Journal's deprecated keys: with both set, a category // save purges through the framework target only. -func TestSiteCacheFrameworkKeysWin(t *testing.T) { +func TestCategoriesAdminFrameworkKeysWin(t *testing.T) { framework := newPurgeReceiver(t) alias := newPurgeReceiver(t) env := newPostsEnv(t, append(alias.env(), diff --git a/site_cache_keys_test.go b/site_cache_keys_test.go new file mode 100644 index 0000000..c473256 --- /dev/null +++ b/site_cache_keys_test.go @@ -0,0 +1,222 @@ +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) + } +}