feat(api): validate required openapi parameters
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
1f43f019b1
commit
f67e3fe5de
2 changed files with 86 additions and 3 deletions
45
client.go
45
client.go
|
|
@ -50,8 +50,9 @@ type openAPIOperation struct {
|
|||
}
|
||||
|
||||
type openAPIParameter struct {
|
||||
name string
|
||||
in string
|
||||
name string
|
||||
in string
|
||||
required bool
|
||||
}
|
||||
|
||||
// OpenAPIClientOption configures a runtime OpenAPI client.
|
||||
|
|
@ -349,6 +350,11 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st
|
|||
} else {
|
||||
pathValues = params
|
||||
}
|
||||
|
||||
if err := validateRequiredParameters(op, params, pathKeys); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, key := range pathKeys {
|
||||
if value, ok := pathValues[key]; ok {
|
||||
placeholder := "{" + key + "}"
|
||||
|
|
@ -572,7 +578,8 @@ func parseOperationParameters(operation map[string]any) []openAPIParameter {
|
|||
if name == "" || in == "" {
|
||||
continue
|
||||
}
|
||||
params = append(params, openAPIParameter{name: name, in: in})
|
||||
required, _ := param["required"].(bool)
|
||||
params = append(params, openAPIParameter{name: name, in: in, required: required})
|
||||
}
|
||||
|
||||
return params
|
||||
|
|
@ -587,6 +594,38 @@ func operationParameterLocation(op openAPIOperation, name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error {
|
||||
for _, param := range op.parameters {
|
||||
if !param.required {
|
||||
continue
|
||||
}
|
||||
if containsString(pathKeys, param.name) {
|
||||
continue
|
||||
}
|
||||
if parameterProvided(params, param.name, param.in) {
|
||||
continue
|
||||
}
|
||||
return coreerr.E("OpenAPIClient.buildURL", fmt.Sprintf("missing required %s parameter %q", param.in, param.name), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parameterProvided(params map[string]any, name, location string) bool {
|
||||
if nested, ok := nestedMap(params, location); ok {
|
||||
if _, exists := nested[name]; exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if value, exists := params[name]; exists {
|
||||
if value != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func encodeJSONBody(v any) ([]byte, error) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -384,6 +384,50 @@ paths:
|
|||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIClient_Bad_MissingRequiredQueryParameter(t *testing.T) {
|
||||
called := make(chan struct{}, 1)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/submit", 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:
|
||||
/submit:
|
||||
post:
|
||||
operationId: submit_item
|
||||
parameters:
|
||||
- name: verbose
|
||||
in: query
|
||||
required: true
|
||||
`)
|
||||
|
||||
client := api.NewOpenAPIClient(
|
||||
api.WithSpec(specPath),
|
||||
api.WithBaseURL(srv.URL),
|
||||
)
|
||||
|
||||
if _, err := client.Call("submit_item", map[string]any{
|
||||
"name": "Ada",
|
||||
}); err == nil {
|
||||
t.Fatal("expected required query 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