From 4e06303620bc0515b7f5a8df8ccb7378fd60aea4 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Thu, 21 May 2026 13:43:23 +0200 Subject: [PATCH 1/5] fix(mcp): fix fake function if schema from Kafka message config is provided --- js/faker/json.go | 22 +++++--- mcp/run_fake_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++ mcp/run_kafka_test.go | 2 +- 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 mcp/run_fake_test.go diff --git a/js/faker/json.go b/js/faker/json.go index ed195fe23..e113c2394 100644 --- a/js/faker/json.go +++ b/js/faker/json.go @@ -11,10 +11,16 @@ import ( ) func ToJsonSchema(v goja.Value, rt *goja.Runtime) (*jsonSchema.Schema, error) { - s := &jsonSchema.Schema{} + if v == goja.Undefined() { + return nil, nil + } + + if s, ok := v.Export().(*jsonSchema.Schema); ok { + return s, nil + } switch v.ExportType().Kind() { - case reflect.Map: + case reflect.Map, reflect.Struct, reflect.Ptr: break case reflect.Bool: b := v.ToBoolean() @@ -23,22 +29,24 @@ func ToJsonSchema(v goja.Value, rt *goja.Runtime) (*jsonSchema.Schema, error) { return nil, fmt.Errorf("expect JSON schema but got: %v", util.JsType(v.Export())) } + s := &jsonSchema.Schema{} obj := v.ToObject(rt) for _, k := range obj.Keys() { switch k { case "type": i := obj.Get(k).Export() - if arr, ok := i.([]interface{}); ok { - for _, t := range arr { + switch vv := i.(type) { + case string: + s.Type = []string{vv} + case []any: + for _, t := range vv { tn, ok := t.(string) if !ok { return nil, fmt.Errorf("unexpected type for 'type': %v", util.JsType(t)) } s.Type = append(s.Type, tn) } - } else if t, ok := i.(string); ok { - s.Type = []string{t} - } else { + default: return nil, fmt.Errorf("unexpected type for 'type': %v", util.JsType(i)) } case "enum": diff --git a/mcp/run_fake_test.go b/mcp/run_fake_test.go new file mode 100644 index 000000000..2d95f45ba --- /dev/null +++ b/mcp/run_fake_test.go @@ -0,0 +1,116 @@ +package mcp_test + +import ( + "context" + "mokapi/mcp" + "mokapi/providers/asyncapi3" + "mokapi/providers/asyncapi3/asyncapi3test" + "mokapi/runtime" + "mokapi/runtime/runtimetest" + "mokapi/schema/json/generator" + "mokapi/schema/json/schema/schematest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService_Run_Fake(t *testing.T) { + testcases := []struct { + name string + app *runtime.App + test func(t *testing.T, s *mcp.Service) + }{ + { + name: "fake string", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.fake({ type: 'string' })`, + }, + ) + require.NoError(t, err) + require.Equal(t, "P8", r.Result) + }, + }, + { + name: "fake object", + app: runtimetest.NewApp(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `mokapi.fake({ type: 'object', properties: { foo: { type: 'string' } } })`, + }, + ) + require.NoError(t, err) + require.Equal(t, map[string]interface{}{"foo": "P8"}, r.Result) + }, + }, + { + name: "fake from message payload", + app: func() *runtime.App { + msg := asyncapi3test.NewMessage( + asyncapi3test.WithMessageName("msg-name-1"), + asyncapi3test.WithMessageTitle("msg-title-1"), + asyncapi3test.WithMessageSummary("msg-summary-1"), + asyncapi3test.WithMessageDescription("msg-description-1"), + asyncapi3test.WithContentType("application/json"), + asyncapi3test.WithPayload( + schematest.New("object", + schematest.WithProperty("foo", schematest.New("string")), + ), + ), + ) + + ch := asyncapi3test.NewChannel( + asyncapi3test.WithChannelTitle("title-1"), + asyncapi3test.WithChannelSummary("channel-1 summary"), + asyncapi3test.WithChannelDescription("description"), + asyncapi3test.UseMessage("foo", &asyncapi3.MessageRef{Value: msg}), + ) + + return runtimetest.NewKafkaApp( + asyncapi3test.NewConfig( + asyncapi3test.WithInfo("foo", "", ""), + asyncapi3test.AddChannel("channel-1", ch), + asyncapi3test.WithOperation("publish", + asyncapi3test.WithOperationAction("send"), + asyncapi3test.WithOperationTitle("op-title-1"), + asyncapi3test.WithOperationSummary("op-summary-1"), + asyncapi3test.WithOperationDescription("op-description-1"), + asyncapi3test.WithOperationChannel(ch), + ), + ), + ) + }(), + test: func(t *testing.T, s *mcp.Service) { + r, err := s.GetRunResponse( + context.Background(), + mcp.RunInput{ + Code: `const topic = mokapi.getApi('foo').getTopic('channel-1') +const operation = topic.operations.find(x => x.action === 'send') +mokapi.fake(operation.messages[0].payload) +`, + }, + ) + require.NoError(t, err) + require.IsType(t, map[string]any{}, r.Result) + data := r.Result.(map[string]any) + require.Equal(t, map[string]any{ + "foo": "P8", + }, data) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + generator.Seed(123456) + + s := mcp.NewService(tc.app) + tc.test(t, s) + }) + } +} diff --git a/mcp/run_kafka_test.go b/mcp/run_kafka_test.go index fc1b67230..06a298066 100644 --- a/mcp/run_kafka_test.go +++ b/mcp/run_kafka_test.go @@ -186,7 +186,7 @@ func TestService_Run_Kafka(t *testing.T) { }, }, { - name: "get topic operation does not define any message", + name: "get topic operation not define any message", app: func() *runtime.App { msg := asyncapi3test.NewMessage( asyncapi3test.WithMessageName("msg-name-1"), From e4e0010bbc04828ba7db992c13b48c1f8ac029da Mon Sep 17 00:00:00 2001 From: marle3003 Date: Thu, 21 May 2026 13:55:51 +0200 Subject: [PATCH 2/5] fix(search): add again search paginator --- webui/src/components/dashboard/Search.vue | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/webui/src/components/dashboard/Search.vue b/webui/src/components/dashboard/Search.vue index bcadfb0d2..4661bf430 100644 --- a/webui/src/components/dashboard/Search.vue +++ b/webui/src/components/dashboard/Search.vue @@ -397,6 +397,28 @@ function facetTitle(s: string) { {{ errorMessage }} +
+
+ +
+
+ From d221d9e31d1d7604d1a37a51a7444688789363fa Mon Sep 17 00:00:00 2001 From: marle3003 <56543258+marle3003@users.noreply.github.com> Date: Thu, 21 May 2026 14:31:40 +0200 Subject: [PATCH 3/5] chore: undo not needed code --- js/faker/json.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/faker/json.go b/js/faker/json.go index e113c2394..3537d8161 100644 --- a/js/faker/json.go +++ b/js/faker/json.go @@ -20,7 +20,7 @@ func ToJsonSchema(v goja.Value, rt *goja.Runtime) (*jsonSchema.Schema, error) { } switch v.ExportType().Kind() { - case reflect.Map, reflect.Struct, reflect.Ptr: + case reflect.Map: break case reflect.Bool: b := v.ToBoolean() From 302d6d1ad040159b48ea079f5d0ffaa9a2dc9885 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Thu, 21 May 2026 18:27:44 +0200 Subject: [PATCH 4/5] fix(search): index operation for every status code refactor(webui): refactor search views --- acceptance/petstore_test.go | 6 +- providers/mail/log.go | 9 +- runtime/runtime_http_search.go | 2 +- runtime/runtime_http_search_test.go | 59 +++++++++++++ runtime/runtime_mail_search_test.go | 3 +- webui/e2e/mocks/http_handler.js | 2 +- webui/src/components/dashboard/Search.vue | 83 +++---------------- .../components/dashboard/search/Config.vue | 8 +- .../src/components/dashboard/search/Event.vue | 21 ++++- .../src/components/dashboard/search/Http.vue | 18 +++- .../src/components/dashboard/search/Kafka.vue | 11 ++- .../src/components/dashboard/search/Ldap.vue | 8 +- .../src/components/dashboard/search/Mail.vue | 11 ++- 13 files changed, 141 insertions(+), 100 deletions(-) diff --git a/acceptance/petstore_test.go b/acceptance/petstore_test.go index a98b45f20..5e0805d0f 100644 --- a/acceptance/petstore_test.go +++ b/acceptance/petstore_test.go @@ -315,7 +315,7 @@ func (suite *PetStoreSuite) TestSearch_Paging() { assert.NoError(t, err) assert.NotNil(t, data) - assert.Equal(t, float64(35), data["total"]) + assert.Equal(t, float64(51), data["total"]) items := data["results"].([]any) assert.Len(t, items, 10) @@ -335,13 +335,13 @@ func (suite *PetStoreSuite) TestSearch_Paging() { assert.NoError(t, err) assert.NotNil(t, data) - assert.Equal(t, float64(35), data["total"]) + assert.Equal(t, float64(51), data["total"]) items := data["results"].([]any) assert.Len(t, items, 10) evt := items[0].(map[string]interface{}) assert.Equal(t, "HTTP", evt["type"]) - assert.Equal(t, "/pet/{petId}", evt["title"]) + assert.Equal(t, "/pet/{petId}/uploadImage", evt["title"]) assert.Equal(t, "Swagger Petstore", evt["domain"]) }), ) diff --git a/providers/mail/log.go b/providers/mail/log.go index b636523f4..8a8fa07a3 100644 --- a/providers/mail/log.go +++ b/providers/mail/log.go @@ -48,10 +48,11 @@ func (l *Log) Title() string { func (l *Log) IndexFields() map[string]any { m := map[string]any{ - "from": l.From, - "to": l.To, - "messageId": l.MessageId, - "subject": l.Subject, + "from": l.From, + "to": l.To, + "messageId": l.MessageId, + "subject": l.Subject, + "metadata.messageId": l.MessageId, } return m } diff --git a/runtime/runtime_http_search.go b/runtime/runtime_http_search.go index 4ab156ed0..e0ce456c9 100644 --- a/runtime/runtime_http_search.go +++ b/runtime/runtime_http_search.go @@ -193,7 +193,7 @@ func (s *HttpStore) addToIndex(cfg *openapi.Config) { Responses: responses, } - s.index.Add(id, opData) + s.index.Add(fmt.Sprintf("%s_%d", id, statusCode), opData) } } else { opData := httpOperationSearchIndexData{ diff --git a/runtime/runtime_http_search_test.go b/runtime/runtime_http_search_test.go index bcab2cfd4..219aa9a8c 100644 --- a/runtime/runtime_http_search_test.go +++ b/runtime/runtime_http_search_test.go @@ -348,6 +348,65 @@ func TestIndex_Http(t *testing.T) { r.Results[0]) }, }, + { + name: "Search operation with multiple status", + test: func(t *testing.T, app *runtime.App) { + cfg := openapitest.NewConfig("3.0", + openapitest.WithInfo("foo", "1.0", "a description"), + openapitest.WithPath("/pets", + openapitest.WithPathInfo("", "a description"), + openapitest.WithOperation(http.MethodGet, + openapitest.WithOperationInfo("Summary value", "Description value", "", false), + openapitest.WithHeaderParam("foo", true, openapitest.WithParamInfo("parameter description")), + openapitest.WithResponse(http.StatusOK), + openapitest.WithResponse(http.StatusForbidden), + ), + ), + ) + app.Http.Add(toConfig(cfg)) + + var r search.Result + var err error + waitSearchIndex(t, func() bool { + r, err = app.Search(search.Request{QueryText: "method:get", Limit: 10}) + require.NoError(t, err) + return len(r.Results) == 2 + }) + require.Len(t, r.Results, 2) + require.Contains(t, + r.Results, + search.ResultItem{ + Type: "HTTP", + Domain: "foo", + Title: "/pets", + Description: `Summary value Description value`, + Fragments: []string{"GET"}, + Params: map[string]string{ + "type": "http", + "service": "foo", + "path": "/pets", + "method": "GET", + "statusCode": "200", + }, + }) + require.Contains(t, + r.Results, + search.ResultItem{ + Type: "HTTP", + Domain: "foo", + Title: "/pets", + Description: `Summary value Description value`, + Fragments: []string{"GET"}, + Params: map[string]string{ + "type": "http", + "service": "foo", + "path": "/pets", + "method": "GET", + "statusCode": "403", + }, + }) + }, + }, { name: "Search by api field", test: func(t *testing.T, app *runtime.App) { diff --git a/runtime/runtime_mail_search_test.go b/runtime/runtime_mail_search_test.go index 9e155fe84..2a42ec802 100644 --- a/runtime/runtime_mail_search_test.go +++ b/runtime/runtime_mail_search_test.go @@ -207,11 +207,12 @@ func TestIndex_Mail_Event(t *testing.T) { require.Len(t, r.Results[0].Fragments, 2) require.Contains(t, r.Results[0].Fragments, "Test Mail") require.Contains(t, r.Results[0].Fragments, "event") - require.Len(t, r.Results[0].Params, 4) + require.Len(t, r.Results[0].Params, 5) require.Equal(t, "event", r.Results[0].Params["type"]) require.Equal(t, "mail", r.Results[0].Params["traits.namespace"]) require.Equal(t, "Test Mail Events", r.Results[0].Params["traits.name"]) require.Contains(t, r.Results[0].Params, "id") + require.Contains(t, r.Results[0].Params, "messageId") require.NotEmpty(t, r.Results[0].Time) }, }, diff --git a/webui/e2e/mocks/http_handler.js b/webui/e2e/mocks/http_handler.js index d6a73af2e..02eed5194 100644 --- a/webui/e2e/mocks/http_handler.js +++ b/webui/e2e/mocks/http_handler.js @@ -864,7 +864,7 @@ function getSearchResults() { type: 'event', 'traits.namespace': 'mail', 'traits.name': 'Mail Testserver', - id: '273cd167-f5a5-4da1-969e-d44213686491' + messageId: '20230223-084925.763-4196@mokapi.io' } }, { diff --git a/webui/src/components/dashboard/Search.vue b/webui/src/components/dashboard/Search.vue index 4661bf430..4e8ba32fa 100644 --- a/webui/src/components/dashboard/Search.vue +++ b/webui/src/components/dashboard/Search.vue @@ -55,68 +55,6 @@ watch(queryText, async () => { timeout = setTimeout(async () => { await search() }, 1000) }) -async function navigateToSearchResult(result: any) { - switch (result.type.toLowerCase()) { - case 'http': - if (result.params.path) { - const endpoint = result.params.path.split('/') - endpoint.shift() // path starts with a slash: remove first empty entry - if (result.params.method) { - endpoint.push(result.params.method) - } - return router.push({ name: 'httpEndpoint', params: { endpoint, ...result.params } }) - } - else { - return router.push({ name: 'httpService', params: result.params }) - } - case 'config': - return router.push({ name: 'config', params: result.params }) - case 'kafka': - if (result.params.topic) { - return router.push({ name: 'kafkaTopic', params: result.params }) - } - return router.push({ name: 'kafkaService', params: result.params }) - case 'mail': - if (result.params.mailbox) { - return router.push({ name: 'smtpMailbox', params: { ...{ name: result.params.mailbox }, ...result.params } }) - } - return router.push({ name: 'mailService', params: result.params }) - case 'ldap': - return router.push({ name: 'ldapService', params: result.params }) - case 'event': - switch (result.params['traits.namespace']) { - case 'http': - return router.push({ name: 'httpRequest', params: result.params }) - case 'kafka': - return router.push({ name: 'kafkaMessage', params: result.params }) - case 'mail': - const res = await fetch(transformPath(`/api/events/${result.params.id}`)); - const event: ServiceEvent = await res.json(); - const data = event.data as SmtpEventData - return router.push({ name: 'smtpMail', params: Object.assign(result.params, { id: data.messageId }) }) - case 'ldap': - return router.push({ name: 'ldapRequest', params: result.params }) - } - } - console.error(`search result type '${result.type.toLowerCase()}' not supported for navigation`) -} -function title(result: SearchItem) { - switch (result.type) { - case "Config": - const n = result.title.length - if (n > 55) { - return '...' + result.title.slice(n - 55) - } - break - case "Event": - if (result.params.namespace === 'kafka') { - return `Key: ${result.title}` - } - break - } - return result.title -} - onMounted(async () => { for (const param in route.query) { if (param === 'q' || param === 'index') { @@ -195,15 +133,15 @@ function pageIndex_click(index: number) { async function search() { loading.start() - let path = `/api/search/query?q=${queryText.value}` + let path = `/api/search/query?q=${encodeURIComponent(queryText.value)}` if (pageIndex.value !== 0) { path += `&index=${pageIndex.value}` } for (const facetName in facets.value) { const v = facets.value[facetName] - if (v !== '') { - path += `&${facetName}=${v}` + if (v) { + path += `&${facetName}=${encodeURIComponent(v)}` } } @@ -362,19 +300,20 @@ function facetTitle(s: string) {
-
+
- + - + - + - + - + + +
Search Result Item not defined: {{ item.params.type }}
diff --git a/webui/src/components/dashboard/search/Config.vue b/webui/src/components/dashboard/search/Config.vue index ccd7f47cb..6b1b7febf 100644 --- a/webui/src/components/dashboard/search/Config.vue +++ b/webui/src/components/dashboard/search/Config.vue @@ -1,5 +1,4 @@