|
@@ -0,0 +1,508 @@
|
|
|
|
|
+package server
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "context"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "log/slog"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/http/httptest"
|
|
|
|
|
+ "slices"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "sync"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+
|
|
|
|
|
+ "wikijs-mcp/internal/wikijs"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type capturedRequest struct {
|
|
|
|
|
+ Query string `json:"query"`
|
|
|
|
|
+ Variables map[string]any `json:"variables"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type fakeWiki struct {
|
|
|
|
|
+ t *testing.T
|
|
|
|
|
+ mu sync.Mutex
|
|
|
|
|
+ requests []capturedRequest
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (f *fakeWiki) handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ f.t.Helper()
|
|
|
|
|
+ if got, want := r.Header.Get("Authorization"), "Bearer test-key"; got != want {
|
|
|
|
|
+ f.t.Errorf("Authorization = %q, want %q", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var req capturedRequest
|
|
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
+ f.t.Errorf("decode request: %v", err)
|
|
|
|
|
+ http.Error(w, "bad request", http.StatusBadRequest)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ f.mu.Lock()
|
|
|
|
|
+ f.requests = append(f.requests, req)
|
|
|
|
|
+ f.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
+ switch {
|
|
|
|
|
+ case strings.Contains(req.Query, "query SearchPages"):
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"search":{"results":[
|
|
|
|
|
+ {"title":"MCP","description":"protocol","path":"/mcp","locale":"en"}
|
|
|
|
|
+ ],"suggestions":["mcp"],"totalHits":1}}}}`)
|
|
|
|
|
+
|
|
|
|
|
+ case strings.Contains(req.Query, "query GetPageById"):
|
|
|
|
|
+ id, _ := req.Variables["id"].(float64)
|
|
|
|
|
+ if id == 404 {
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"single":null}}}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Fprintf(w, `{"data":{"pages":{"single":{
|
|
|
|
|
+ "id":%d,"path":"/dev/go","title":"Go","description":"desc",
|
|
|
|
|
+ "tags":[{"tag":"go","title":"Go"}],"content":"body",
|
|
|
|
|
+ "createdAt":"c","updatedAt":"u","locale":"de"}}}}`, int(id))
|
|
|
|
|
+
|
|
|
|
|
+ case strings.Contains(req.Query, "query GetPageByPath"):
|
|
|
|
|
+ path, _ := req.Variables["path"].(string)
|
|
|
|
|
+ locale, _ := req.Variables["locale"].(string)
|
|
|
|
|
+ fmt.Fprintf(w, `{"data":{"pages":{"singleByPath":{
|
|
|
|
|
+ "id":7,"path":%q,"title":"Start","description":"desc",
|
|
|
|
|
+ "tags":[],"content":"body","createdAt":"c","updatedAt":"u","locale":%q}}}}`, path, locale)
|
|
|
|
|
+
|
|
|
|
|
+ case strings.Contains(req.Query, "query GetAllPages"):
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"list":[
|
|
|
|
|
+ {"id":1,"path":"/a","locale":"de","title":"A","description":"a",
|
|
|
|
|
+ "createdAt":"c","updatedAt":"u","tags":["go"]}
|
|
|
|
|
+ ]}}}`)
|
|
|
|
|
+
|
|
|
|
|
+ case strings.Contains(req.Query, "mutation CreatePage"):
|
|
|
|
|
+ title, _ := req.Variables["title"].(string)
|
|
|
|
|
+ if title == "fail" {
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"create":{"responseResult":{
|
|
|
|
|
+ "succeeded":false,"errorCode":6001,"slug":"","message":"Nope"}}}}}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"create":{"responseResult":{
|
|
|
|
|
+ "succeeded":true,"errorCode":0,"slug":"new","message":"Page created"}}}}}`)
|
|
|
|
|
+
|
|
|
|
|
+ case strings.Contains(req.Query, "mutation UpdatePage"):
|
|
|
|
|
+ id, _ := req.Variables["id"].(float64)
|
|
|
|
|
+ if id == 404 {
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"update":{"responseResult":{
|
|
|
|
|
+ "succeeded":false,"errorCode":6001,"slug":"","message":"Page not found"}}}}}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Fprint(w, `{"data":{"pages":{"update":{"responseResult":{
|
|
|
|
|
+ "succeeded":true,"errorCode":0,"slug":"","message":"Page updated"}}}}}`)
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ f.t.Errorf("unexpected query: %s", req.Query)
|
|
|
|
|
+ http.Error(w, "unknown operation", http.StatusBadRequest)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (f *fakeWiki) lastContaining(t *testing.T, fragment string) capturedRequest {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ f.mu.Lock()
|
|
|
|
|
+ defer f.mu.Unlock()
|
|
|
|
|
+ for i := len(f.requests) - 1; i >= 0; i-- {
|
|
|
|
|
+ if strings.Contains(f.requests[i].Query, fragment) {
|
|
|
|
|
+ return f.requests[i]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Fatalf("no request containing %q", fragment)
|
|
|
|
|
+ return capturedRequest{}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func newTestSession(t *testing.T, enableEdit bool) (*mcp.ClientSession, *fakeWiki) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+
|
|
|
|
|
+ fake := &fakeWiki{t: t}
|
|
|
|
|
+ wikiSrv := httptest.NewServer(http.HandlerFunc(fake.handle))
|
|
|
|
|
+ t.Cleanup(wikiSrv.Close)
|
|
|
|
|
+
|
|
|
|
|
+ wikiClient, err := wikijs.New(wikiSrv.URL, "test-key")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("wikijs.New() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ srv := New(wikiClient, Options{
|
|
|
|
|
+ Version: "test",
|
|
|
|
|
+ EnableEdit: enableEdit,
|
|
|
|
|
+ Logger: slog.New(slog.DiscardHandler),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ st, ct := mcp.NewInMemoryTransports()
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ ss, err := srv.Connect(ctx, st, nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("server connect: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = ss.Close() })
|
|
|
|
|
+
|
|
|
|
|
+ client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0"}, nil)
|
|
|
|
|
+ cs, err := client.Connect(ctx, ct, nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("client connect: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = cs.Close() })
|
|
|
|
|
+
|
|
|
|
|
+ return cs, fake
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func callTool(t *testing.T, cs *mcp.ClientSession, name string, args map[string]any) *mcp.CallToolResult {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ res, err := cs.CallTool(context.Background(), &mcp.CallToolParams{Name: name, Arguments: args})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("CallTool(%s) error = %v", name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return res
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func toolText(t *testing.T, res *mcp.CallToolResult) string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ if len(res.Content) == 0 {
|
|
|
|
|
+ t.Fatal("tool result has no content")
|
|
|
|
|
+ }
|
|
|
|
|
+ text, ok := res.Content[0].(*mcp.TextContent)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ t.Fatalf("content type = %T, want *mcp.TextContent", res.Content[0])
|
|
|
|
|
+ }
|
|
|
|
|
+ return text.Text
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestToolListWithoutEdit(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res, err := cs.ListTools(context.Background(), nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("ListTools() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ got := toolNames(res.Tools)
|
|
|
|
|
+ want := []string{"search_pages", "get_page_by_id", "get_page_by_path", "get_all_pages"}
|
|
|
|
|
+ if !equalNames(got, want) {
|
|
|
|
|
+ t.Errorf("tools = %v, want %v", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestToolListWithEdit(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, true)
|
|
|
|
|
+
|
|
|
|
|
+ res, err := cs.ListTools(context.Background(), nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("ListTools() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ got := toolNames(res.Tools)
|
|
|
|
|
+ want := []string{"search_pages", "get_page_by_id", "get_page_by_path", "get_all_pages", "create_page", "edit_page"}
|
|
|
|
|
+ if !equalNames(got, want) {
|
|
|
|
|
+ t.Errorf("tools = %v, want %v", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func toolNames(tools []*mcp.Tool) []string {
|
|
|
|
|
+ names := make([]string, len(tools))
|
|
|
|
|
+ for i, tool := range tools {
|
|
|
|
|
+ names[i] = tool.Name
|
|
|
|
|
+ }
|
|
|
|
|
+ return names
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func equalNames(got, want []string) bool {
|
|
|
|
|
+ got = slices.Clone(got)
|
|
|
|
|
+ want = slices.Clone(want)
|
|
|
|
|
+ slices.Sort(got)
|
|
|
|
|
+ slices.Sort(want)
|
|
|
|
|
+ return slices.Equal(got, want)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSearchPagesTool(t *testing.T) {
|
|
|
|
|
+ cs, fake := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "search_pages", map[string]any{"query": "mcp"})
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+ if res.StructuredContent == nil {
|
|
|
|
|
+ t.Error("StructuredContent = nil")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var out wikijs.SearchResults
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &out); err != nil {
|
|
|
|
|
+ t.Fatalf("decode result: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if out.TotalHits != 1 || len(out.Results) != 1 {
|
|
|
|
|
+ t.Fatalf("result = %+v", out)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasSuffix(out.Results[0].URL, "/mcp") {
|
|
|
|
|
+ t.Errorf("URL = %q", out.Results[0].URL)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := fake.lastContaining(t, "query SearchPages")
|
|
|
|
|
+ if _, ok := req.Variables["path"]; ok {
|
|
|
|
|
+ t.Errorf("path variable present: %v", req.Variables["path"])
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSearchPagesValidation(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ tests := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ args map[string]any
|
|
|
|
|
+ want string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {name: "empty query", args: map[string]any{"query": " "}, want: "query must not be empty"},
|
|
|
|
|
+ {name: "bad locale", args: map[string]any{"query": "x", "locale": "english"}, want: "invalid locale"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, tt := range tests {
|
|
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
+ res := callTool(t, cs, "search_pages", tt.args)
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := toolText(t, res); !strings.Contains(got, tt.want) {
|
|
|
|
|
+ t.Errorf("text = %q, want substring %q", got, tt.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetPageByIDTool(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_page_by_id", map[string]any{"id": 42})
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+ var page wikijs.Page
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &page); err != nil {
|
|
|
|
|
+ t.Fatalf("decode page: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if page.ID != 42 || page.Title != "Go" {
|
|
|
|
|
+ t.Errorf("page = %+v", page)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasSuffix(page.URL, "/de/dev/go") {
|
|
|
|
|
+ t.Errorf("URL = %q", page.URL)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetPageByIDNotFound(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_page_by_id", map[string]any{"id": 404})
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := strings.TrimSpace(toolText(t, res)); got != "null" {
|
|
|
|
|
+ t.Errorf("text = %q, want null", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetPageByIDInvalid(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_page_by_id", map[string]any{"id": 0})
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := toolText(t, res); !strings.Contains(got, "positive") {
|
|
|
|
|
+ t.Errorf("text = %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetPageByPathTool(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_page_by_path", map[string]any{"path": "/start", "locale": "de-DE"})
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+ var page wikijs.Page
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &page); err != nil {
|
|
|
|
|
+ t.Fatalf("decode page: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if page.ID != 7 {
|
|
|
|
|
+ t.Errorf("page = %+v", page)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasSuffix(page.URL, "/de-DE/start") {
|
|
|
|
|
+ t.Errorf("URL = %q", page.URL)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGetPageByPathRequiresLocale(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_page_by_path", map[string]any{"path": "/start"})
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestListPagesTool(t *testing.T) {
|
|
|
|
|
+ cs, fake := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_all_pages", map[string]any{
|
|
|
|
|
+ "limit": 10,
|
|
|
|
|
+ "locale": "de",
|
|
|
|
|
+ "tags": []string{"go"},
|
|
|
|
|
+ })
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+ var pages []wikijs.PageListItem
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &pages); err != nil {
|
|
|
|
|
+ t.Fatalf("decode pages: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(pages) != 1 || pages[0].ID != 1 {
|
|
|
|
|
+ t.Errorf("pages = %+v", pages)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := fake.lastContaining(t, "query GetAllPages")
|
|
|
|
|
+ if got := req.Variables["limit"]; got != float64(10) {
|
|
|
|
|
+ t.Errorf("variables[limit] = %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := req.Variables["locale"]; got != "de" {
|
|
|
|
|
+ t.Errorf("variables[locale] = %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestListPagesValidation(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_all_pages", map[string]any{"limit": 0})
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := toolText(t, res); !strings.Contains(got, "limit must be a positive integer") {
|
|
|
|
|
+ t.Errorf("text = %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestCreatePageDefaults(t *testing.T) {
|
|
|
|
|
+ cs, fake := newTestSession(t, true)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "create_page", map[string]any{
|
|
|
|
|
+ "title": "New",
|
|
|
|
|
+ "content": "body",
|
|
|
|
|
+ "description": "desc",
|
|
|
|
|
+ "path": "/new",
|
|
|
|
|
+ })
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var out mutationOutput
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &out); err != nil {
|
|
|
|
|
+ t.Fatalf("decode output: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !out.Succeeded || out.Message != "Page created" {
|
|
|
|
|
+ t.Errorf("output = %+v", out)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := fake.lastContaining(t, "mutation CreatePage")
|
|
|
|
|
+ want := map[string]any{
|
|
|
|
|
+ "editor": "markdown",
|
|
|
|
|
+ "isPublished": true,
|
|
|
|
|
+ "isPrivate": false,
|
|
|
|
|
+ "locale": "en",
|
|
|
|
|
+ }
|
|
|
|
|
+ for key, value := range want {
|
|
|
|
|
+ if got := req.Variables[key]; got != value {
|
|
|
|
|
+ t.Errorf("variables[%s] = %v, want %v", key, got, value)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ tags, ok := req.Variables["tags"].([]any)
|
|
|
|
|
+ if !ok || len(tags) != 0 {
|
|
|
|
|
+ t.Errorf("variables[tags] = %v, want []", req.Variables["tags"])
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestCreatePageFailure(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, true)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "create_page", map[string]any{
|
|
|
|
|
+ "title": "fail",
|
|
|
|
|
+ "content": "body",
|
|
|
|
|
+ "description": "desc",
|
|
|
|
|
+ "path": "/new",
|
|
|
|
|
+ })
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ text := toolText(t, res)
|
|
|
|
|
+ if !strings.Contains(text, "failed to create page") || !strings.Contains(text, "Nope") || !strings.Contains(text, "6001") {
|
|
|
|
|
+ t.Errorf("text = %q", text)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestEditPageDefaults(t *testing.T) {
|
|
|
|
|
+ cs, fake := newTestSession(t, true)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "edit_page", map[string]any{
|
|
|
|
|
+ "id": 7,
|
|
|
|
|
+ "title": "Updated",
|
|
|
|
|
+ "content": "body",
|
|
|
|
|
+ "description": "desc",
|
|
|
|
|
+ "path": "/start",
|
|
|
|
|
+ })
|
|
|
|
|
+ if res.IsError {
|
|
|
|
|
+ t.Fatalf("IsError = true: %s", toolText(t, res))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var out mutationOutput
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &out); err != nil {
|
|
|
|
|
+ t.Fatalf("decode output: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !out.Succeeded || out.Message != "Page updated" {
|
|
|
|
|
+ t.Errorf("output = %+v", out)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := fake.lastContaining(t, "mutation UpdatePage")
|
|
|
|
|
+ if got := req.Variables["id"]; got != float64(7) {
|
|
|
|
|
+ t.Errorf("variables[id] = %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := req.Variables["editor"]; got != "markdown" {
|
|
|
|
|
+ t.Errorf("variables[editor] = %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestEditPageNotFound(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, true)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "edit_page", map[string]any{
|
|
|
|
|
+ "id": 404,
|
|
|
|
|
+ "title": "Updated",
|
|
|
|
|
+ "content": "body",
|
|
|
|
|
+ "description": "desc",
|
|
|
|
|
+ "path": "/start",
|
|
|
|
|
+ })
|
|
|
|
|
+ if !res.IsError {
|
|
|
|
|
+ t.Fatal("IsError = false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ text := toolText(t, res)
|
|
|
|
|
+ if !strings.Contains(text, "failed to update page") || !strings.Contains(text, "Page not found") {
|
|
|
|
|
+ t.Errorf("text = %q", text)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestEditToolsHiddenWithoutEditMode(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ _, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
|
|
|
|
+ Name: "create_page",
|
|
|
|
|
+ Arguments: map[string]any{"title": "x", "content": "", "description": "", "path": "/x"},
|
|
|
|
|
+ })
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("CallTool(create_page) error = nil, want tool not found")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestToolOutputIsJSON(t *testing.T) {
|
|
|
|
|
+ cs, _ := newTestSession(t, false)
|
|
|
|
|
+
|
|
|
|
|
+ res := callTool(t, cs, "get_all_pages", nil)
|
|
|
|
|
+ var decoded any
|
|
|
|
|
+ if err := json.Unmarshal([]byte(toolText(t, res)), &decoded); err != nil {
|
|
|
|
|
+ t.Fatalf("output is not JSON: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|