feat(api): validate openapi parameter values

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:27:25 +00:00
parent 2fb2c6939f
commit 152645489b
2 changed files with 142 additions and 1 deletions

View file

@ -53,6 +53,7 @@ type openAPIParameter struct {
name string
in string
required bool
schema map[string]any
}
// OpenAPIClientOption configures a runtime OpenAPI client.
@ -358,6 +359,9 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st
if err := validateRequiredParameters(op, params, pathKeys); err != nil {
return "", err
}
if err := validateParameterValues(op, params); err != nil {
return "", err
}
for _, key := range pathKeys {
if value, ok := pathValues[key]; ok {
@ -583,7 +587,8 @@ func parseOperationParameters(operation map[string]any) []openAPIParameter {
continue
}
required, _ := param["required"].(bool)
params = append(params, openAPIParameter{name: name, in: in, required: required})
schema, _ := param["schema"].(map[string]any)
params = append(params, openAPIParameter{name: name, in: in, required: required, schema: schema})
}
return params
@ -598,6 +603,49 @@ func operationParameterLocation(op openAPIOperation, name string) string {
return ""
}
func validateParameterValues(op openAPIOperation, params map[string]any) error {
pathKeys := pathParameterNames(op.pathTemplate)
for _, param := range op.parameters {
if len(param.schema) == 0 {
continue
}
if containsString(pathKeys, param.name) {
continue
}
if nested, ok := nestedMap(params, param.in); ok {
if value, exists := nested[param.name]; exists {
if err := validateParameterValue(param, value); err != nil {
return err
}
continue
}
}
if value, exists := params[param.name]; exists {
if err := validateParameterValue(param, value); err != nil {
return err
}
}
}
return nil
}
func validateParameterValue(param openAPIParameter, value any) error {
if value == nil {
return nil
}
data, err := json.Marshal(value)
if err != nil {
return coreerr.E("OpenAPIClient.validateParameterValue", fmt.Sprintf("marshal %s parameter %q", param.in, param.name), err)
}
if err := validateOpenAPISchema(data, param.schema, fmt.Sprintf("%s parameter %q", param.in, param.name)); err != nil {
return err
}
return nil
}
func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error {
for _, param := range op.parameters {
if !param.required {

View file

@ -428,6 +428,99 @@ paths:
}
}
func TestOpenAPIClient_Bad_ValidatesQueryParameterAgainstSchema(t *testing.T) {
called := make(chan struct{}, 1)
mux := http.NewServeMux()
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
called <- struct{}{}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"ok":true}}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
specPath := writeTempSpec(t, `openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/search:
get:
operationId: search_items
parameters:
- name: page
in: query
schema:
type: integer
`)
client := api.NewOpenAPIClient(
api.WithSpec(specPath),
api.WithBaseURL(srv.URL),
)
if _, err := client.Call("search_items", map[string]any{
"page": "two",
}); err == nil {
t.Fatal("expected query parameter validation error, got nil")
}
select {
case <-called:
t.Fatal("expected validation to fail before the HTTP call")
default:
}
}
func TestOpenAPIClient_Bad_ValidatesPathParameterAgainstSchema(t *testing.T) {
called := make(chan struct{}, 1)
mux := http.NewServeMux()
mux.HandleFunc("/users/123", func(w http.ResponseWriter, r *http.Request) {
called <- struct{}{}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"ok":true}}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
specPath := writeTempSpec(t, `openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/users/{id}:
get:
operationId: get_user
parameters:
- name: id
in: path
required: true
schema:
type: integer
`)
client := api.NewOpenAPIClient(
api.WithSpec(specPath),
api.WithBaseURL(srv.URL),
)
if _, err := client.Call("get_user", map[string]any{
"path": map[string]any{
"id": "abc",
},
}); err == nil {
t.Fatal("expected path parameter validation error, got nil")
}
select {
case <-called:
t.Fatal("expected validation to fail before the HTTP call")
default:
}
}
func TestOpenAPIClient_Good_UsesHeaderAndCookieParameters(t *testing.T) {
errCh := make(chan error, 1)
mux := http.NewServeMux()