- pact: HasMenuItemTypes, MenuItemType, MenuItemReference, MenuItemResolve,
ResolvedMenuItem, MenuItemAlternate, DependentOptionsProvider,
DependentOptionSet; Option gains Depth (omitempty)
- signpost (new module): boot-time registry with fail-loud key and pattern
checks, layered URL patterns incl. PatternFrom for list types, host
overrides at menu_items.patterns, Resolve and ReferenceOptions
- party: signpost.Activate after postcard, before Boot
- cabana: dependsOn on dropdowns (boot checks, controller-only), GET
fields/{field}/dependent-options with swag, inventories, conformance case
- companions fixture, TestSignpostSmoke, TestCompanionsDependentSmoke, docs
154 lines
6.6 KiB
Go
154 lines
6.6 KiB
Go
package signpost_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
)
|
|
|
|
type typesPlugin struct {
|
|
id string
|
|
types []pact.MenuItemType
|
|
}
|
|
|
|
func (p typesPlugin) ID() string { return p.id }
|
|
func (p typesPlugin) MenuItemTypes() []pact.MenuItemType { return p.types }
|
|
|
|
// echo resolves an item to its layered pattern, so a test sees the layer the
|
|
// registry picked.
|
|
func echo(_ context.Context, in pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
|
|
return &pact.ResolvedMenuItem{Title: in.Type, URL: in.Pattern, Alternates: []pact.MenuItemAlternate{{Locale: "en", URL: in.Pattern}}}, nil
|
|
}
|
|
|
|
func smokeApp(t *testing.T, menuItems string) *backpack.App {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if menuItems != "" {
|
|
if err := os.WriteFile(filepath.Join(dir, "menu_items.yaml"), []byte(menuItems), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{"SUMMER_ENV=testing"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return backpack.New(cfg)
|
|
}
|
|
|
|
func acmeTypes() []pact.MenuItemType {
|
|
return []pact.MenuItemType{
|
|
{Key: "acme-post", Label: "Post", DefaultPattern: "/blog/:slug", Resolve: echo,
|
|
References: func(context.Context) ([]pact.MenuItemReference, error) {
|
|
return []pact.MenuItemReference{{Value: "1", Label: "First", Items: []pact.MenuItemReference{{Value: "2", Label: "Child"}}}, {Value: "3", Label: "Second"}}, nil
|
|
}},
|
|
{Key: "acme-posts", Label: "All posts", DefaultPattern: "/blog/:slug", PatternFrom: "acme-post", Resolve: echo},
|
|
{Key: "acme-page", Label: "Page", DefaultPattern: "/:slug", Resolve: echo},
|
|
}
|
|
}
|
|
|
|
// TestSignpostSmoke checks the registry's boot refusals and the D-02 pattern
|
|
// layers, including a list type that follows its record type's host override.
|
|
func TestSignpostSmoke(t *testing.T) {
|
|
t.Run("duplicate key names both plugins", func(t *testing.T) {
|
|
app := smokeApp(t, "")
|
|
err := signpost.Activate(app, []typesPlugin{
|
|
{id: "acme.blog", types: []pact.MenuItemType{{Key: "acme-post", DefaultPattern: "/a", Resolve: echo}}},
|
|
{id: "acme.news", types: []pact.MenuItemType{{Key: "acme-post", DefaultPattern: "/b", Resolve: echo}}},
|
|
})
|
|
if err == nil || !strings.HasPrefix(err.Error(), "signpost: ") || !strings.Contains(err.Error(), "acme.blog") || !strings.Contains(err.Error(), "acme.news") || !strings.Contains(err.Error(), "acme-post") {
|
|
t.Fatalf("duplicate key error = %v", err)
|
|
}
|
|
})
|
|
t.Run("boot refusals", func(t *testing.T) {
|
|
for name, types := range map[string][]pact.MenuItemType{
|
|
"empty key": {{Key: "", DefaultPattern: "/a", Resolve: echo}},
|
|
"nil resolve": {{Key: "acme-a", DefaultPattern: "/a"}},
|
|
"empty pattern": {{Key: "acme-a", Resolve: echo}},
|
|
"relative pattern": {{Key: "acme-a", DefaultPattern: "a/:slug", Resolve: echo}},
|
|
"dangling PatternFrom": {{Key: "acme-a", DefaultPattern: "/a", PatternFrom: "acme-missing", Resolve: echo}},
|
|
"self PatternFrom": {{Key: "acme-a", DefaultPattern: "/a", PatternFrom: "acme-a", Resolve: echo}},
|
|
"chained PatternFrom": {
|
|
{Key: "acme-a", DefaultPattern: "/a", Resolve: echo},
|
|
{Key: "acme-b", DefaultPattern: "/b", PatternFrom: "acme-a", Resolve: echo},
|
|
{Key: "acme-c", DefaultPattern: "/c", PatternFrom: "acme-b", Resolve: echo},
|
|
},
|
|
} {
|
|
err := signpost.Activate(smokeApp(t, ""), []typesPlugin{{id: "acme.blog", types: types}})
|
|
if err == nil || !strings.HasPrefix(err.Error(), "signpost: ") {
|
|
t.Fatalf("%s: error = %v", name, err)
|
|
}
|
|
}
|
|
err := signpost.Activate(smokeApp(t, "patterns:\n acme-unknown: /x\n"), []typesPlugin{{id: "acme.blog", types: acmeTypes()}})
|
|
if err == nil || !strings.Contains(err.Error(), "acme-unknown") {
|
|
t.Fatalf("unknown host override error = %v", err)
|
|
}
|
|
})
|
|
t.Run("pattern layers", func(t *testing.T) {
|
|
app := smokeApp(t, "patterns:\n acme-post: /news/:slug\n acme-page: /p/:slug\n")
|
|
if err := signpost.Activate(app, []typesPlugin{{id: "acme.blog", types: acmeTypes()}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg, ok := app.Lookup[*signpost.Registry]()
|
|
if !ok || len(reg.Types()) != 3 {
|
|
t.Fatalf("registry not published: %v", ok)
|
|
}
|
|
for _, tc := range []struct{ key, item, want string }{
|
|
{"acme-page", "", "/p/:slug"}, // host override
|
|
{"acme-page", "/custom/:slug", "/custom/:slug"}, // item override beats host
|
|
{"acme-post", "", "/news/:slug"}, // host override
|
|
{"acme-posts", "", "/news/:slug"}, // list type follows its record type's host override
|
|
{"acme-posts", "/mine/:slug", "/mine/:slug"}, // item override beats both
|
|
{"acme-unknown", "", ""},
|
|
} {
|
|
if got := reg.Pattern(tc.key, tc.item); got != tc.want {
|
|
t.Fatalf("Pattern(%q, %q) = %q, want %q", tc.key, tc.item, got, tc.want)
|
|
}
|
|
}
|
|
resolved, err := reg.Resolve(context.Background(), "acme-posts", "", "", false, "")
|
|
if err != nil || resolved.URL != "/news/:slug" {
|
|
t.Fatalf("Resolve = %+v, %v", resolved, err)
|
|
}
|
|
if _, err := reg.Resolve(context.Background(), "acme-unknown", "", "", false, ""); err == nil {
|
|
t.Fatal("unknown type resolved")
|
|
}
|
|
set, err := signpost.ReferenceOptions(context.Background(), reg, "acme-post")
|
|
if err != nil || !set.Applies || len(set.Options) != 3 || set.Options[1].Depth != 1 || set.Options[2].Depth != 0 {
|
|
t.Fatalf("ReferenceOptions = %+v, %v", set, err)
|
|
}
|
|
none, err := signpost.ReferenceOptions(context.Background(), reg, "acme-page")
|
|
if err != nil || none.Applies {
|
|
t.Fatalf("type without references = %+v, %v", none, err)
|
|
}
|
|
})
|
|
t.Run("host override of the list type beats its record type", func(t *testing.T) {
|
|
app := smokeApp(t, "patterns:\n acme-post: /news/:slug\n acme-posts: /all/:slug\n")
|
|
if err := signpost.Activate(app, []typesPlugin{{id: "acme.blog", types: acmeTypes()}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg, _ := app.Lookup[*signpost.Registry]()
|
|
if got := reg.Pattern("acme-posts", ""); got != "/all/:slug" {
|
|
t.Fatalf("list type host override = %q", got)
|
|
}
|
|
if got := reg.Pattern("acme-posts", "/item/:slug"); got != "/item/:slug" {
|
|
t.Fatalf("item override = %q", got)
|
|
}
|
|
})
|
|
t.Run("default pattern without host config", func(t *testing.T) {
|
|
app := smokeApp(t, "")
|
|
if err := signpost.Activate(app, []typesPlugin{{id: "acme.blog", types: acmeTypes()}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg, _ := app.Lookup[*signpost.Registry]()
|
|
if got := reg.Pattern("acme-posts", ""); got != "/blog/:slug" {
|
|
t.Fatalf("default = %q", got)
|
|
}
|
|
})
|
|
}
|