-
Notifications
You must be signed in to change notification settings - Fork 1
wftest YAML runner: add JSON path and header response assertions #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
53657d7
3dbbbdc
3a20992
83854ff
a9b3658
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package wftest | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| ) | ||
|
|
||
| // JSONPath traverses a JSON body using a dot-separated path (e.g., "user.name"). | ||
| // Returns the value at the path, or an error if the path cannot be traversed. | ||
| func JSONPath(body []byte, path string) (any, error) { | ||
| var root any | ||
| if err := json.Unmarshal(body, &root); err != nil { | ||
| return nil, fmt.Errorf("JSON path %q: invalid JSON body: %w", path, err) | ||
| } | ||
| parts := strings.Split(path, ".") | ||
| current := root | ||
| for _, part := range parts { | ||
| m, ok := current.(map[string]any) | ||
| if !ok { | ||
| return nil, fmt.Errorf("JSON path %q: cannot traverse into non-object at %q", path, part) | ||
| } | ||
| current, ok = m[part] | ||
| if !ok { | ||
| return nil, fmt.Errorf("JSON path %q: key %q not found", path, part) | ||
| } | ||
| } | ||
| return current, nil | ||
| } | ||
|
|
||
| // IsJSONEmpty reports whether a JSON value should be considered empty. | ||
| // A value is empty if it is nil, an empty string, an empty slice, or an empty map. | ||
| func IsJSONEmpty(val any) bool { | ||
| if val == nil { | ||
| return true | ||
| } | ||
| switch v := val.(type) { | ||
| case string: | ||
| return v == "" | ||
| case []any: | ||
| return len(v) == 0 | ||
| case map[string]any: | ||
| return len(v) == 0 | ||
| } | ||
| return false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
@@ -248,6 +249,42 @@ func applyAssertion(t *testing.T, label string, result *Result, a *Assertion, h | |
| if a.Response.Body != "" && !strings.Contains(string(result.RawBody), a.Response.Body) { | ||
| t.Errorf("assertion %s: body %q not found in %q", label, a.Response.Body, string(result.RawBody)) | ||
| } | ||
| for path, expected := range a.Response.JSON { | ||
| val, err := JSONPath(result.RawBody, path) | ||
| if err != nil { | ||
| t.Errorf("assertion %s: %v", label, err) | ||
| continue | ||
| } | ||
| wantJSON, err := json.Marshal(expected) | ||
| if err != nil { | ||
| t.Errorf("assertion %s: JSON %q: cannot marshal expected value: %v", label, path, err) | ||
| continue | ||
| } | ||
| gotJSON, err := json.Marshal(val) | ||
| if err != nil { | ||
| t.Errorf("assertion %s: JSON %q: cannot marshal actual value: %v", label, path, err) | ||
| continue | ||
| } | ||
| if !bytes.Equal(wantJSON, gotJSON) { | ||
| t.Errorf("assertion %s: JSON %q: want %s, got %s", label, path, string(wantJSON), string(gotJSON)) | ||
| } | ||
| } | ||
| for _, path := range a.Response.JSONNotEmpty { | ||
| val, err := JSONPath(result.RawBody, path) | ||
| if err != nil { | ||
| t.Errorf("assertion %s: %v", label, err) | ||
| continue | ||
| } | ||
| if IsJSONEmpty(val) { | ||
| t.Errorf("assertion %s: JSON %q: expected non-empty, got %v", label, path, val) | ||
| } | ||
| } | ||
| for header, expected := range a.Response.Headers { | ||
| actual := result.Header(http.CanonicalHeaderKey(header)) | ||
| if actual != expected { | ||
| t.Errorf("assertion %s: header %q: want %q, got %q", label, header, expected, actual) | ||
| } | ||
|
Comment on lines
+282
to
+286
|
||
| } | ||
| return | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,49 @@ func TestYAMLRunner_StatefulTestData(t *testing.T) { | |
| wftest.RunYAMLTests(t, "testdata/stateful_test.yaml") | ||
| } | ||
|
|
||
| func TestYAMLRunner_ResponseJSON(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| writeFile(t, tmpDir+"/json_test.yaml", ` | ||
| yaml: | | ||
| modules: | ||
| - name: router | ||
| type: http.router | ||
| pipelines: | ||
| hello: | ||
| trigger: | ||
| type: http | ||
| config: | ||
| path: /hello | ||
| method: GET | ||
| steps: | ||
| - name: respond | ||
| type: step.json_response | ||
| config: | ||
| status: 200 | ||
| body: | ||
| message: "hello" | ||
| data: | ||
| id: "abc123" | ||
| tests: | ||
| json-path-check: | ||
| trigger: | ||
| type: http | ||
| path: /hello | ||
|
Comment on lines
+249
to
+251
|
||
| assertions: | ||
| - response: | ||
| status: 200 | ||
| json: | ||
| message: "hello" | ||
| data.id: "abc123" | ||
| json_not_empty: | ||
| - message | ||
| - data | ||
| headers: | ||
| Content-Type: "application/json" | ||
| `) | ||
| wftest.RunYAMLTests(t, tmpDir+"/json_test.yaml") | ||
| } | ||
|
|
||
| func TestRunYAMLTests_ScheduleTrigger(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| writeFile(t, tmpDir+"/schedule_test.yaml", ` | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json_not_emptycurrently treats empty arrays/maps as non-empty because it only checksval == nilorfmt.Sprintf("%v", val) == "". This will incorrectly pass for{}and[]. Consider handling common JSON container types explicitly (string/[]any/map[string]any) and checking length == 0 as empty.