package surf import ( "io/fs" "net/http" "os" "path/filepath" "strings" "testing" "testing/fstest" "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/party" ) // TestPhase10AdminPrefixCollision fails boot when a plugin other than cabana // registers a route at or under the admin prefix (backend.uri), which would // otherwise shadow or be shadowed by the admin SPA and API. func TestPhase10AdminPrefixCollision(t *testing.T) { for _, tc := range []struct { name string method string path string raw bool }{ {"exact prefix", http.MethodGet, "/acme-admin", false}, {"under prefix", http.MethodPost, "/acme-admin/hook", false}, {"raw under api", http.MethodGet, "/acme-admin/api/v1/extra", true}, {"deeper path", http.MethodGet, "/acme-admin/a/b/c", false}, {"exact prefix raw", http.MethodPost, "/acme-admin", true}, } { t.Run(tc.name, func(t *testing.T) { app := adminPrefixApp(t) plugins := []party.Plugin{ adminPrefixPlugin{}, prefixRoutePlugin{id: "acme.intruder", method: tc.method, path: tc.path, raw: tc.raw}, } _, err := BuildRouter(app, plugins) if err == nil { t.Fatalf("BuildRouter accepted %s %s under the admin prefix", tc.method, tc.path) } for _, want := range []string{tc.method, tc.path, "acme.intruder", "/acme-admin"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error %q does not name %q", err, want) } } }) } t.Run("sibling path is allowed", func(t *testing.T) { app := adminPrefixApp(t) plugins := []party.Plugin{ adminPrefixPlugin{}, prefixRoutePlugin{id: "acme.neighbour", method: http.MethodGet, path: "/acme-adminx"}, } if _, err := BuildRouter(app, plugins); err != nil { t.Fatalf("sibling path rejected: %v", err) } }) t.Run("default /backend prefix", func(t *testing.T) { for _, tc := range []struct { path string collide bool }{ {"/backend", true}, {"/backend/deep/hook", true}, {"/backendx", false}, {"/back", false}, {"/api/backend", false}, } { app := adminPrefixAppWith(t, "") plugins := []party.Plugin{ adminPrefixPlugin{}, prefixRoutePlugin{id: "acme.intruder", method: http.MethodGet, path: tc.path}, } _, err := BuildRouter(app, plugins) if tc.collide && (err == nil || !strings.Contains(err.Error(), "/backend")) { t.Fatalf("%s under the default prefix: err=%v", tc.path, err) } if !tc.collide && err != nil { t.Fatalf("%s rejected next to the default prefix: %v", tc.path, err) } } }) t.Run("no admin means no prefix check", func(t *testing.T) { app := adminPrefixApp(t) plugins := []party.Plugin{prefixRoutePlugin{id: "acme.site", method: http.MethodGet, path: "/acme-admin"}} if _, err := BuildRouter(app, plugins); err != nil { t.Fatalf("a route at the prefix without admin controllers was rejected: %v", err) } if err := (&Router{}).checkAdminPrefix(""); err != nil { t.Fatalf("empty prefix: %v", err) } }) } func adminPrefixApp(t *testing.T) *backpack.App { t.Helper() return adminPrefixAppWith(t, "/acme-admin") } // adminPrefixAppWith configures backend.uri; an empty uri keeps the default. func adminPrefixAppWith(t *testing.T, uri string) *backpack.App { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: admin-prefix\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "http.yaml"), []byte("body_limits:\n default_bytes: 1024\n upload_bytes: 1024\n"), 0o644); err != nil { t.Fatal(err) } if uri != "" { if err := os.WriteFile(filepath.Join(dir, "backend.yaml"), []byte("uri: "+uri+"\n"), 0o644); err != nil { t.Fatal(err) } } cfg, err := compass.Open(compass.Options{ Dir: dir, Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=summercms-test-only-admin-hs256-secret"}, }) if err != nil { t.Fatal(err) } return backpack.New(cfg) } type adminPrefixPlugin struct{} func (adminPrefixPlugin) ID() string { return "acme.demo" } func (adminPrefixPlugin) Requires() []string { return nil } func (adminPrefixPlugin) Register(*backpack.App) error { return nil } func (adminPrefixPlugin) Boot(*backpack.App) error { return nil } func (adminPrefixPlugin) AdminControllers() []pact.AdminController { return []pact.AdminController{adminPrefixController{}} } func (adminPrefixPlugin) AdminFS() fs.FS { return fstest.MapFS{ "controllers/widgets/config_list.yaml": &fstest.MapFile{Data: []byte("list: ~/plugins/acme/demo/models/widget/columns.yaml\nmodelClass: Widget\nrecordsPerPage: 20\n")}, "models/widget/columns.yaml": &fstest.MapFile{Data: []byte("columns:\n name:\n label: Name\n")}, } } type adminPrefixController struct{} func (adminPrefixController) ID() string { return "acme.demo.widgets" } func (adminPrefixController) ModelName() string { return "Widget" } func (adminPrefixController) ConfigDir() string { return "controllers/widgets" } type prefixRoutePlugin struct { id string method string path string raw bool } func (p prefixRoutePlugin) ID() string { return p.id } func (p prefixRoutePlugin) Requires() []string { return nil } func (p prefixRoutePlugin) Register(*backpack.App) error { return nil } func (p prefixRoutePlugin) Boot(*backpack.App) error { return nil } func (p prefixRoutePlugin) Routes(r pact.Router) error { h := func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) } open := r.Group if p.raw { open = r.GroupRaw } open("", nil, func(g pact.Router) { switch p.method { case http.MethodPost: g.Post(p.path, h) default: g.Get(p.path, h) } }) return nil }