You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
memos/server/router/mcp/openapi_test.go

193 lines
5.2 KiB
Go

package mcp
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestLoadOpenAPIOperationsIncludesCuratedIDs(t *testing.T) {
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
require.NoError(t, err)
registry, err := buildOperationRegistry(spec)
require.NoError(t, err)
curatedIDs := []string{
"MemoService_ListMemos",
"MemoService_CreateMemo",
"MemoService_GetMemo",
"MemoService_UpdateMemo",
"MemoService_DeleteMemo",
"MemoService_ListMemoComments",
"MemoService_CreateMemoComment",
"MemoService_ListMemoAttachments",
"MemoService_SetMemoAttachments",
"MemoService_ListMemoReactions",
"MemoService_UpsertMemoReaction",
"MemoService_DeleteMemoReaction",
"MemoService_ListMemoRelations",
"MemoService_SetMemoRelations",
"AttachmentService_ListAttachments",
"AttachmentService_GetAttachment",
"AttachmentService_DeleteAttachment",
}
for _, operationID := range curatedIDs {
operation, ok := registry[operationID]
require.True(t, ok, "missing curated operation %s", operationID)
require.NotEmpty(t, operation.Method, operationID)
require.NotEmpty(t, operation.Path, operationID)
require.NotEmpty(t, operation.Description, operationID)
require.NotNil(t, operation.ResponseSchema, operationID)
}
createMemo := registry["MemoService_CreateMemo"]
require.NotNil(t, createMemo.RequestBodySchema)
require.Equal(t, "object", createMemo.RequestBodySchema["type"])
}
func TestBuildOperationRegistryRejectsDuplicateOperationIDs(t *testing.T) {
spec := &openAPISpec{
Paths: map[string]map[string]*openAPIOperation{
"/a": {
"get": {OperationID: "MemoService_GetMemo", Description: "first"},
},
"/b": {
"get": {OperationID: "MemoService_GetMemo", Description: "second"},
},
},
}
_, err := buildOperationRegistry(spec)
require.ErrorContains(t, err, "duplicate OpenAPI operationId")
}
func TestResolveSchemaRef(t *testing.T) {
spec := &openAPISpec{
Components: openAPIComponents{
Schemas: map[string]jsonSchema{
"ListMemosResponse": {
"type": "object",
"properties": map[string]any{
"memos": map[string]any{"type": "array"},
},
},
},
},
}
schema, err := resolveSchemaRef(spec, jsonSchema{"$ref": "#/components/schemas/ListMemosResponse"})
require.NoError(t, err)
require.Equal(t, "object", schema["type"])
require.Contains(t, schema["properties"], "memos")
}
func TestResolveSchemaRefRewritesNestedComponentRefs(t *testing.T) {
spec := &openAPISpec{
Components: openAPIComponents{
Schemas: map[string]jsonSchema{
"ListMemosResponse": {
"type": "object",
"properties": map[string]any{
"memos": map[string]any{
"type": "array",
"items": map[string]any{"$ref": "#/components/schemas/Memo"},
},
},
},
"Memo": {
"type": "object",
"properties": map[string]any{
"attachment": map[string]any{"$ref": "#/components/schemas/Attachment"},
},
},
"Attachment": {
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
},
},
},
},
}
schema, err := resolveSchemaRef(spec, jsonSchema{"$ref": "#/components/schemas/ListMemosResponse"})
require.NoError(t, err)
data, err := json.Marshal(schema)
require.NoError(t, err)
require.NotContains(t, string(data), "#/components/schemas")
require.Contains(t, string(data), `"#/$defs/Memo"`)
require.Contains(t, string(data), `"#/$defs/Attachment"`)
}
func TestBuildOperationRegistryResolvesRequestBodySchema(t *testing.T) {
spec := &openAPISpec{
Paths: map[string]map[string]*openAPIOperation{
"/memos": {
"post": {
OperationID: "MemoService_CreateMemo",
RequestBody: &openAPIRequestBody{
Content: map[string]openAPIMediaType{
"application/json": {Schema: jsonSchema{"$ref": "#/components/schemas/CreateMemoRequest"}},
},
},
Responses: map[string]openAPIResponse{
"200": {
Content: map[string]openAPIMediaType{
"application/json": {Schema: jsonSchema{"$ref": "#/components/schemas/Memo"}},
},
},
},
},
},
},
Components: openAPIComponents{
Schemas: map[string]jsonSchema{
"CreateMemoRequest": {
"type": "object",
"properties": map[string]any{
"content": map[string]any{"type": "string"},
},
},
"Memo": {
"type": "object",
},
},
},
}
registry, err := buildOperationRegistry(spec)
require.NoError(t, err)
requestSchema := registry["MemoService_CreateMemo"].RequestBodySchema
require.Equal(t, "object", requestSchema["type"])
require.Contains(t, requestSchema["properties"], "content")
}
func TestBuildOperationRegistryUsesOKSchemaForEmptySuccessResponse(t *testing.T) {
spec := &openAPISpec{
Paths: map[string]map[string]*openAPIOperation{
"/memos/{memo}": {
"delete": {
OperationID: "MemoService_DeleteMemo",
Responses: map[string]openAPIResponse{
"200": {
Content: map[string]openAPIMediaType{},
},
},
},
},
},
}
registry, err := buildOperationRegistry(spec)
require.NoError(t, err)
responseSchema := registry["MemoService_DeleteMemo"].ResponseSchema
require.Equal(t, "object", responseSchema["type"])
require.Contains(t, responseSchema["properties"], "ok")
}