test(cabana): Plugins screen endpoints
- TestPluginsEndpoints: 401/403/200 for both endpoints, the update rule table, cache hit and expiry on an injected clock, failure caching, at most 4 version lookups at once, call timeout, marketplace validation, markers, https-only icons, disabled registry making no call - fix: marketplace categories come only from plugins the screen offers, not from registry entries dropped for an invalid ID - check-phase34.sh: TestPluginsEndpoints and useSettings.test.ts required
This commit is contained in:
@@ -456,11 +456,15 @@ func (s *service) pluginsMarketplace(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// categoriesOf returns the distinct valid categories of plugins, sorted.
|
||||
// categoriesOf returns the distinct valid categories of the plugins the
|
||||
// marketplace offers (valid IDs only), sorted.
|
||||
func categoriesOf(plugins []kiosk.RegistryPlugin) []string {
|
||||
seen := map[string]bool{}
|
||||
out := []string{}
|
||||
for _, p := range plugins {
|
||||
if !marketplaceIDRe.MatchString(p.ID) {
|
||||
continue
|
||||
}
|
||||
if p.Category != "" && marketplaceCategoryRe.MatchString(p.Category) && !seen[p.Category] {
|
||||
seen[p.Category] = true
|
||||
out = append(out, p.Category)
|
||||
|
||||
473
modules/cabana/plugins_test.go
Normal file
473
modules/cabana/plugins_test.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/kiosk"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// memPluginRegistry is an in-process pluginRegistry: versions per ID,
|
||||
// a list, call counters, an in-flight gauge and a failure switch.
|
||||
type memPluginRegistry struct {
|
||||
mu sync.Mutex
|
||||
versions map[string][]kiosk.RegistryVersion
|
||||
list []kiosk.RegistryPlugin
|
||||
fail error
|
||||
hold time.Duration
|
||||
calls []string
|
||||
inFlight int
|
||||
maxFlight int
|
||||
}
|
||||
|
||||
func (m *memPluginRegistry) List(_ context.Context, q, category string) (kiosk.ListResponse, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.calls = append(m.calls, "list "+q+"|"+category)
|
||||
if m.fail != nil {
|
||||
return kiosk.ListResponse{}, m.fail
|
||||
}
|
||||
var out []kiosk.RegistryPlugin
|
||||
for _, p := range m.list {
|
||||
if (category == "" || p.Category == category) && (q == "" || strings.Contains(p.ID, q)) {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return kiosk.ListResponse{Data: out, Meta: kiosk.ListMeta{Total: len(out)}}, nil
|
||||
}
|
||||
|
||||
func (m *memPluginRegistry) Versions(ctx context.Context, id string) ([]kiosk.RegistryVersion, error) {
|
||||
m.mu.Lock()
|
||||
m.calls = append(m.calls, "versions "+id)
|
||||
m.inFlight++
|
||||
if m.inFlight > m.maxFlight {
|
||||
m.maxFlight = m.inFlight
|
||||
}
|
||||
hold, fail := m.hold, m.fail
|
||||
v, ok := m.versions[id]
|
||||
m.mu.Unlock()
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
m.inFlight--
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
if hold > 0 {
|
||||
select {
|
||||
case <-time.After(hold):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
if fail != nil {
|
||||
return nil, fail
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is %w at mem", id, kiosk.ErrNotFound)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (m *memPluginRegistry) count(prefix string) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
n := 0
|
||||
for _, c := range m.calls {
|
||||
if strings.HasPrefix(c, prefix) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// testClock is a settable clock for the catalog's cache.
|
||||
type testClock struct {
|
||||
mu sync.Mutex
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (c *testClock) Now() time.Time {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.now
|
||||
}
|
||||
|
||||
func (c *testClock) advance(d time.Duration) {
|
||||
c.mu.Lock()
|
||||
c.now = c.now.Add(d)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// unitPluginsService is pluginsService with its catalog replaced by one
|
||||
// over reg (nil: disabled) and clock.
|
||||
func unitPluginsService(t *testing.T, installed *party.Installed, reg *memPluginRegistry, clock *testClock) *service {
|
||||
t.Helper()
|
||||
svc := pluginsService(t, pluginsConfig(t, ""), installed, nil)
|
||||
catalog := &pluginCatalog{
|
||||
timeout: time.Second,
|
||||
now: clock.Now,
|
||||
lists: map[string]cachedList{},
|
||||
versions: map[string]cachedVersions{},
|
||||
}
|
||||
if reg == nil {
|
||||
catalog.disabled = true
|
||||
} else {
|
||||
catalog.registry = reg
|
||||
}
|
||||
svc.catalog = catalog
|
||||
return svc
|
||||
}
|
||||
|
||||
func rel(version, framework string) kiosk.RegistryVersion {
|
||||
return kiosk.RegistryVersion{Version: version, Framework: framework}
|
||||
}
|
||||
|
||||
func byPluginID(overview PluginsOverview) map[string]InstalledPlugin {
|
||||
out := map[string]InstalledPlugin{}
|
||||
for _, p := range overview.Plugins {
|
||||
out[p.ID] = p
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestPluginsEndpoints covers the Plugins screen endpoints at unit level:
|
||||
// access, the settings entry, the update rule, the registry cache and
|
||||
// concurrency bound, and the marketplace query rules.
|
||||
func TestPluginsEndpoints(t *testing.T) {
|
||||
clock := &testClock{now: time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)}
|
||||
|
||||
t.Run("access", func(t *testing.T) {
|
||||
svc := unitPluginsService(t, testInstalled(), nil, clock)
|
||||
notBackend := &bouncer.Principal{ID: 9, Backend: false, IsSuperuser: true}
|
||||
for _, target := range []string{"/plugins", "/plugins/marketplace"} {
|
||||
handler := (*service).pluginsInstalled
|
||||
if target != "/plugins" {
|
||||
handler = (*service).pluginsMarketplace
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
principal *bouncer.Principal
|
||||
want int
|
||||
}{
|
||||
{"anonymous", nil, http.StatusUnauthorized},
|
||||
{"frontend principal", notBackend, http.StatusUnauthorized},
|
||||
{"no permission", pluginsDenied, http.StatusForbidden},
|
||||
{"permission", pluginsViewer, http.StatusOK},
|
||||
{"superuser", pluginsSuperuser, http.StatusOK},
|
||||
} {
|
||||
rec := pluginsCall(svc, handler, c.principal, target)
|
||||
if rec.Code != c.want {
|
||||
t.Errorf("%s %s: status %d, want %d (%s)", target, c.name, rec.Code, c.want, rec.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("settings entry", func(t *testing.T) {
|
||||
entry := pluginsSettingsEntry("Plugins", "Installed plugins", "system")
|
||||
if entry.Path != "/system/plugins" || entry.Code != "plugins" || entry.Controller != "" || entry.Keywords == nil {
|
||||
t.Errorf("entry = %+v", entry)
|
||||
}
|
||||
raw, _ := json.Marshal(SettingsEntry{Code: "other"})
|
||||
if strings.Contains(string(raw), `"path"`) {
|
||||
t.Errorf("an entry without a path serializes one: %s", raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update rule", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, installed, framework string
|
||||
versions []kiosk.RegistryVersion
|
||||
latest, latestFramework string
|
||||
update bool
|
||||
}{
|
||||
{"exact tag, newer compatible", "v0.1.0", "v0.6.0", []kiosk.RegistryVersion{rel("v0.2.0", "v0.6.0"), rel("v0.1.0", "v0.5.0")}, "v0.2.0", "", true},
|
||||
{"exact tag, current", "v0.2.0", "v0.6.0", []kiosk.RegistryVersion{rel("v0.2.0", "v0.6.0")}, "v0.2.0", "", false},
|
||||
{"describe base equal to latest", "v0.1.1-3-gabc1234", "v0.6.0", []kiosk.RegistryVersion{rel("v0.1.1", "v0.6.0")}, "v0.1.1", "", false},
|
||||
{"describe base below latest", "v0.1.1-3-gabc1234", "v0.6.0", []kiosk.RegistryVersion{rel("v0.1.2", "v0.6.0"), rel("v0.1.1", "v0.6.0")}, "v0.1.2", "", true},
|
||||
{"untagged commit", "abc1234", "v0.6.0", []kiosk.RegistryVersion{rel("v0.1.2", "v0.6.0")}, "v0.1.2", "", false},
|
||||
{"newer incompatible only", "v0.1.0", "v0.6.0", []kiosk.RegistryVersion{rel("v0.3.0", "v0.7.0"), rel("v0.1.0", "v0.5.0")}, "v0.1.0", "v0.7.0", false},
|
||||
{"compatible update and newer incompatible", "v0.1.0", "v0.6.0", []kiosk.RegistryVersion{rel("v0.3.0", "v0.8.0"), rel("v0.2.0", "v0.6.0")}, "v0.2.0", "v0.8.0", true},
|
||||
{"incompatible but not newer than installed", "v0.3.0", "v0.6.0", []kiosk.RegistryVersion{rel("v0.3.0", "v0.7.0")}, "", "", false},
|
||||
{"framework is a describe string", "v0.1.0", "v0.6.0-4-gdeadbee", []kiosk.RegistryVersion{rel("v0.2.0", "v0.6.0")}, "v0.2.0", "", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
p := InstalledPlugin{ID: "acme.blog", Version: c.installed}
|
||||
applyUpdate(&p, c.versions, frameworkRelease(c.framework))
|
||||
wantCmd := ""
|
||||
if c.update {
|
||||
wantCmd = "summer plugin:update acme.blog"
|
||||
}
|
||||
if !p.Listed || p.Latest != c.latest || p.LatestFramework != c.latestFramework || p.UpdateAvailable != c.update || p.UpdateCommand != wantCmd {
|
||||
t.Errorf("%s: %+v", c.name, p)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update badges through the endpoint", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{versions: map[string][]kiosk.RegistryVersion{
|
||||
"acme.blog": {rel("v0.2.0", "v0.6.0"), rel("v0.1.0", "v0.5.0")},
|
||||
"acme.translate": {rel("v0.1.1", "v0.6.0")},
|
||||
}}
|
||||
installed := testInstalled()
|
||||
installed.Plugins = append(installed.Plugins, party.Info{ID: "acme.unlisted", Version: "v1.0.0", Source: party.SourceModule})
|
||||
svc := unitPluginsService(t, installed, reg, clock)
|
||||
overview := decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsViewer, "/plugins"))
|
||||
if overview.Registry != (RegistryState{Enabled: true, Status: registryStatusOK}) {
|
||||
t.Errorf("registry = %+v", overview.Registry)
|
||||
}
|
||||
got := byPluginID(overview)
|
||||
if b := got["acme.blog"]; !b.UpdateAvailable || b.Latest != "v0.2.0" || b.UpdateCommand != "summer plugin:update acme.blog" {
|
||||
t.Errorf("blog = %+v", b)
|
||||
}
|
||||
if tr := got["acme.translate"]; tr.UpdateAvailable || tr.Latest != "v0.1.1" || !tr.Listed {
|
||||
t.Errorf("translate = %+v", tr)
|
||||
}
|
||||
if u := got["acme.unlisted"]; u.Listed || u.UpdateAvailable || u.Latest != "" {
|
||||
t.Errorf("unlisted = %+v", u)
|
||||
}
|
||||
if l := got["acme.local"]; l.Listed || l.UpdateAvailable {
|
||||
t.Errorf("local = %+v", l)
|
||||
}
|
||||
if n := reg.count("versions acme.local"); n != 0 {
|
||||
t.Errorf("the local plugin was looked up %d times", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("registry error keeps the list", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{fail: errors.New("connection refused")}
|
||||
svc := unitPluginsService(t, testInstalled(), reg, clock)
|
||||
overview := decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsSuperuser, "/plugins"))
|
||||
if overview.Registry != (RegistryState{Enabled: true, Status: registryStatusUnavailable}) || len(overview.Plugins) != 3 {
|
||||
t.Errorf("overview = %+v", overview)
|
||||
}
|
||||
for _, p := range overview.Plugins {
|
||||
if p.UpdateAvailable || p.Listed {
|
||||
t.Errorf("plugin with a failed lookup = %+v", p)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cache hit and expiry", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{versions: map[string][]kiosk.RegistryVersion{"acme.blog": {rel("v0.2.0", "v0.6.0")}, "acme.translate": {rel("v0.1.1", "v0.6.0")}}}
|
||||
clk := &testClock{now: clock.Now()}
|
||||
svc := unitPluginsService(t, testInstalled(), reg, clk)
|
||||
call := func() { decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsSuperuser, "/plugins")) }
|
||||
call()
|
||||
call()
|
||||
clk.advance(versionsTTL - time.Second)
|
||||
call()
|
||||
if n := reg.count("versions acme.blog"); n != 1 {
|
||||
t.Fatalf("lookups within the TTL = %d, want 1", n)
|
||||
}
|
||||
clk.advance(2 * time.Second)
|
||||
call()
|
||||
if n := reg.count("versions acme.blog"); n != 2 {
|
||||
t.Fatalf("lookups after the TTL = %d, want 2", n)
|
||||
}
|
||||
|
||||
// A failure is cached for registryFailureTTL only.
|
||||
reg.mu.Lock()
|
||||
reg.fail = errors.New("down")
|
||||
reg.mu.Unlock()
|
||||
clk.advance(versionsTTL + time.Second)
|
||||
call()
|
||||
before := reg.count("versions acme.blog")
|
||||
clk.advance(registryFailureTTL / 2)
|
||||
call()
|
||||
if n := reg.count("versions acme.blog"); n != before {
|
||||
t.Fatalf("a cached failure was retried within %s", registryFailureTTL)
|
||||
}
|
||||
clk.advance(registryFailureTTL)
|
||||
call()
|
||||
if n := reg.count("versions acme.blog"); n != before+1 {
|
||||
t.Fatalf("a failure was not retried after %s", registryFailureTTL)
|
||||
}
|
||||
|
||||
// Searches are cached per query and category.
|
||||
reg.mu.Lock()
|
||||
reg.fail = nil
|
||||
reg.mu.Unlock()
|
||||
for i := 0; i < 3; i++ {
|
||||
pluginsCall(svc, (*service).pluginsMarketplace, pluginsSuperuser, "/plugins/marketplace?q=blog")
|
||||
}
|
||||
if n := reg.count("list blog|"); n != 1 {
|
||||
t.Fatalf("search lookups = %d, want 1", n)
|
||||
}
|
||||
clk.advance(marketplaceTTL + time.Second)
|
||||
pluginsCall(svc, (*service).pluginsMarketplace, pluginsSuperuser, "/plugins/marketplace?q=blog")
|
||||
if n := reg.count("list blog|"); n != 2 {
|
||||
t.Fatalf("search lookups after the TTL = %d, want 2", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrency bound", func(t *testing.T) {
|
||||
installed := &party.Installed{Framework: "v0.6.0"}
|
||||
reg := &memPluginRegistry{versions: map[string][]kiosk.RegistryVersion{}, hold: 30 * time.Millisecond}
|
||||
for i := 0; i < 12; i++ {
|
||||
id := fmt.Sprintf("acme.p%d", i)
|
||||
installed.Plugins = append(installed.Plugins, party.Info{ID: id, Version: "v0.1.0", Source: party.SourceModule})
|
||||
reg.versions[id] = []kiosk.RegistryVersion{rel("v0.1.0", "v0.6.0")}
|
||||
}
|
||||
svc := unitPluginsService(t, installed, reg, clock)
|
||||
overview := decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsSuperuser, "/plugins"))
|
||||
if overview.Registry.Status != registryStatusOK || reg.count("versions ") != 12 {
|
||||
t.Fatalf("registry %+v, lookups %d", overview.Registry, reg.count("versions "))
|
||||
}
|
||||
if reg.maxFlight > registryLookups || reg.maxFlight < 2 {
|
||||
t.Errorf("at most %d lookups ran at once, want between 2 and %d", reg.maxFlight, registryLookups)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("slow registry times out", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{versions: map[string][]kiosk.RegistryVersion{"acme.blog": {rel("v0.2.0", "v0.6.0")}}, hold: time.Second}
|
||||
svc := unitPluginsService(t, testInstalled(), reg, clock)
|
||||
svc.catalog.timeout = 20 * time.Millisecond
|
||||
start := time.Now()
|
||||
overview := decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsSuperuser, "/plugins"))
|
||||
if overview.Registry.Status != registryStatusUnavailable || time.Since(start) > 500*time.Millisecond {
|
||||
t.Errorf("registry %+v after %s", overview.Registry, time.Since(start))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("marketplace", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{list: []kiosk.RegistryPlugin{
|
||||
{ID: "acme.blog", Name: "Blog", Category: "content", LatestVersion: "v0.2.0", LatestFramework: "v0.6.0", IconURL: "https://cdn.example.test/blog.png"},
|
||||
{ID: "acme.shop", Name: "Shop", Category: "commerce", Featured: true, LatestVersion: "v1.0.0", LatestFramework: "v0.9.0", IconURL: "http://cdn.example.test/shop.png"},
|
||||
{ID: "acme.forum", Name: "Forum", Category: "communication", LatestVersion: "v0.1.0", IconURL: "https://user:pw@cdn.example.test/x.png"},
|
||||
{ID: "Acme.Evil;rm", Name: "Evil", Category: "tools"},
|
||||
{ID: "acme.odd", Name: "Odd", Category: "Not Valid"},
|
||||
}}
|
||||
svc := unitPluginsService(t, testInstalled(), reg, clock)
|
||||
rec := pluginsCall(svc, (*service).pluginsMarketplace, pluginsViewer, "/plugins/marketplace")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
out := decodeMarketplace(t, rec)
|
||||
if out.Meta.Total != 4 || len(out.Data) != 4 || out.Meta.Registry.Status != registryStatusOK {
|
||||
t.Fatalf("marketplace = %+v", out)
|
||||
}
|
||||
if strings.Join(out.Meta.Categories, ",") != "commerce,communication,content" {
|
||||
t.Errorf("categories = %v", out.Meta.Categories)
|
||||
}
|
||||
got := map[string]MarketplacePlugin{}
|
||||
for _, p := range out.Data {
|
||||
got[p.ID] = p
|
||||
if p.InstallCommand != "summer plugin:install "+p.ID {
|
||||
t.Errorf("%s command = %q", p.ID, p.InstallCommand)
|
||||
}
|
||||
}
|
||||
if _, ok := got["Acme.Evil;rm"]; ok {
|
||||
t.Error("an ID outside the plugin ID shape was offered")
|
||||
}
|
||||
if b := got["acme.blog"]; !b.Installed || !b.Compatible || b.IconURL != "https://cdn.example.test/blog.png" {
|
||||
t.Errorf("blog = %+v", b)
|
||||
}
|
||||
if s := got["acme.shop"]; s.Installed || s.Compatible || s.IconURL != "" || !s.Featured {
|
||||
t.Errorf("shop = %+v", s)
|
||||
}
|
||||
if f := got["acme.forum"]; !f.Compatible || f.IconURL != "" {
|
||||
t.Errorf("forum = %+v", f)
|
||||
}
|
||||
|
||||
// A filtered search still gets every category for the select.
|
||||
rec = pluginsCall(svc, (*service).pluginsMarketplace, pluginsViewer, "/plugins/marketplace?category=content&q=blog")
|
||||
filtered := decodeMarketplace(t, rec)
|
||||
if len(filtered.Data) != 1 || filtered.Data[0].ID != "acme.blog" || len(filtered.Meta.Categories) != 3 {
|
||||
t.Errorf("filtered = %+v", filtered)
|
||||
}
|
||||
if n := reg.count("list blog|content"); n != 1 {
|
||||
t.Errorf("filtered search lookups = %d", n)
|
||||
}
|
||||
|
||||
for target, field := range map[string]string{
|
||||
"/plugins/marketplace?q=" + strings.Repeat("x", 101): "q",
|
||||
"/plugins/marketplace?category=Content": "category",
|
||||
"/plugins/marketplace?category=" + strings.Repeat("a", 41): "category",
|
||||
"/plugins/marketplace?category=" + url.QueryEscape("a b"): "category",
|
||||
} {
|
||||
before := reg.count("list ")
|
||||
rec := pluginsCall(svc, (*service).pluginsMarketplace, pluginsViewer, target)
|
||||
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "invalid_query") || !strings.Contains(rec.Body.String(), `"`+field+`"`) {
|
||||
t.Errorf("%s: status %d body %s", target, rec.Code, rec.Body.String())
|
||||
}
|
||||
if reg.count("list ") != before {
|
||||
t.Errorf("%s reached the registry", target)
|
||||
}
|
||||
}
|
||||
if rec := pluginsCall(svc, (*service).pluginsMarketplace, pluginsViewer, "/plugins/marketplace?q="+strings.Repeat("ż", 100)); rec.Code != http.StatusOK {
|
||||
t.Errorf("100 runes: status %d", rec.Code)
|
||||
}
|
||||
|
||||
reg.mu.Lock()
|
||||
reg.fail = errors.New("down")
|
||||
reg.mu.Unlock()
|
||||
rec = pluginsCall(svc, (*service).pluginsMarketplace, pluginsViewer, "/plugins/marketplace?q=new")
|
||||
down := decodeMarketplace(t, rec)
|
||||
if rec.Code != http.StatusOK || down.Meta.Registry.Status != registryStatusUnavailable || len(down.Data) != 0 || down.Data == nil {
|
||||
t.Errorf("registry down: %d %+v", rec.Code, down)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled registry makes no call", func(t *testing.T) {
|
||||
reg := &memPluginRegistry{versions: map[string][]kiosk.RegistryVersion{"acme.blog": {rel("v0.2.0", "v0.6.0")}}}
|
||||
svc := unitPluginsService(t, testInstalled(), reg, clock)
|
||||
svc.catalog.disabled = true
|
||||
overview := decodeOverview(t, pluginsCall(svc, (*service).pluginsInstalled, pluginsSuperuser, "/plugins"))
|
||||
rec := pluginsCall(svc, (*service).pluginsMarketplace, pluginsSuperuser, "/plugins/marketplace?q=x")
|
||||
market := decodeMarketplace(t, rec)
|
||||
if overview.Registry != (RegistryState{Enabled: false, Status: registryStatusDisabled}) || market.Meta.Registry.Status != registryStatusDisabled {
|
||||
t.Errorf("overview %+v, marketplace %+v", overview.Registry, market.Meta)
|
||||
}
|
||||
if len(reg.calls) != 0 {
|
||||
t.Errorf("a disabled registry was called: %v", reg.calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unusable registry URL", func(t *testing.T) {
|
||||
c := newPluginCatalog(pluginsConfig(t, "ftp://registry.example.test"))
|
||||
if !c.enabled() || c.registry != nil {
|
||||
t.Fatalf("catalog = %+v", c)
|
||||
}
|
||||
if _, _, err := c.releases(context.Background(), "acme.blog"); !errors.Is(err, errRegistryUnusable) {
|
||||
t.Errorf("releases = %v", err)
|
||||
}
|
||||
if _, err := c.list(context.Background(), "", ""); !errors.Is(err, errRegistryUnusable) {
|
||||
t.Errorf("list = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("helpers", func(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"https://cdn.example.test/a.png": "https://cdn.example.test/a.png",
|
||||
"http://cdn.example.test/a.png": "",
|
||||
"//cdn.example.test/a.png": "",
|
||||
"javascript:alert(1)": "",
|
||||
"https:///a.png": "",
|
||||
"": "",
|
||||
} {
|
||||
if got := httpsURL(in); got != want {
|
||||
t.Errorf("httpsURL(%q) = %q", in, got)
|
||||
}
|
||||
}
|
||||
for in, want := range map[string]string{"v0.6.0": "v0.6.0", "v0.6.0-2-gabc1234": "v0.6.0", "devel": "devel", "": ""} {
|
||||
if got := frameworkRelease(in); got != want {
|
||||
t.Errorf("frameworkRelease(%q) = %q", in, got)
|
||||
}
|
||||
}
|
||||
got := categoriesOf([]kiosk.RegistryPlugin{{ID: "a.seo", Category: "seo"}, {ID: "a.blog", Category: "content"}, {ID: "b.seo", Category: "seo"},
|
||||
{ID: "a.none", Category: ""}, {ID: "a.bad", Category: "Bad Cat"}, {ID: "Bad;id", Category: "tools"}})
|
||||
if strings.Join(got, ",") != "content,seo" {
|
||||
t.Errorf("categoriesOf = %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user