Skip to content

Commit ca3e591

Browse files
http/probe: apispec: generate dummy param data and request bodies (#148)
* http/probe: apispec: generate dummy param data and request bodies from schemas Generate realistic requests from OpenAPI/Swagger specs during HTTP probing. Populate path, query, header and body params using schema info so the probe exercises more code paths. Changes: - Substitute path params based on schema types (with brace-strip fallback) - Build query strings and headers from parameter schemas (JSON-stringify object-typed params) - Create JSON request bodies from requestBody schemas and set Content-Type - Basic multipart/form-data support (pick first field, retry with image/JSON on 500) - Merge path-level and op-level params with op-level override - Pass a headers map into requests; remove duplicate param collection work - Add tests for path substitution, query/header generation (object/array), param string generation, and param merging This improves probe coverage without changing existing flags or behavior for non-API-spec probes. Fixes: #73 Link: #73 Signed-off-by: Artem Tkachuk <[email protected]> * PR feedback --------- Signed-off-by: Artem Tkachuk <[email protected]>
1 parent f103a4b commit ca3e591

File tree

2 files changed

+338
-17
lines changed

2 files changed

+338
-17
lines changed

pkg/app/master/probe/http/swagger.go

Lines changed: 160 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"io"
78
"net/http"
@@ -274,6 +275,135 @@ func addPathOp(m *map[string]*openapi3.Operation, op *openapi3.Operation, name s
274275
}
275276
}
276277

278+
// collectParameters merges path-level and operation-level parameters.
279+
// Operation-level parameters override path-level ones with the same (in,name).
280+
func collectParameters(pathItem *openapi3.PathItem, op *openapi3.Operation) []*openapi3.Parameter {
281+
var result []*openapi3.Parameter
282+
// index to handle overrides
283+
key := func(p *openapi3.Parameter) string { return p.In + "\x00" + p.Name }
284+
seen := map[string]bool{}
285+
286+
if pathItem != nil {
287+
for _, pref := range pathItem.Parameters {
288+
if pref == nil || pref.Value == nil {
289+
continue
290+
}
291+
p := pref.Value
292+
result = append(result, p)
293+
seen[key(p)] = true
294+
}
295+
}
296+
297+
if op != nil {
298+
for _, pref := range op.Parameters {
299+
if pref == nil || pref.Value == nil {
300+
continue
301+
}
302+
p := pref.Value
303+
k := key(p)
304+
if seen[k] {
305+
for i := range result {
306+
// override by replacing prior entry
307+
if key(result[i]) == k {
308+
result[i] = p
309+
// OpenAPI params are unique per operation by (in,name). An op-level param
310+
// overrides at most one path-level entry, so replace once and stop.
311+
break
312+
}
313+
}
314+
} else {
315+
result = append(result, p)
316+
seen[k] = true
317+
}
318+
}
319+
}
320+
321+
return result
322+
}
323+
324+
func paramStringForSchema(sref *openapi3.SchemaRef) string {
325+
if sref == nil || sref.Value == nil {
326+
return "x"
327+
}
328+
s := sref.Value
329+
330+
if len(s.Enum) > 0 {
331+
if v, ok := s.Enum[0].(string); ok {
332+
return v
333+
}
334+
return fmt.Sprint(s.Enum[0])
335+
}
336+
337+
switch s.Type {
338+
case "integer", "number":
339+
return "1"
340+
case "boolean":
341+
return "true"
342+
case "array":
343+
return paramStringForSchema(s.Items)
344+
case "object":
345+
return "x"
346+
default:
347+
return "x"
348+
}
349+
}
350+
351+
func substitutePathParams(apiPath string, params []*openapi3.Parameter) string {
352+
if !strings.Contains(apiPath, "{") {
353+
return apiPath
354+
}
355+
356+
for _, p := range params {
357+
if p == nil || p.In != "path" {
358+
continue
359+
}
360+
placeholder := "{" + p.Name + "}"
361+
if strings.Contains(apiPath, placeholder) {
362+
apiPath = strings.ReplaceAll(apiPath, placeholder, url.PathEscape(paramStringForSchema(p.Schema)))
363+
}
364+
}
365+
366+
// fallback: strip any remaining braces
367+
if strings.Contains(apiPath, "{") {
368+
apiPath = strings.ReplaceAll(apiPath, "{", "")
369+
apiPath = strings.ReplaceAll(apiPath, "}", "")
370+
}
371+
372+
return apiPath
373+
}
374+
375+
func buildQueryAndHeaders(params []*openapi3.Parameter) (string, map[string]string) {
376+
var parts []string
377+
headers := make(map[string]string)
378+
for _, p := range params {
379+
if p == nil {
380+
continue
381+
}
382+
383+
getStringValue := func(pref *openapi3.SchemaRef) string {
384+
if pref != nil && pref.Value != nil && pref.Value.Type == "object" {
385+
// generate a small object and JSON-stringify it
386+
obj, _ := genSchemaObject(pref.Value, false)
387+
if data, err := json.Marshal(obj); err == nil {
388+
return string(data)
389+
}
390+
return "{}"
391+
}
392+
return paramStringForSchema(pref)
393+
}
394+
395+
switch p.In {
396+
case "query":
397+
v := getStringValue(p.Schema)
398+
parts = append(parts, url.QueryEscape(p.Name)+"="+url.QueryEscape(v))
399+
case "header":
400+
v := getStringValue(p.Schema)
401+
headers[p.Name] = v
402+
}
403+
}
404+
return strings.Join(parts, "&"), headers
405+
}
406+
277407
func genSchemaObject(schema *openapi3.Schema, minimal bool) (interface{}, bool) {
278408
//todo: also need 'max' as a param to generate as many fields as possible
279409

@@ -330,7 +460,7 @@ func genSchemaObject(schema *openapi3.Schema, minimal bool) (interface{}, bool)
330460
stringVal, _ = schema.Example.(string)
331461
} else if schema.Default != nil {
332462
stringVal, _ = schema.Default.(string)
333-
} else if schema.Enum != nil && len(schema.Enum) > 0 {
463+
} else if len(schema.Enum) > 0 {
334464
stringVal, _ = schema.Enum[0].(string)
335465
}
336466

