227 lines
6.8 KiB
Go
227 lines
6.8 KiB
Go
package mockmcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
|
|
)
|
|
|
|
func TestDefaultFixtureValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if err := DefaultFixture().Validate(); err != nil {
|
|
t.Fatalf("default fixture should validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMarketServersEndpoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := DefaultServer()
|
|
defer srv.Close()
|
|
|
|
resp := mustDoRequest(t, http.MethodGet, srv.MarketURL(), nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
|
}
|
|
|
|
var payload struct {
|
|
Metadata struct {
|
|
Count int `json:"count"`
|
|
NextCursor string `json:"nextCursor"`
|
|
} `json:"metadata"`
|
|
Servers []struct {
|
|
Server struct {
|
|
SchemaURI string `json:"$schema"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Remotes []struct {
|
|
Type string `json:"type"`
|
|
URL string `json:"url"`
|
|
} `json:"remotes"`
|
|
} `json:"server"`
|
|
Meta struct {
|
|
Metadata market.RegistryMetadata `json:"metadata"`
|
|
CLI market.CLIOverlay `json:"cli"`
|
|
} `json:"_meta"`
|
|
} `json:"servers"`
|
|
}
|
|
decodeJSON(t, resp.Body, &payload)
|
|
|
|
if payload.Metadata.Count != 2 {
|
|
t.Fatalf("expected 2 servers, got %d", payload.Metadata.Count)
|
|
}
|
|
if len(payload.Servers) != 2 {
|
|
t.Fatalf("expected 2 servers, got %d", len(payload.Servers))
|
|
}
|
|
|
|
doc := payload.Servers[0]
|
|
if doc.Meta.Metadata.MCPID != 9629 {
|
|
t.Fatalf("expected doc mcp id 9629, got %d", doc.Meta.Metadata.MCPID)
|
|
}
|
|
if got, want := doc.Meta.Metadata.DetailURL, srv.DetailURL(9629); got != want {
|
|
t.Fatalf("unexpected detail url: got %q want %q", got, want)
|
|
}
|
|
if doc.Meta.CLI.ID != "doc" || doc.Meta.CLI.Hidden {
|
|
t.Fatalf("expected visible doc cli overlay, got %+v", doc.Meta.CLI)
|
|
}
|
|
if doc.Server.Remotes[0].URL != srv.RemoteURL("/server/doc") {
|
|
t.Fatalf("unexpected remote url: %q", doc.Server.Remotes[0].URL)
|
|
}
|
|
|
|
legacy := payload.Servers[1]
|
|
if legacy.Meta.Metadata.Lifecycle.DeprecatedBy != 9629 {
|
|
t.Fatalf("expected legacy lifecycle deprecatedBy 9629, got %d", legacy.Meta.Metadata.Lifecycle.DeprecatedBy)
|
|
}
|
|
if !legacy.Meta.CLI.Hidden {
|
|
t.Fatalf("expected legacy CLI overlay to be hidden")
|
|
}
|
|
}
|
|
|
|
func TestMarketDetailEndpoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := DefaultServer()
|
|
defer srv.Close()
|
|
|
|
resp := mustDoRequest(t, http.MethodGet, srv.DetailURL(9629), nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
|
}
|
|
|
|
var payload market.DetailResponse
|
|
decodeJSON(t, resp.Body, &payload)
|
|
|
|
if !payload.Success {
|
|
t.Fatalf("expected success detail response")
|
|
}
|
|
if payload.Result.MCPID != 9629 {
|
|
t.Fatalf("unexpected mcp id: %d", payload.Result.MCPID)
|
|
}
|
|
if len(payload.Result.Tools) != 2 {
|
|
t.Fatalf("expected 2 tools, got %d", len(payload.Result.Tools))
|
|
}
|
|
if payload.Result.Tools[0].ActionVersion != "G-ACT-VER-100" {
|
|
t.Fatalf("unexpected action version: %q", payload.Result.Tools[0].ActionVersion)
|
|
}
|
|
if !strings.Contains(payload.Result.Tools[0].ToolRequest, "\"keyword\"") {
|
|
t.Fatalf("expected tool request schema to include keyword")
|
|
}
|
|
}
|
|
|
|
func TestJSONRPCLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := DefaultServer()
|
|
defer srv.Close()
|
|
|
|
initResp := mustDoRequest(t, http.MethodPost, srv.RemoteURL("/server/doc"), strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"dws","version":"0.0.0-dev"}}}`))
|
|
defer initResp.Body.Close()
|
|
|
|
if initResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("unexpected initialize status: %d", initResp.StatusCode)
|
|
}
|
|
|
|
var initPayload struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result struct {
|
|
ProtocolVersion string `json:"protocolVersion"`
|
|
Capabilities map[string]any `json:"capabilities"`
|
|
ServerInfo map[string]any `json:"serverInfo"`
|
|
} `json:"result"`
|
|
}
|
|
decodeJSON(t, initResp.Body, &initPayload)
|
|
|
|
if initPayload.Result.ProtocolVersion != DefaultProtocolVer {
|
|
t.Fatalf("unexpected protocol version: %q", initPayload.Result.ProtocolVersion)
|
|
}
|
|
if got := initPayload.Result.ServerInfo["name"]; got != "doc" {
|
|
t.Fatalf("unexpected server info name: %v", got)
|
|
}
|
|
|
|
notifyResp := mustDoRequest(t, http.MethodPost, srv.RemoteURL("/server/doc"), strings.NewReader(`{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}`))
|
|
defer notifyResp.Body.Close()
|
|
if notifyResp.StatusCode != http.StatusNoContent {
|
|
t.Fatalf("unexpected notify status: %d", notifyResp.StatusCode)
|
|
}
|
|
|
|
listResp := mustDoRequest(t, http.MethodPost, srv.RemoteURL("/server/doc"), strings.NewReader(`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`))
|
|
defer listResp.Body.Close()
|
|
|
|
var listPayload struct {
|
|
Result struct {
|
|
Tools []struct {
|
|
Name string `json:"name"`
|
|
} `json:"tools"`
|
|
} `json:"result"`
|
|
}
|
|
decodeJSON(t, listResp.Body, &listPayload)
|
|
if len(listPayload.Result.Tools) != 2 {
|
|
t.Fatalf("expected 2 tools, got %d", len(listPayload.Result.Tools))
|
|
}
|
|
if listPayload.Result.Tools[0].Name != "search_documents" {
|
|
t.Fatalf("unexpected first tool name: %q", listPayload.Result.Tools[0].Name)
|
|
}
|
|
|
|
callResp := mustDoRequest(t, http.MethodPost, srv.RemoteURL("/server/doc"), strings.NewReader(`{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_documents","arguments":{"keyword":"plan"}}}`))
|
|
defer callResp.Body.Close()
|
|
|
|
var callPayload struct {
|
|
Result struct {
|
|
Content map[string]any `json:"content"`
|
|
} `json:"result"`
|
|
}
|
|
decodeJSON(t, callResp.Body, &callPayload)
|
|
if items, ok := callPayload.Result.Content["items"].([]any); !ok || len(items) != 1 {
|
|
t.Fatalf("unexpected call result content: %#v", callPayload.Result.Content)
|
|
}
|
|
|
|
createResp := mustDoRequest(t, http.MethodPost, srv.RemoteURL("/server/doc"), strings.NewReader(`{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"create_document","arguments":{"title":"hello"}}}`))
|
|
defer createResp.Body.Close()
|
|
|
|
var createPayload struct {
|
|
Result struct {
|
|
Content map[string]any `json:"content"`
|
|
} `json:"result"`
|
|
}
|
|
decodeJSON(t, createResp.Body, &createPayload)
|
|
if got := createPayload.Result.Content["documentId"]; got != "doc-123" {
|
|
t.Fatalf("unexpected create result: %#v", createPayload.Result.Content)
|
|
}
|
|
}
|
|
|
|
func mustDoRequest(t *testing.T, method, url string, body io.Reader) *http.Response {
|
|
t.Helper()
|
|
|
|
req, err := http.NewRequest(method, url, body)
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("do request: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func decodeJSON(t *testing.T, r io.Reader, out any) {
|
|
t.Helper()
|
|
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatalf("read json: %v", err)
|
|
}
|
|
if err := json.Unmarshal(data, out); err != nil {
|
|
t.Fatalf("unmarshal json: %v\npayload: %s", err, string(data))
|
|
}
|
|
}
|