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