Plugins declare site-root path claims (exact or prefix) and a sitemap contribution flag in plugin.mod; the packer validates and stamps them into PluginManifest. Validation rejects reserved prefixes (/admin, /api/plugins, /ws), traversal, duplicates, and prefix claims that cover a reserved prefix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
215 lines
5.8 KiB
Go
215 lines
5.8 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseModFull_PublicRoutesAndSitemap(t *testing.T) {
|
|
src := []byte(`
|
|
[plugin]
|
|
name = "perthplaygrounds"
|
|
version = "1.0.0"
|
|
public_routes = [{ path = "/area", prefix = true }, { path = "/badges" }, { path = "/api/directory", prefix = true }]
|
|
sitemap = true
|
|
`)
|
|
m, err := ParseModFull(src)
|
|
if err != nil {
|
|
t.Fatalf("ParseModFull err: %v", err)
|
|
}
|
|
routes := m.Plugin.PublicRoutes
|
|
if len(routes) != 3 {
|
|
t.Fatalf("PublicRoutes len = %d, want 3 (%v)", len(routes), routes)
|
|
}
|
|
want := []ModPublicRoute{
|
|
{Path: "/area", Prefix: true},
|
|
{Path: "/badges", Prefix: false},
|
|
{Path: "/api/directory", Prefix: true},
|
|
}
|
|
for i, w := range want {
|
|
if routes[i] != w {
|
|
t.Errorf("PublicRoutes[%d] = %+v, want %+v", i, routes[i], w)
|
|
}
|
|
}
|
|
if !m.Plugin.Sitemap {
|
|
t.Errorf("Sitemap = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestParseModFull_PublicRoutesArrayOfTables(t *testing.T) {
|
|
src := []byte(`
|
|
[plugin]
|
|
name = "perthplaygrounds"
|
|
version = "1.0.0"
|
|
|
|
[[plugin.public_routes]]
|
|
path = "/area"
|
|
prefix = true
|
|
|
|
[[plugin.public_routes]]
|
|
path = "/badges"
|
|
`)
|
|
m, err := ParseModFull(src)
|
|
if err != nil {
|
|
t.Fatalf("ParseModFull err: %v", err)
|
|
}
|
|
routes := m.Plugin.PublicRoutes
|
|
if len(routes) != 2 {
|
|
t.Fatalf("PublicRoutes len = %d, want 2 (%v)", len(routes), routes)
|
|
}
|
|
if routes[0] != (ModPublicRoute{Path: "/area", Prefix: true}) {
|
|
t.Errorf("PublicRoutes[0] = %+v", routes[0])
|
|
}
|
|
if routes[1] != (ModPublicRoute{Path: "/badges"}) {
|
|
t.Errorf("PublicRoutes[1] = %+v", routes[1])
|
|
}
|
|
}
|
|
|
|
func TestParseModFull_PublicRoutesOmitted(t *testing.T) {
|
|
src := []byte(`
|
|
[plugin]
|
|
name = "plain"
|
|
version = "0.1.0"
|
|
`)
|
|
m, err := ParseModFull(src)
|
|
if err != nil {
|
|
t.Fatalf("ParseModFull err: %v", err)
|
|
}
|
|
if m.Plugin.PublicRoutes != nil {
|
|
t.Errorf("PublicRoutes = %v, want nil when omitted", m.Plugin.PublicRoutes)
|
|
}
|
|
if m.Plugin.Sitemap {
|
|
t.Errorf("Sitemap = true, want false (absent key)")
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_HappyPath(t *testing.T) {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{
|
|
{Path: "/area", Prefix: true},
|
|
{Path: "/badges", Prefix: true},
|
|
{Path: "/age", Prefix: true},
|
|
{Path: "/api/directory", Prefix: true},
|
|
{Path: "/api/semantic-search"},
|
|
{Path: "/some_page.html"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ValidatePublicRoutes err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_EmptyAndNil(t *testing.T) {
|
|
if err := ValidatePublicRoutes(nil); err != nil {
|
|
t.Errorf("nil err: %v", err)
|
|
}
|
|
if err := ValidatePublicRoutes([]ModPublicRoute{}); err != nil {
|
|
t.Errorf("empty err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_RejectsReservedPrefixes(t *testing.T) {
|
|
cases := []ModPublicRoute{
|
|
{Path: "/admin"},
|
|
{Path: "/admin", Prefix: true},
|
|
{Path: "/admin/settings"},
|
|
{Path: "/api/plugins"},
|
|
{Path: "/api/plugins/foo", Prefix: true},
|
|
{Path: "/ws"},
|
|
}
|
|
for _, c := range cases {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{c})
|
|
if err == nil {
|
|
t.Errorf("ValidatePublicRoutes(%+v) = nil, want reserved-prefix error", c)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), "reserved prefix") {
|
|
t.Errorf("error for %+v does not mention reserved prefix: %v", c, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_RejectsPrefixCoveringReserved(t *testing.T) {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{{Path: "/api", Prefix: true}})
|
|
if err == nil {
|
|
t.Fatal("prefix claim /api should be rejected: it covers /api/plugins")
|
|
}
|
|
if !strings.Contains(err.Error(), "covers reserved prefix /api/plugins") {
|
|
t.Errorf("error does not explain the covered prefix: %v", err)
|
|
}
|
|
if err := ValidatePublicRoutes([]ModPublicRoute{{Path: "/api"}}); err != nil {
|
|
t.Errorf("exact claim /api should be allowed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_RejectsMalformedPaths(t *testing.T) {
|
|
cases := []struct {
|
|
route ModPublicRoute
|
|
frag string
|
|
}{
|
|
{ModPublicRoute{Path: ""}, "empty path"},
|
|
{ModPublicRoute{Path: "area"}, "absolute"},
|
|
{ModPublicRoute{Path: "/"}, "site root"},
|
|
{ModPublicRoute{Path: "/area/"}, "trailing slash"},
|
|
{ModPublicRoute{Path: "/area//list"}, "empty path segment"},
|
|
{ModPublicRoute{Path: "/area/../admin"}, "traversal"},
|
|
{ModPublicRoute{Path: "/area?x=1"}, "characters outside"},
|
|
{ModPublicRoute{Path: "/area list"}, "characters outside"},
|
|
}
|
|
for _, c := range cases {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{c.route})
|
|
if err == nil {
|
|
t.Errorf("ValidatePublicRoutes(%q) = nil, want error", c.route.Path)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), c.frag) {
|
|
t.Errorf("error for %q = %v, want mention of %q", c.route.Path, err, c.frag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_RejectsDuplicates(t *testing.T) {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{
|
|
{Path: "/area"},
|
|
{Path: "/area", Prefix: true},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("duplicate path should be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate path") {
|
|
t.Errorf("error does not mention duplicate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_ReportsAllOffendersInOnePass(t *testing.T) {
|
|
err := ValidatePublicRoutes([]ModPublicRoute{
|
|
{Path: "bad"},
|
|
{Path: "/admin/x"},
|
|
{Path: "/fine"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
for _, frag := range []string{`"bad"`, `"/admin/x"`} {
|
|
if !strings.Contains(err.Error(), frag) {
|
|
t.Errorf("error %q does not mention %s", err.Error(), frag)
|
|
}
|
|
}
|
|
if strings.Contains(err.Error(), `"/fine"`) {
|
|
t.Errorf("error should not name the valid route: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePublicRoutes_CapEnforced(t *testing.T) {
|
|
routes := make([]ModPublicRoute, MaxPublicRoutes+1)
|
|
for i := range routes {
|
|
routes[i] = ModPublicRoute{Path: fmt.Sprintf("/r%d", i)}
|
|
}
|
|
err := ValidatePublicRoutes(routes)
|
|
if err == nil {
|
|
t.Fatal("expected too-many error")
|
|
}
|
|
if !strings.Contains(err.Error(), "too many public_routes") {
|
|
t.Errorf("error %q does not mention the cap", err.Error())
|
|
}
|
|
}
|