feat(api): validate openapi parameter values
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2fb2c6939f
commit
152645489b
2 changed files with 142 additions and 1 deletions
50
client.go
50
client.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue