- form extensions: placement across three plugins, required plugins' tabs and fields, Hide with neighbours and whole tabs, spans, grouping only of extended forms; every remaining boot error with plugin, controller and offending name - record events: the pure snapshot/diff (create, update, pointers, times as instants, timestamps and key omitted, per-locale translations) and the value helpers - descriptors: MatchPattern edge cases, MatchPath ordering, types without Content, duplicate Model/Controller across plugins
427 lines
19 KiB
Go
427 lines
19 KiB
Go
package signpost_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
)
|
|
|
|
// contentCalls records what the descriptor funcs of a test type received.
|
|
type contentCalls struct {
|
|
describe []pact.ContentDescribe
|
|
list []pact.ContentList
|
|
match []pact.ContentMatch
|
|
}
|
|
|
|
// contentType is a record type whose descriptor answers match for the paths
|
|
// it names, with Exact as given.
|
|
func contentType(key, pattern, model, controller string, calls *contentCalls, match map[string]bool) pact.MenuItemType {
|
|
c := &pact.ContentDescriptor{
|
|
Controller: controller,
|
|
Model: model,
|
|
Describe: func(_ context.Context, in pact.ContentDescribe) (*pact.ContentRecord, error) {
|
|
calls.describe = append(calls.describe, in)
|
|
if in.ID == "missing" {
|
|
return nil, nil
|
|
}
|
|
return &pact.ContentRecord{ID: in.ID, Published: true, Locales: []pact.ContentLocale{{Locale: "en", URL: strings.ReplaceAll(in.Pattern, ":slug", in.ID)}}}, nil
|
|
},
|
|
Records: func(_ context.Context, in pact.ContentList) (pact.ContentPage, error) {
|
|
calls.list = append(calls.list, in)
|
|
return pact.ContentPage{Records: []pact.ContentRecord{{ID: "1"}}, Next: "1"}, nil
|
|
},
|
|
}
|
|
if match != nil {
|
|
c.Match = func(_ context.Context, in pact.ContentMatch) (*pact.ContentMatchResult, error) {
|
|
calls.match = append(calls.match, in)
|
|
if in.Path == "/boom" {
|
|
return nil, errors.New("database down")
|
|
}
|
|
exact, ok := match[in.Path]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &pact.ContentMatchResult{ID: key + "-record", Locale: "en", Exact: exact}, nil
|
|
}
|
|
}
|
|
return pact.MenuItemType{Key: key, DefaultPattern: pattern, Resolve: echo, Content: c}
|
|
}
|
|
|
|
func contentRegistry(t *testing.T, host string, types ...pact.MenuItemType) *signpost.Registry {
|
|
t.Helper()
|
|
app := smokeApp(t, host)
|
|
if err := signpost.Activate(app, []typesPlugin{{id: "acme.blog", types: types}}); err != nil {
|
|
t.Fatalf("Activate: %v", err)
|
|
}
|
|
reg, ok := app.Lookup[*signpost.Registry]()
|
|
if !ok {
|
|
t.Fatal("registry not published")
|
|
}
|
|
return reg
|
|
}
|
|
|
|
func keys(types []pact.MenuItemType) []string {
|
|
out := []string{}
|
|
for _, t := range types {
|
|
out = append(out, t.Key)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestContentLookups: types with a content descriptor are listed in
|
|
// registration order and found by key, controller and model; a type without
|
|
// one is absent and resolves exactly as before; Describe and Records hand
|
|
// the descriptor the layered pattern.
|
|
func TestContentLookups(t *testing.T) {
|
|
posts, tags := &contentCalls{}, &contentCalls{}
|
|
reg := contentRegistry(t, "patterns:\n acme-post: /news/:slug\n",
|
|
pact.MenuItemType{Key: "acme-page", DefaultPattern: "/:slug", Resolve: echo},
|
|
contentType("acme-post", "/blog/:slug", "acme_posts", "acme.blog.posts", posts, nil),
|
|
contentType("acme-tag", "/blog/tag/:slug", "acme_tags", "", tags, nil),
|
|
)
|
|
if got := keys(reg.ContentTypes()); !reflect.DeepEqual(got, []string{"acme-post", "acme-tag"}) {
|
|
t.Fatalf("ContentTypes = %v", got)
|
|
}
|
|
if typ, ok := reg.ContentType("acme-post"); !ok || typ.Key != "acme-post" {
|
|
t.Fatalf("ContentType(acme-post) = %v %v", typ.Key, ok)
|
|
}
|
|
if _, ok := reg.ContentType("acme-page"); ok {
|
|
t.Fatal("a type without Content is a content type")
|
|
}
|
|
if _, ok := reg.ContentType("acme-missing"); ok {
|
|
t.Fatal("an unknown key is a content type")
|
|
}
|
|
if typ, ok := reg.ContentTypeForController("acme.blog.posts"); !ok || typ.Key != "acme-post" {
|
|
t.Fatalf("ContentTypeForController = %v %v", typ.Key, ok)
|
|
}
|
|
if _, ok := reg.ContentTypeForController(""); ok {
|
|
t.Fatal("the empty controller ID names a type")
|
|
}
|
|
if typ, ok := reg.ContentTypeForModel("acme_tags"); !ok || typ.Key != "acme-tag" {
|
|
t.Fatalf("ContentTypeForModel = %v %v", typ.Key, ok)
|
|
}
|
|
if _, ok := reg.ContentTypeForModel("acme_pages"); ok {
|
|
t.Fatal("an unknown model names a type")
|
|
}
|
|
|
|
item, err := reg.Resolve(context.Background(), "acme-page", "", "", false, "")
|
|
if err != nil || item.URL != "/:slug" {
|
|
t.Fatalf("a type without Content resolves to %+v, %v", item, err)
|
|
}
|
|
|
|
rec, err := reg.Describe(context.Background(), "acme-post", "hello", "pl")
|
|
if err != nil || rec == nil || rec.Locales[0].URL != "/news/hello" {
|
|
t.Fatalf("Describe = %+v, %v", rec, err)
|
|
}
|
|
if want := (pact.ContentDescribe{ID: "hello", Pattern: "/news/:slug", UnprefixedLocale: "pl"}); !reflect.DeepEqual(posts.describe, []pact.ContentDescribe{want}) {
|
|
t.Fatalf("Describe received %+v", posts.describe)
|
|
}
|
|
if rec, err := reg.Describe(context.Background(), "acme-post", "missing", ""); err != nil || rec != nil {
|
|
t.Fatalf("Describe of an unknown record = %+v, %v", rec, err)
|
|
}
|
|
for _, key := range []string{"acme-page", "acme-missing"} {
|
|
if _, err := reg.Describe(context.Background(), key, "1", ""); err == nil || !strings.HasPrefix(err.Error(), "signpost: ") {
|
|
t.Fatalf("Describe(%s) error = %v", key, err)
|
|
}
|
|
}
|
|
|
|
page, err := reg.Records(context.Background(), "acme-tag", "7", 50, "en")
|
|
if err != nil || len(page.Records) != 1 || page.Next != "1" {
|
|
t.Fatalf("Records = %+v, %v", page, err)
|
|
}
|
|
if want := (pact.ContentList{Pattern: "/blog/tag/:slug", After: "7", Limit: 50, UnprefixedLocale: "en"}); !reflect.DeepEqual(tags.list, []pact.ContentList{want}) {
|
|
t.Fatalf("Records received %+v", tags.list)
|
|
}
|
|
if _, err := reg.Records(context.Background(), "acme-page", "", 10, ""); err == nil {
|
|
t.Fatal("Records of a type without Content succeeded")
|
|
}
|
|
}
|
|
|
|
// TestContentRecordsOptional: a content type without Records answers an
|
|
// empty page.
|
|
func TestContentRecordsOptional(t *testing.T) {
|
|
typ := contentType("acme-post", "/blog/:slug", "acme_posts", "", &contentCalls{}, nil)
|
|
typ.Content.Records = nil
|
|
reg := contentRegistry(t, "", typ)
|
|
page, err := reg.Records(context.Background(), "acme-post", "", 10, "")
|
|
if err != nil || len(page.Records) != 0 || page.Next != "" {
|
|
t.Fatalf("Records without a func = %+v, %v", page, err)
|
|
}
|
|
}
|
|
|
|
// TestContentBootErrors: a descriptor without Model or Describe, and two
|
|
// types declaring the same Model or Controller, stop the boot naming the
|
|
// type and the plugin.
|
|
func TestContentBootErrors(t *testing.T) {
|
|
calls := &contentCalls{}
|
|
noModel := contentType("acme-post", "/blog/:slug", "", "", calls, nil)
|
|
noDescribe := contentType("acme-post", "/blog/:slug", "acme_posts", "", calls, nil)
|
|
noDescribe.Content.Describe = nil
|
|
for _, tc := range []struct {
|
|
name string
|
|
types []pact.MenuItemType
|
|
want []string
|
|
}{
|
|
{"no Model", []pact.MenuItemType{noModel}, []string{`"acme-post"`, "acme.blog", "Model"}},
|
|
{"no Describe", []pact.MenuItemType{noDescribe}, []string{`"acme-post"`, "acme.blog", "Describe"}},
|
|
{"same Model", []pact.MenuItemType{
|
|
contentType("acme-post", "/a/:slug", "acme_posts", "", calls, nil),
|
|
contentType("acme-article", "/b/:slug", "acme_posts", "", calls, nil),
|
|
}, []string{`"acme-article"`, `"acme-post"`, `"acme_posts"`}},
|
|
{"same Controller", []pact.MenuItemType{
|
|
contentType("acme-post", "/a/:slug", "acme_posts", "acme.blog.posts", calls, nil),
|
|
contentType("acme-article", "/b/:slug", "acme_articles", "acme.blog.posts", calls, nil),
|
|
}, []string{`"acme-article"`, `"acme-post"`, `"acme.blog.posts"`}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := signpost.Activate(smokeApp(t, ""), []typesPlugin{{id: "acme.blog", types: tc.types}})
|
|
if err == nil {
|
|
t.Fatal("Activate accepted the descriptor")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "signpost: ") {
|
|
t.Fatalf("error = %q, want the signpost prefix", err)
|
|
}
|
|
for _, part := range tc.want {
|
|
if !strings.Contains(err.Error(), part) {
|
|
t.Fatalf("error = %q, want %q", err, part)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
// Two types without a controller ID do not clash.
|
|
contentRegistry(t, "",
|
|
contentType("acme-post", "/a/:slug", "acme_posts", "", calls, nil),
|
|
contentType("acme-tag", "/b/:slug", "acme_tags", "", calls, nil),
|
|
)
|
|
}
|
|
|
|
// TestContentMatchPattern: whole-segment placeholders read their values; a
|
|
// literal segment, the segment count and an empty value must all match.
|
|
func TestContentMatchPattern(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
pattern, path string
|
|
want map[string]string
|
|
ok bool
|
|
}{
|
|
{"/blog/:slug", "/blog/hello", map[string]string{"slug": "hello"}, true},
|
|
{"/:category/:slug", "/news/hello", map[string]string{"category": "news", "slug": "hello"}, true},
|
|
{"/blog/:year/:month/:slug", "/blog/2026/10/hello", map[string]string{"year": "2026", "month": "10", "slug": "hello"}, true},
|
|
{"/", "/", map[string]string{}, true},
|
|
{"/blog", "/blog", map[string]string{}, true},
|
|
{"/blog/:slug", "/blog/hello/extra", nil, false},
|
|
{"/blog/:slug", "/blog", nil, false},
|
|
{"/blog/:slug", "/blog/", nil, false},
|
|
{"/blog/:slug", "/news/hello", nil, false},
|
|
{"/:category/:slug", "//hello", nil, false},
|
|
{"/blog/:slug", "blog/hello", nil, false},
|
|
{"/:slug/:slug", "/a/b", nil, false},
|
|
{"/:slug/:slug", "/a/a", map[string]string{"slug": "a"}, true},
|
|
} {
|
|
got, ok := signpost.MatchPattern(tc.pattern, tc.path)
|
|
if ok != tc.ok || (tc.ok && !reflect.DeepEqual(got, tc.want)) {
|
|
t.Fatalf("MatchPattern(%q, %q) = %v %v, want %v %v", tc.pattern, tc.path, got, ok, tc.want, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestContentMatchPath: MatchPath asks the content types with Match in
|
|
// registration order with their layered pattern; the first exact match
|
|
// wins, else the first non-exact one, else none; a Match error is returned.
|
|
func TestContentMatchPath(t *testing.T) {
|
|
first, second := &contentCalls{}, &contentCalls{}
|
|
reg := contentRegistry(t, "patterns:\n acme-page: /p/:slug\n",
|
|
contentType("acme-post", "/blog/:slug", "acme_posts", "", first, map[string]bool{"/shared": false, "/post-only": false, "/both-exact": true}),
|
|
contentType("acme-plain", "/plain/:slug", "acme_plain", "", &contentCalls{}, nil),
|
|
contentType("acme-page", "/:slug", "acme_pages", "", second, map[string]bool{"/shared": true, "/both-exact": true}),
|
|
)
|
|
ctx := context.Background()
|
|
key, res, err := reg.MatchPath(ctx, "/shared", "pl")
|
|
if err != nil || key != "acme-page" || res == nil || !res.Exact || res.ID != "acme-page-record" {
|
|
t.Fatalf("exact match = %q %+v %v, want acme-page exact", key, res, err)
|
|
}
|
|
if len(first.match) != 1 || first.match[0] != (pact.ContentMatch{Path: "/shared", Pattern: "/blog/:slug", UnprefixedLocale: "pl"}) {
|
|
t.Fatalf("first type received %+v", first.match)
|
|
}
|
|
if len(second.match) != 1 || second.match[0].Pattern != "/p/:slug" {
|
|
t.Fatalf("second type received %+v, want the host pattern", second.match)
|
|
}
|
|
if key, res, err := reg.MatchPath(ctx, "/both-exact", ""); err != nil || key != "acme-post" || !res.Exact {
|
|
t.Fatalf("two exact matches = %q %+v %v, want the first registered", key, res, err)
|
|
}
|
|
if key, res, err := reg.MatchPath(ctx, "/post-only", ""); err != nil || key != "acme-post" || res == nil || res.Exact {
|
|
t.Fatalf("non-exact match = %q %+v %v", key, res, err)
|
|
}
|
|
if key, res, err := reg.MatchPath(ctx, "/nothing", ""); err != nil || key != "" || res != nil {
|
|
t.Fatalf("no match = %q %+v %v", key, res, err)
|
|
}
|
|
if _, _, err := reg.MatchPath(ctx, "/boom", ""); err == nil || !strings.HasPrefix(err.Error(), "signpost: ") || !strings.Contains(err.Error(), "acme-post") {
|
|
t.Fatalf("Match error = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestContentMatchPatternEdgeCases: trailing slashes, encoded and empty
|
|
// segments, case and segment counts. Segments are compared as given; the
|
|
// caller decodes the path and strips the query.
|
|
func TestContentMatchPatternEdgeCases(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, pattern, path string
|
|
want map[string]string
|
|
ok bool
|
|
}{
|
|
{"a trailing slash on the path is an extra empty segment", "/blog/:slug", "/blog/hello/", nil, false},
|
|
{"a trailing slash on both matches", "/blog/:slug/", "/blog/hello/", map[string]string{"slug": "hello"}, true},
|
|
{"a trailing slash on the pattern only", "/blog/:slug/", "/blog/hello", nil, false},
|
|
{"an encoded space stays encoded", "/blog/:slug", "/blog/hello%20world", map[string]string{"slug": "hello%20world"}, true},
|
|
{"an encoded slash is one segment", "/blog/:slug", "/blog/a%2Fb", map[string]string{"slug": "a%2Fb"}, true},
|
|
{"a decoded slash is two segments", "/blog/:slug", "/blog/a/b", nil, false},
|
|
{"literal segments are case-sensitive", "/blog/:slug", "/Blog/hello", nil, false},
|
|
{"an empty segment in the middle", "/blog/:a/:b", "/blog//hello", nil, false},
|
|
{"a missing segment", "/blog/:a/:b", "/blog/x", nil, false},
|
|
{"an extra segment", "/blog/:a", "/blog/x/y", nil, false},
|
|
{"a pattern without a leading slash", "blog/:slug", "/blog/x", nil, false},
|
|
{"an empty path", "/blog/:slug", "", nil, false},
|
|
{"a lone colon is a literal segment", "/a/:", "/a/:", map[string]string{}, true},
|
|
{"a lone colon does not match a value", "/a/:", "/a/x", nil, false},
|
|
{"the root does not match a segment", "/", "/x", nil, false},
|
|
{"a placeholder matches a unicode segment", "/:slug", "/grzyby-leśne", map[string]string{"slug": "grzyby-leśne"}, true},
|
|
{"three different placeholders", "/:a/:b/:c", "/1/2/3", map[string]string{"a": "1", "b": "2", "c": "3"}, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok := signpost.MatchPattern(tc.pattern, tc.path)
|
|
if ok != tc.ok || (tc.ok && !reflect.DeepEqual(got, tc.want)) {
|
|
t.Fatalf("MatchPattern(%q, %q) = %v %v, want %v %v", tc.pattern, tc.path, got, ok, tc.want, tc.ok)
|
|
}
|
|
if !ok && got != nil {
|
|
t.Fatalf("a failed match returned values %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestContentMatchPathOrdering: an exact match of a later type beats a
|
|
// non-exact match of an earlier one, the first of two non-exact matches
|
|
// wins, types without Match are never asked, and a nil registry or context
|
|
// is safe.
|
|
func TestContentMatchPathOrdering(t *testing.T) {
|
|
first, second, third := &contentCalls{}, &contentCalls{}, &contentCalls{}
|
|
plain := contentType("acme-plain", "/plain/:slug", "acme_plain", "", &contentCalls{}, nil)
|
|
reg := contentRegistry(t, "",
|
|
contentType("acme-post", "/blog/:slug", "acme_posts", "", first, map[string]bool{"/x": false, "/y": false}),
|
|
plain,
|
|
contentType("acme-page", "/:slug", "acme_pages", "", second, map[string]bool{"/x": true, "/y": false}),
|
|
contentType("acme-tag", "/tag/:slug", "acme_tags", "", third, map[string]bool{"/x": true}),
|
|
)
|
|
ctx := context.Background()
|
|
if key, res, err := reg.MatchPath(ctx, "/x", ""); err != nil || key != "acme-page" || !res.Exact {
|
|
t.Fatalf("/x = %q %+v %v, want the later exact acme-page", key, res, err)
|
|
}
|
|
if len(third.match) != 0 {
|
|
t.Fatalf("the search went on after an exact match: %+v", third.match)
|
|
}
|
|
if key, res, err := reg.MatchPath(ctx, "/y", ""); err != nil || key != "acme-post" || res.Exact {
|
|
t.Fatalf("/y = %q %+v %v, want the first non-exact acme-post", key, res, err)
|
|
}
|
|
if len(third.match) != 1 {
|
|
t.Fatalf("without an exact match every type is asked: %+v", third.match)
|
|
}
|
|
//nolint:staticcheck // a nil context is part of the contract
|
|
if key, _, err := reg.MatchPath(nil, "/x", ""); err != nil || key != "acme-page" {
|
|
t.Fatalf("nil context: %q %v", key, err)
|
|
}
|
|
var none *signpost.Registry
|
|
if key, res, err := none.MatchPath(ctx, "/x", ""); key != "" || res != nil || err != nil {
|
|
t.Fatalf("nil registry = %q %+v %v", key, res, err)
|
|
}
|
|
if got := none.ContentTypes(); got == nil || len(got) != 0 {
|
|
t.Fatalf("nil registry ContentTypes = %#v", got)
|
|
}
|
|
if _, ok := none.ContentTypeForModel("acme_posts"); ok {
|
|
t.Fatal("a nil registry names a model")
|
|
}
|
|
if _, ok := none.ContentTypeForController("acme.blog.posts"); ok {
|
|
t.Fatal("a nil registry names a controller")
|
|
}
|
|
}
|
|
|
|
// TestContentTypesWithoutContent: types without a content descriptor keep
|
|
// resolving as before and stay out of every content lookup.
|
|
func TestContentTypesWithoutContent(t *testing.T) {
|
|
reg := contentRegistry(t, "patterns:\n acme-page: /p/:slug\n",
|
|
pact.MenuItemType{Key: "acme-page", DefaultPattern: "/:slug", Resolve: echo},
|
|
pact.MenuItemType{Key: "acme-home", DefaultPattern: "/", Resolve: echo},
|
|
)
|
|
ctx := context.Background()
|
|
if got := reg.ContentTypes(); len(got) != 0 {
|
|
t.Fatalf("ContentTypes = %v", keys(got))
|
|
}
|
|
if item, err := reg.Resolve(ctx, "acme-page", "", "", false, ""); err != nil || item.URL != "/p/:slug" {
|
|
t.Fatalf("Resolve = %+v %v, want the layered pattern", item, err)
|
|
}
|
|
if _, err := reg.Describe(ctx, "acme-page", "1", ""); err == nil || !strings.Contains(err.Error(), "no content descriptor") {
|
|
t.Fatalf("Describe = %v", err)
|
|
}
|
|
if _, err := reg.Describe(ctx, "acme-missing", "1", ""); err == nil || !strings.Contains(err.Error(), "unknown menu item type") {
|
|
t.Fatalf("Describe of an unknown key = %v", err)
|
|
}
|
|
if _, err := reg.Records(ctx, "acme-missing", "", 10, ""); err == nil {
|
|
t.Fatal("Records of an unknown key succeeded")
|
|
}
|
|
if key, res, err := reg.MatchPath(ctx, "/anything", ""); key != "" || res != nil || err != nil {
|
|
t.Fatalf("MatchPath = %q %+v %v", key, res, err)
|
|
}
|
|
|
|
mixed := contentRegistry(t, "",
|
|
pact.MenuItemType{Key: "acme-page", DefaultPattern: "/:slug", Resolve: echo},
|
|
contentType("acme-post", "/blog/:slug", "acme_posts", "acme.blog.posts", &contentCalls{}, nil),
|
|
)
|
|
got := mixed.ContentTypes()
|
|
got[0].Key = "changed"
|
|
if again := keys(mixed.ContentTypes()); !reflect.DeepEqual(again, []string{"acme-post"}) {
|
|
t.Fatalf("ContentTypes is not a copy: %v", again)
|
|
}
|
|
if typ, ok := mixed.Type("acme-page"); !ok || typ.Content != nil {
|
|
t.Fatalf("acme-page = %+v %v", typ, ok)
|
|
}
|
|
}
|
|
|
|
// TestContentBootErrorsAcrossPlugins: duplicate Model and Controller
|
|
// declarations are refused across plugins too and name both plugins; a
|
|
// blank Model is a missing one.
|
|
func TestContentBootErrorsAcrossPlugins(t *testing.T) {
|
|
calls := &contentCalls{}
|
|
for _, tc := range []struct {
|
|
name string
|
|
plugins []typesPlugin
|
|
want []string
|
|
}{
|
|
{"same Model in two plugins", []typesPlugin{
|
|
{id: "acme.blog", types: []pact.MenuItemType{contentType("acme-post", "/a/:slug", "acme_posts", "", calls, nil)}},
|
|
{id: "acme.news", types: []pact.MenuItemType{contentType("acme-article", "/b/:slug", "acme_posts", "", calls, nil)}},
|
|
}, []string{`"acme-article"`, "acme.news", `"acme-post"`, "acme.blog", `"acme_posts"`}},
|
|
{"same Controller in two plugins", []typesPlugin{
|
|
{id: "acme.blog", types: []pact.MenuItemType{contentType("acme-post", "/a/:slug", "acme_posts", "acme.blog.posts", calls, nil)}},
|
|
{id: "acme.news", types: []pact.MenuItemType{contentType("acme-article", "/b/:slug", "acme_articles", "acme.blog.posts", calls, nil)}},
|
|
}, []string{`"acme-article"`, "acme.news", `"acme-post"`, "acme.blog", `"acme.blog.posts"`}},
|
|
{"a blank Model", []typesPlugin{
|
|
{id: "acme.blog", types: []pact.MenuItemType{contentType("acme-post", "/a/:slug", " ", "", calls, nil)}},
|
|
}, []string{`"acme-post"`, "acme.blog", "without Model"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := signpost.Activate(smokeApp(t, ""), tc.plugins)
|
|
if err == nil {
|
|
t.Fatal("Activate accepted the descriptors")
|
|
}
|
|
for _, part := range tc.want {
|
|
if !strings.Contains(err.Error(), part) {
|
|
t.Fatalf("error = %q, want %q", err, part)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|