From aba8e6d7c20b7da2a2291c138f297aa374e39fe0 Mon Sep 17 00:00:00 2001 From: bensynapse <118375461+bensynapse@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:31:36 +0300 Subject: [PATCH] Fix deepObject query parameter matching --- providers/openapi/parameter_query.go | 5 +- providers/openapi/parameter_query_test.go | 166 ++++++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/providers/openapi/parameter_query.go b/providers/openapi/parameter_query.go index cb730ecd6..680a59b5a 100644 --- a/providers/openapi/parameter_query.go +++ b/providers/openapi/parameter_query.go @@ -68,12 +68,15 @@ func parseQueryObject(param *Parameter, u *url.URL) (string, interface{}, error) i, err := parseUnExplodeObject(param, raw, ",") return raw, i, err } else if param.Style == "deepObject" && param.IsExplode() { - paramRegex := regexp.MustCompile(fmt.Sprintf(`%v\[(?P.+)\]`, param.Name)) + paramRegex := regexp.MustCompile(fmt.Sprintf(`^%s\[(?P.+)\]$`, regexp.QuoteMeta(param.Name))) obj := map[string]interface{}{} raw := strings.Builder{} for k, values := range u.Query() { match := paramRegex.FindStringSubmatch(k) + if match == nil { + continue + } name := match[1] prop := param.Schema.Properties.Get(name) if prop == nil && !param.Schema.IsFreeForm() && !param.Schema.IsDictionary() { diff --git a/providers/openapi/parameter_query_test.go b/providers/openapi/parameter_query_test.go index eb1be7d0d..3c3f4d624 100644 --- a/providers/openapi/parameter_query_test.go +++ b/providers/openapi/parameter_query_test.go @@ -5,6 +5,7 @@ import ( "mokapi/providers/openapi/schema/schematest" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/stretchr/testify/require" @@ -538,3 +539,168 @@ func TestParseQuery(t *testing.T) { } } + +func TestParseQueryDeepObjectOtherParameters(t *testing.T) { + objectParameter := func(name string) *openapi.Parameter { + return &openapi.Parameter{ + Name: name, + Type: openapi.ParameterQuery, + Schema: schematest.New("object", + schematest.WithProperty("role", schematest.New("string")), + schematest.WithFreeForm(false), + ), + Style: "deepObject", + Explode: new(true), + } + } + defaultParameter := objectParameter("filter") + defaultParameter.Schema.Default = map[string]any{"role": "guest"} + requiredParameter := objectParameter("filter") + requiredParameter.Required = new(true) + + testcases := []struct { + name string + params openapi.Parameters + query url.Values + want map[string]any + wantErr string + }{ + { + name: "undeclared scalar pagination", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{"filter[role]": {"admin"}, "page": {"2"}}, + want: map[string]any{"filter": map[string]any{"role": "admin"}}, + }, + { + name: "declared scalar pagination", + params: openapi.Parameters{ + {Value: objectParameter("filter")}, + {Value: &openapi.Parameter{ + Name: "page", + Type: openapi.ParameterQuery, + Schema: schematest.New("integer"), + Style: "form", + }}, + }, + query: url.Values{"filter[role]": {"admin"}, "page": {"2"}}, + want: map[string]any{"filter": map[string]any{"role": "admin"}, "page": int64(2)}, + }, + { + name: "multiple object parameters", + params: openapi.Parameters{ + {Value: objectParameter("filter")}, + {Value: objectParameter("selection")}, + }, + query: url.Values{"filter[role]": {"admin"}, "selection[role]": {"guest"}}, + want: map[string]any{ + "filter": map[string]any{"role": "admin"}, + "selection": map[string]any{"role": "guest"}, + }, + }, + { + name: "optional object absent beside scalar", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{"page": {"2"}}, + want: map[string]any{"filter": nil}, + }, + { + name: "default object absent beside scalar", + params: openapi.Parameters{{Value: defaultParameter}}, + query: url.Values{"page": {"2"}}, + want: map[string]any{"filter": map[string]any{"role": "guest"}}, + }, + { + name: "required object absent beside scalar", + params: openapi.Parameters{{Value: requiredParameter}}, + query: url.Values{"page": {"2"}}, + wantErr: "parse query parameter 'filter' failed: parameter is required", + }, + { + name: "parameter name prefix lookalike", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{"otherfilter[role]": {"guest"}}, + want: map[string]any{"filter": nil}, + }, + { + name: "parameter name suffix lookalike", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{"filter[role]other": {"guest"}}, + want: map[string]any{"filter": nil}, + }, + { + name: "regexp punctuation is literal", + params: openapi.Parameters{{Value: objectParameter("filter.name")}}, + query: url.Values{"filterXname[role]": {"guest"}}, + want: map[string]any{"filter.name": nil}, + }, + { + name: "lookalikes beside matching object", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{ + "filter[role]": {"admin"}, + "otherfilter[other]": {"guest"}, + "filter[other]tail": {"guest"}, + }, + want: map[string]any{"filter": map[string]any{"role": "admin"}}, + }, + { + name: "matching unknown property is rejected", + params: openapi.Parameters{{Value: objectParameter("filter")}}, + query: url.Values{"filter[unknown]": {"guest"}}, + wantErr: "parse query parameter 'filter' failed: property 'unknown' not defined in schema: schema type=object properties=[role] free-form=false", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, "https://foo.bar?"+tc.query.Encode(), nil) + var result *openapi.RequestParameters + var err error + require.NotPanics(t, func() { + result, err = openapi.FromRequest(tc.params, "", request) + }) + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + return + } + require.NoError(t, err) + require.Len(t, result.Query, len(tc.want)) + for name, value := range tc.want { + require.NotNil(t, result.Query[name]) + require.Equal(t, value, result.Query[name].Value) + } + }) + } +} + +func TestParseQueryDeepObjectParameterNames(t *testing.T) { + for _, name := range []string{ + "filter.name", "filter+name", "filter(name", "filter)name", + "filter{name", "filter}name", + "filter|name", "filter*name", "filter?name", "filter^name", + "filter$name", `filter\name`, + } { + t.Run(name, func(t *testing.T) { + params := openapi.Parameters{{Value: &openapi.Parameter{ + Name: name, + Type: openapi.ParameterQuery, + Schema: schematest.New("object", + schematest.WithProperty("role", schematest.New("string")), + schematest.WithFreeForm(false), + ), + Style: "deepObject", + Explode: new(true), + }}} + query := url.Values{name + "[role]": {"admin"}} + request := httptest.NewRequest(http.MethodGet, "https://foo.bar?"+query.Encode(), nil) + var result *openapi.RequestParameters + var err error + require.NotPanics(t, func() { + result, err = openapi.FromRequest(params, "", request) + }) + require.NoError(t, err) + require.Equal(t, map[string]any{"role": "admin"}, result.Query[name].Value) + require.Equal(t, name+"[role]=admin", *result.Query[name].Raw) + }) + } +}