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) } }) }