- TestReadIdentity: literal, const, chained, cross-package, alias, other-module and Requires shapes; refusals naming ID()/Requires() - TestDerive, TestManifestFormat, TestCheck, TestVersions, TestRegistryClient (httptest: escaping, 404, envelopes, cap, redirect) - runnable ExampleMarshal and ExampleNewestCompatible - party TestDescribe: copy of the plugin list, publish once per app
262 lines
9.6 KiB
Go
262 lines
9.6 KiB
Go
package kiosk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
)
|
|
|
|
// registryServer serves handler under RegistryAPIPath and records the
|
|
// request URIs it saw.
|
|
func registryServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *[]string) {
|
|
t.Helper()
|
|
var mu sync.Mutex
|
|
var seen []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
seen = append(seen, r.URL.RequestURI())
|
|
mu.Unlock()
|
|
handler(w, r)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv, &seen
|
|
}
|
|
|
|
func newTestClient(t *testing.T, base string) *Client {
|
|
t.Helper()
|
|
c, err := NewClient(base, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient(%s): %v", base, err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// TestRegistryClient covers decoding, query escaping, the error mapping
|
|
// and the transport limits of kiosk.Client.
|
|
func TestRegistryClient(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("decode", func(t *testing.T) {
|
|
srv, seen := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Accept") != "application/json" {
|
|
t.Errorf("Accept = %q", r.Header.Get("Accept"))
|
|
}
|
|
switch r.URL.Path {
|
|
case RegistryAPIPath + "/plugins":
|
|
_ = json.NewEncoder(w).Encode(ListResponse{Data: []RegistryPlugin{{ID: "acme.shop", LatestVersion: "v0.2.0"}}, Meta: ListMeta{Total: 1}})
|
|
case RegistryAPIPath + "/plugins/acme.shop":
|
|
_ = json.NewEncoder(w).Encode(DetailResponse{Data: RegistryPluginDetail{RegistryPlugin: RegistryPlugin{ID: "acme.shop"}, Requires: Requires{"acme.user": "v0.1.0"}}})
|
|
case RegistryAPIPath + "/plugins/acme.shop/versions":
|
|
_, _ = w.Write([]byte(`{"data":[{"version":"v0.2.0","framework":"v0.6.0","requires":{},"future":true},{"version":"v0.1.0","framework":"v0.5.0","requires":{"acme.user":""}}]}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
c := newTestClient(t, srv.URL+"/")
|
|
if c.BaseURL() != srv.URL {
|
|
t.Errorf("BaseURL = %q, want the trailing slash trimmed", c.BaseURL())
|
|
}
|
|
list, err := c.List(ctx, "", "")
|
|
if err != nil || len(list.Data) != 1 || list.Data[0].ID != "acme.shop" || list.Meta.Total != 1 {
|
|
t.Fatalf("List = %+v, %v", list, err)
|
|
}
|
|
detail, err := c.Plugin(ctx, "acme.shop")
|
|
if err != nil || detail.ID != "acme.shop" || detail.Requires["acme.user"] != "v0.1.0" {
|
|
t.Fatalf("Plugin = %+v, %v", detail, err)
|
|
}
|
|
versions, err := c.Versions(ctx, "acme.shop")
|
|
if err != nil || len(versions) != 2 || versions[0].Version != "v0.2.0" || versions[1].Framework != "v0.5.0" {
|
|
t.Fatalf("Versions = %+v, %v", versions, err)
|
|
}
|
|
if (*seen)[0] != RegistryAPIPath+"/plugins" {
|
|
t.Errorf("List without filters asked %q", (*seen)[0])
|
|
}
|
|
})
|
|
|
|
t.Run("query escaping", func(t *testing.T) {
|
|
var got []string
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
got = append(got, r.URL.Path+"|"+r.URL.Query().Get("q")+"|"+r.URL.Query().Get("category")+"|"+r.URL.RawQuery)
|
|
_, _ = w.Write([]byte(`{"data":[],"meta":{"total":0}}`))
|
|
})
|
|
c := newTestClient(t, srv.URL)
|
|
if _, err := c.List(ctx, "100% a&b=c #x", "content"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := c.List(ctx, "", "seo"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got[0] != RegistryAPIPath+"/plugins|100% a&b=c #x|content|category=content&q=100%25+a%26b%3Dc+%23x" {
|
|
t.Errorf("search request = %q", got[0])
|
|
}
|
|
if got[1] != RegistryAPIPath+"/plugins||seo|category=seo" {
|
|
t.Errorf("category request = %q", got[1])
|
|
}
|
|
})
|
|
|
|
t.Run("plugin id is path-escaped", func(t *testing.T) {
|
|
srv, seen := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
http.NotFound(w, r)
|
|
})
|
|
c := newTestClient(t, srv.URL)
|
|
_, _ = c.Plugin(ctx, "a/../b?x")
|
|
if len(*seen) != 1 || (*seen)[0] != RegistryAPIPath+"/plugins/a%2F..%2Fb%3Fx" {
|
|
t.Errorf("requested %q", *seen)
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no plugin"}}`))
|
|
})
|
|
c := newTestClient(t, srv.URL)
|
|
_, err := c.Plugin(ctx, "acme.gone")
|
|
if !errors.Is(err, ErrNotFound) || !strings.Contains(err.Error(), "acme.gone") || !strings.Contains(err.Error(), srv.URL) {
|
|
t.Errorf("Plugin 404 = %v, want ErrNotFound naming the ID and the registry", err)
|
|
}
|
|
_, err = c.Versions(ctx, "acme.gone")
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("Versions 404 = %v", err)
|
|
}
|
|
_, err = c.List(ctx, "", "")
|
|
if !errors.Is(err, ErrNotFound) || !strings.Contains(err.Error(), srv.URL) {
|
|
t.Errorf("List 404 = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("error envelope", func(t *testing.T) {
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") == "plain" {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte("<html>bad gateway</html>"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"invalid_query","message":"category must match ^[a-z0-9-]{1,40}$"}}`))
|
|
})
|
|
c := newTestClient(t, srv.URL)
|
|
_, err := c.List(ctx, "", "Bad")
|
|
if err == nil || errors.Is(err, ErrNotFound) || !strings.Contains(err.Error(), "HTTP 400 invalid_query") || !strings.Contains(err.Error(), "category must match") {
|
|
t.Errorf("400 = %v, want the code and message", err)
|
|
}
|
|
_, err = c.List(ctx, "plain", "")
|
|
if err == nil || !strings.Contains(err.Error(), "HTTP 502") || strings.Contains(err.Error(), "html") {
|
|
t.Errorf("502 without an envelope = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("server error with envelope", func(t *testing.T) {
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"internal","message":"database down"}}`))
|
|
})
|
|
_, err := newTestClient(t, srv.URL).Versions(ctx, "acme.shop")
|
|
if err == nil || !strings.Contains(err.Error(), "HTTP 500 internal: database down") {
|
|
t.Errorf("500 = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("bad json", func(t *testing.T) {
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"data":[`))
|
|
})
|
|
_, err := newTestClient(t, srv.URL).Versions(ctx, "acme.shop")
|
|
if err == nil || !strings.Contains(err.Error(), "decode") {
|
|
t.Errorf("truncated JSON = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("oversized body", func(t *testing.T) {
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"data":[],"meta":{"total":0},"pad":"`))
|
|
_, _ = w.Write([]byte(strings.Repeat("x", registryMaxBytes)))
|
|
_, _ = w.Write([]byte(`"}`))
|
|
})
|
|
if _, err := newTestClient(t, srv.URL).List(ctx, "", ""); err == nil {
|
|
t.Error("a body over the cap was accepted")
|
|
}
|
|
})
|
|
|
|
t.Run("redirect is not followed", func(t *testing.T) {
|
|
target, targetSeen := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"data":[],"meta":{"total":0}}`))
|
|
})
|
|
srv, _ := registryServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, target.URL+r.URL.RequestURI(), http.StatusFound)
|
|
})
|
|
_, err := newTestClient(t, srv.URL).List(ctx, "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "redirect") || !strings.Contains(err.Error(), "plugins.registry_url") {
|
|
t.Errorf("redirect = %v, want an error naming plugins.registry_url", err)
|
|
}
|
|
if len(*targetSeen) != 0 {
|
|
t.Errorf("the redirect target was called: %v", *targetSeen)
|
|
}
|
|
})
|
|
|
|
t.Run("NewClient", func(t *testing.T) {
|
|
for _, base := range []string{"", " ", "/"} {
|
|
if _, err := NewClient(base, nil); !errors.Is(err, ErrRegistryDisabled) {
|
|
t.Errorf("NewClient(%q) = %v, want ErrRegistryDisabled", base, err)
|
|
}
|
|
}
|
|
for base, want := range map[string]string{
|
|
"ftp://example.test": "https://",
|
|
"example.test": "https://",
|
|
"https://": "no host",
|
|
"https://user:pw@example.test": "user info",
|
|
"https://example.test/?a=b": "query",
|
|
"https://example.test/#x": "query",
|
|
"https://example.test/?": "query",
|
|
"https://exa mple.test": "registry URL",
|
|
} {
|
|
if _, err := NewClient(base, nil); err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("NewClient(%q) = %v, want an error naming %q", base, err, want)
|
|
}
|
|
}
|
|
c, err := NewClient(" https://registry.example.test/ ", nil)
|
|
if err != nil || c.BaseURL() != "https://registry.example.test" {
|
|
t.Errorf("NewClient trims: %v, %v", c, err)
|
|
}
|
|
})
|
|
|
|
t.Run("RegistryURL", func(t *testing.T) {
|
|
if got := RegistryURL(nil); got != DefaultRegistryURL {
|
|
t.Errorf("RegistryURL(nil) = %q", got)
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_ENV=development"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := RegistryURL(cfg); got != DefaultRegistryURL {
|
|
t.Errorf("RegistryURL without the key = %q", got)
|
|
}
|
|
for set, want := range map[string]string{
|
|
"https://registry.example.test/": "https://registry.example.test",
|
|
" http://127.0.0.1:8080 ": "http://127.0.0.1:8080",
|
|
"": "",
|
|
} {
|
|
if err := cfg.Set("plugins.registry_url", set); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := RegistryURL(cfg); got != want {
|
|
t.Errorf("RegistryURL(%q) = %q, want %q", set, got, want)
|
|
}
|
|
}
|
|
env, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_ENV=development", "SUMMER_PLUGINS__REGISTRY_URL=https://env.example.test"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := RegistryURL(env); got != "https://env.example.test" {
|
|
t.Errorf("RegistryURL from the environment = %q", got)
|
|
}
|
|
})
|
|
}
|