@@ -393,16 +523,9 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
393523
}
394524

395525
for apiPath, pathInfo := range spec.Paths {
396-
//very primitive way to set the path params (will break for numeric values)
397-
if strings.Contains(apiPath, "{") {
398-
apiPath = strings.ReplaceAll(apiPath, "{", "")
526+
rawRoute := apiPath
527+
// Path param substitution is handled per operation below.
399528

400-
if strings.Contains(apiPath, "}") {
401-
apiPath = strings.ReplaceAll(apiPath, "}", "")
402-
}
403-
}
404-
405-
endpoint := fmt.Sprintf("%s%s%s", addr, prefix, apiPath)
406529
ops := pathOps(pathInfo)
407530
for apiMethod, apiInfo := range ops {
408531
if apiInfo == nil {
@@ -412,6 +535,19 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
412535
continue
413536
}
414537

538+
// Build endpoint for this operation using dummy path/query params
539+
params := collectParameters(pathInfo, apiInfo)
540+
finalPath := substitutePathParams(rawRoute, params)
541+
endpoint := fmt.Sprintf("%s%s%s", addr, prefix, finalPath)
542+
qstr, hdrs := buildQueryAndHeaders(params)
543+
if qstr != "" {
544+
if strings.Contains(endpoint, "?") {
545+
endpoint = endpoint + "&" + qstr
546+
} else {
547+
endpoint = endpoint + "?" + qstr
548+
}
549+
}
550+
415551
var bodyBytes []byte
416552
var contentType string
417553
var formFieldName string
@@ -510,8 +646,8 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
510646
}
511647
}
512648

513-
//make a call (no params for now)
514-
if p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyBytes) {
649+
//make a call
650+
if p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyBytes, hdrs) {
515651
if formFieldName != "" {
516652
//trying again with a different generated body (simple hacky version)
517653
//retrying only for form data for now
@@ -532,7 +668,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
532668
"op": op,
533669
}).Debug("retrying.form.submit.image p.apiSpecEndpointCall")
534670

535-
if p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyForm.Bytes()) &&
671+
if p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyForm.Bytes(), hdrs) &&
536672
formFieldName != "" {
537673
strBody := strings.NewReader(data.DefaultTextJSON)
538674
var bodyForm *bytes.Buffer
@@ -545,7 +681,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
545681
log.WithFields(log.Fields{
546682
"op": op,
547683
}).Debug("retrying.form.submit.json p.apiSpecEndpointCall")
548-
p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyForm.Bytes())
684+
p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyForm.Bytes(), hdrs)
549685
}
550686
}
551687
}
@@ -561,7 +697,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
561697
"data": string(bodyBytes),
562698
}).Debug("generatedSchemaObject(true)/retrying.post p.apiSpecEndpointCall")
563699

564-
p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyBytes)
700+
p.apiSpecEndpointCall(httpClient, endpoint, apiMethod, contentType, bodyBytes, hdrs)
565701
}
566702
}
567703
}
@@ -574,8 +710,8 @@ func (p *CustomProbe) apiSpecEndpointCall(
574710
method string,
575711
contentType string,
576712
bodyBytes []byte,
713+
headers map[string]string,
577714
) bool {
578-
const op = "probe.http.CustomProbe.apiSpecEndpointCall"
579715
maxRetryCount := p.retryCount()
580716

581717
notReadyErrorWait := time.Duration(16)
@@ -605,7 +741,14 @@ func (p *CustomProbe) apiSpecEndpointCall(
605741
req.Header.Set(HeaderContentType, contentType)
606742
}
607743

608-
//no request headers and no credentials for now
744+
for hname, hvalue := range headers {
745+
if strings.EqualFold(hname, HeaderContentType) {
746+
continue
747+
}
748+
req.Header.Add(hname, hvalue)
749+
}
750+
751+
//no credentials for now
609752
res, err := client.Do(req)
610753
p.CallCount.Inc()
611754

0 commit comments

Comments
 (0)