tinyrelayfile
tinyrelay/tinyclient/webmcp_test.go/

tinyclient/webmcp_test.go raw
1package tinyclient
2
3import (
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)
13
14func 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}
32
33func 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}
50
51func TestWebMCPQueryErrorsAreJSON(t *testing.T) {
52 app, err := New(&fakeBackend{}, Options{Actor: func(*http.Request) (string, error) {
53 return "", context.Canceled
54 }})
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}
65
66func 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}
78
79type webMCPBackend struct {
80 fakeBackend
81 method string
82 actor string
83}
84
85func (b *webMCPBackend) Query(_ context.Context, method string, _ []json.RawMessage, actor string) (any, error) {
86 b.method, b.actor = method, actor
87 return map[string]any{"items": []any{map[string]any{"identifier": "repo-visible"}}}, nil
88}
89
90func 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}
101
102func 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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs