clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / tinyclient / webmcp_test.go
webmcp_test.go 4.8K
1package tinyclient23import (4 "context"5 "encoding/json"6 "github.com/FelineStateMachine/tinyrelay/internal/policy"7 "net/http"8 "net/http/httptest"9 "net/url"10 "strings"11 "testing"12)1314func TestWebMCPScriptUsesNativeModelContextAndExistingAuth(t *testing.T) {15 backend := &fakeBackend{}16 app, err := New(backend, Options{})17 if err != nil {18 t.Fatal(err)19 }20 recorder := httptest.NewRecorder()21 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/webmcp.js", nil))22 if recorder.Code != http.StatusOK {23 t.Fatalf("status=%d", recorder.Code)24 }25 body := recorder.Body.String()26 for _, marker := range []string{"document.modelContext", "navigator.modelContext", "tiny.list_repositories", "tiny.read_repository", "tiny.read_file", "tiny.read_status", "tiny.add_job", "tiny.set_policy", "structuredContent", "state.errors", "tiny.read_management", "tinySignedFetch"} {27 if !strings.Contains(body, marker) {28 t.Fatalf("WebMCP script missing %q", marker)29 }30 }31}3233func TestWebMCPQueryReturnsStructuredBackendResult(t *testing.T) {34 backend := &webMCPBackend{fakeBackend: fakeBackend{}}35 app, err := New(backend, Options{Actor: func(*http.Request) (string, error) { return "actor", nil }})36 if err != nil {37 t.Fatal(err)38 }39 recorder := httptest.NewRecorder()40 request := httptest.NewRequest(http.MethodGet, "/webmcp/query?method=browserepos&params="+url.QueryEscape(`[{"limit":2}]`), nil)41 request.Header.Set("content-type", "application/json")42 app.ServeHTTP(recorder, request)43 if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "repo-visible") || backend.method != "browserepos" || backend.actor != "actor" {44 t.Fatalf("status=%d body=%s method=%s actor=%s", recorder.Code, recorder.Body.String(), backend.method, backend.actor)45 }46 if recorder.Header().Get("cache-control") != "no-store, private" {47 t.Fatalf("query response must not be cached: %q", recorder.Header().Get("cache-control"))48 }49}5051func TestWebMCPQueryErrorsAreJSON(t *testing.T) {52 app, err := New(&fakeBackend{}, Options{Actor: func(*http.Request) (string, error) {53 return "", context.Canceled54 }})55 if err != nil {56 t.Fatal(err)57 }58 recorder := httptest.NewRecorder()59 request := httptest.NewRequest(http.MethodGet, "/webmcp/query?method=browserepos", nil)60 app.ServeHTTP(recorder, request)61 if recorder.Code != http.StatusUnauthorized || !strings.Contains(recorder.Header().Get("content-type"), "application/json") || !strings.Contains(recorder.Body.String(), `"error"`) {62 t.Fatalf("status=%d content-type=%q body=%s", recorder.Code, recorder.Header().Get("content-type"), recorder.Body.String())63 }64}6566func TestWebMCPQueryRejectsMutationMethods(t *testing.T) {67 app, err := New(&fakeBackend{}, Options{})68 if err != nil {69 t.Fatal(err)70 }71 recorder := httptest.NewRecorder()72 request := httptest.NewRequest(http.MethodGet, "/webmcp/query?method=setpolicy", nil)73 app.ServeHTTP(recorder, request)74 if recorder.Code != http.StatusBadRequest {75 t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())76 }77}7879type webMCPBackend struct {80 fakeBackend81 method string82 actor string83}8485func (b *webMCPBackend) Query(_ context.Context, method string, _ []json.RawMessage, actor string) (any, error) {86 b.method, b.actor = method, actor87 return map[string]any{"items": []any{map[string]any{"identifier": "repo-visible"}}}, nil88}8990func TestPagesAdvertiseWebMCPScript(t *testing.T) {91 app, err := New(&fakeBackend{}, Options{})92 if err != nil {93 t.Fatal(err)94 }95 recorder := httptest.NewRecorder()96 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))97 if !strings.Contains(recorder.Body.String(), `src="/webmcp.js"`) {98 t.Fatal("page does not load WebMCP registration")99 }100}101102func TestHealthPageReportsVersionsAndBrowserTools(t *testing.T) {103 backend := &fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}104 app, err := New(backend, Options{Actor: func(*http.Request) (string, error) { return backend.policy.Owner, nil }, Version: "1.4.0", Revision: "abc1234"})105 if err != nil {106 t.Fatal(err)107 }108 recorder := httptest.NewRecorder()109 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/manage/health", nil))110 body := recorder.Body.String()111 for _, want := range []string{"<code>1.4.0</code>", "abc1234", "<code>" + scriptsVersion + "</code>", `id="webmcp-status"`, "WebMCP is unavailable", "signed forms"} {112 if recorder.Code != http.StatusOK || !strings.Contains(body, want) {113 t.Fatalf("health page missing %q: status=%d", want, recorder.Code)114 }115 }116 for _, old := range []string{"/tools", "/manage/status"} {117 moved := httptest.NewRecorder()118 app.ServeHTTP(moved, httptest.NewRequest(http.MethodGet, old, nil))119 if moved.Code != http.StatusMovedPermanently || moved.Header().Get("Location") != "/manage/health" {120 t.Fatalf("%s: status=%d location=%q", old, moved.Code, moved.Header().Get("Location"))121 }122 }123}