Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
11 kB · 276 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277package pluginhost
import ( "context" "encoding/json" "html" "net/http" "net/http/httptest" "testing"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi")
func TestRegisterManagementRoutesSkipsReservedAndUsesPriority(t *testing.T) { high := &managementPluginDouble{ routes: []pluginapi.ManagementRoute{ {Method: http.MethodGet, Path: "/config", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{Body: []byte("reserved")}, nil })}, {Method: http.MethodGet, Path: "/plugins/shared/status", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{Body: []byte("high")}, nil })}, }, } low := &managementPluginDouble{ routes: []pluginapi.ManagementRoute{ {Method: http.MethodGet, Path: "/plugins/shared/status", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{Body: []byte("low")}, nil })}, {Method: http.MethodPost, Path: "plugins/low/run", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{StatusCode: http.StatusAccepted, Body: []byte("low-only")}, nil })}, }, } host := newHostWithRecords( capabilityRecord{id: "low", priority: 1, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ManagementAPI: low}}}, capabilityRecord{id: "high", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ManagementAPI: high}}}, ) host.RegisterManagementRoutes(context.Background(), map[string]struct{}{ "GET /v0/management/config": {}, })
req := httptest.NewRequest(http.MethodGet, "/v0/management/plugins/shared/status", nil) rec := httptest.NewRecorder() if !host.ServeManagementHTTP(rec, req) { t.Fatal("ServeManagementHTTP() = false, want true") } if rec.Body.String() != "high" { t.Fatalf("Body = %q, want high", rec.Body.String()) }
req = httptest.NewRequest(http.MethodPost, "/v0/management/plugins/low/run", nil) rec = httptest.NewRecorder() if !host.ServeManagementHTTP(rec, req) { t.Fatal("ServeManagementHTTP() for low route = false, want true") } if rec.Code != http.StatusAccepted || rec.Body.String() != "low-only" { t.Fatalf("response = %d %q, want 202 low-only", rec.Code, rec.Body.String()) }
req = httptest.NewRequest(http.MethodGet, "/v0/management/config", nil) rec = httptest.NewRecorder() if host.ServeManagementHTTP(rec, req) { t.Fatal("reserved route was served by plugin") }}
func TestServeManagementHTMLEscapesJSONResponseStrings(t *testing.T) { host := newHostWithRecords(capabilityRecord{ id: "json", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ManagementAPI: &managementPluginDouble{routes: []pluginapi.ManagementRoute{{ Method: http.MethodGet, Path: "/plugins/json/status", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{ Headers: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}}, Body: []byte(`{ "title": "<script>alert(1)</script>", "items": ["<b>first</b>", {"description": "safe & sound"}], "count": 1 }`), }, nil }), }}}, }}, }) host.RegisterManagementRoutes(context.Background(), nil)
req := httptest.NewRequest(http.MethodGet, "/v0/management/plugins/json/status", nil) rec := httptest.NewRecorder() if !host.ServeManagementHTTP(rec, req) { t.Fatal("ServeManagementHTTP() = false, want true") } if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) }
var body map[string]any if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil { t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String()) } if body["title"] != html.EscapeString("<script>alert(1)</script>") { t.Fatalf("title = %q, want escaped", body["title"]) } items, okItems := body["items"].([]any) if !okItems || len(items) != 2 { t.Fatalf("items = %#v, want two items", body["items"]) } if items[0] != html.EscapeString("<b>first</b>") { t.Fatalf("items[0] = %q, want escaped", items[0]) } nested, okNested := items[1].(map[string]any) if !okNested { t.Fatalf("items[1] = %#v, want object", items[1]) } if nested["description"] != html.EscapeString("safe & sound") { t.Fatalf("nested description = %q, want escaped", nested["description"]) } if body["count"] != float64(1) { t.Fatalf("count = %#v, want unchanged number", body["count"]) }}
func TestManagementHandlerPanicFusesPlugin(t *testing.T) { host := newHostWithRecords(capabilityRecord{ id: "panic", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ManagementAPI: &managementPluginDouble{routes: []pluginapi.ManagementRoute{{ Method: http.MethodGet, Path: "/plugins/panic", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { panic("boom") }), }}}, }}, }) host.RegisterManagementRoutes(context.Background(), nil)
req := httptest.NewRequest(http.MethodGet, "/v0/management/plugins/panic", nil) rec := httptest.NewRecorder() if !host.ServeManagementHTTP(rec, req) { t.Fatal("ServeManagementHTTP() = false, want true") } if rec.Code != http.StatusBadGateway { t.Fatalf("status = %d, want 502", rec.Code) } if !host.isPluginFused("panic") { t.Fatal("plugin was not fused after panic") }}
func TestServeResourceHTTPDispatchesPluginResource(t *testing.T) { host := newHostWithRecords(capabilityRecord{ id: "resource", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ManagementAPI: &managementPluginDouble{resources: []pluginapi.ResourceRoute{{ Path: "/status", Menu: "Status", Description: "Shows plugin status.", Handler: managementHandlerFunc(func(_ context.Context, req pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { if req.Path != "/v0/resource/plugins/resource/status" { t.Fatalf("resource request path = %q, want normalized resource path", req.Path) } return pluginapi.ManagementResponse{ Headers: http.Header{"Content-Type": []string{"text/html; charset=utf-8"}}, Body: []byte("<!doctype html><title>resource</title>"), }, nil }), }}}, }}, }) host.RegisterManagementRoutes(context.Background(), nil)
req := httptest.NewRequest(http.MethodGet, "/v0/resource/plugins/resource/status", nil) rec := httptest.NewRecorder() if !host.ServeResourceHTTP(rec, req) { t.Fatal("ServeResourceHTTP() = false, want true") } if rec.Code != http.StatusOK || rec.Body.String() != "<!doctype html><title>resource</title>" { t.Fatalf("response = %d %q, want 200 html", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); got != "text/html; charset=utf-8" { t.Fatalf("Content-Type = %q, want text/html; charset=utf-8", got) }}
func TestLegacyGETManagementMenuRegistersAsResource(t *testing.T) { host := newHostWithRecords(capabilityRecord{ id: "legacy", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ ManagementAPI: &managementPluginDouble{routes: []pluginapi.ManagementRoute{{ Method: http.MethodGet, Path: "/plugins/legacy/status", Menu: "Legacy Status", Description: "Shows legacy plugin status.", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{Body: []byte("legacy")}, nil }), }}}, }}, }) host.RegisterManagementRoutes(context.Background(), nil)
managementReq := httptest.NewRequest(http.MethodGet, "/v0/management/plugins/legacy/status", nil) managementRec := httptest.NewRecorder() if host.ServeManagementHTTP(managementRec, managementReq) { t.Fatal("legacy menu route was served as Management API route") }
resourceReq := httptest.NewRequest(http.MethodGet, "/v0/resource/plugins/legacy/status", nil) resourceRec := httptest.NewRecorder() if !host.ServeResourceHTTP(resourceRec, resourceReq) { t.Fatal("legacy menu route was not served as resource route") } if resourceRec.Body.String() != "legacy" { t.Fatalf("resource body = %q, want legacy", resourceRec.Body.String()) }}
func TestRegisteredPluginsIncludesResourceMenus(t *testing.T) { plugin := &managementPluginDouble{ routes: []pluginapi.ManagementRoute{ { Method: http.MethodGet, Path: "/plugins/menu/hidden", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{}, nil }), }, }, resources: []pluginapi.ResourceRoute{ { Path: "/status", Menu: "Status", Description: "Shows plugin status.", Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return pluginapi.ManagementResponse{}, nil }), }, }, } host := newHostWithRecords(capabilityRecord{ id: "menu", meta: pluginapi.Metadata{Name: "menu", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI"}, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ManagementAPI: plugin}}, }) host.RegisterManagementRoutes(context.Background(), nil)
plugins := host.RegisteredPlugins() if len(plugins) != 1 { t.Fatalf("RegisteredPlugins() len = %d, want 1", len(plugins)) } if len(plugins[0].Menus) != 1 { t.Fatalf("RegisteredPlugins()[0].Menus = %#v, want one visible GET menu", plugins[0].Menus) } menu := plugins[0].Menus[0] if menu.Path != "/v0/resource/plugins/menu/status" || menu.Menu != "Status" || menu.Description != "Shows plugin status." { t.Fatalf("menu = %#v, want normalized status menu", menu) }}
type managementPluginDouble struct { routes []pluginapi.ManagementRoute resources []pluginapi.ResourceRoute}
func (p *managementPluginDouble) RegisterManagement(context.Context, pluginapi.ManagementRegistrationRequest) (pluginapi.ManagementRegistrationResponse, error) { return pluginapi.ManagementRegistrationResponse{Routes: p.routes, Resources: p.resources}, nil}
type managementHandlerFunc func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error)
func (f managementHandlerFunc) HandleManagement(ctx context.Context, req pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { return f(ctx, req)}