Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions wftest/bdd/steps_assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package bdd

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/GoCodeAlone/workflow/wftest"
"github.com/cucumber/godog"
)

Expand Down Expand Up @@ -162,7 +161,7 @@ func (sc *ScenarioContext) theResponseJSONShouldBe(path, expected string) error
if err := sc.ensureResult(); err != nil {
return err
}
val, err := jsonPath(sc.result.RawBody, path)
val, err := wftest.JSONPath(sc.result.RawBody, path)
if err != nil {
return err
}
Expand All @@ -178,11 +177,11 @@ func (sc *ScenarioContext) theResponseJSONShouldNotBeEmpty(path string) error {
if err := sc.ensureResult(); err != nil {
return err
}
val, err := jsonPath(sc.result.RawBody, path)
val, err := wftest.JSONPath(sc.result.RawBody, path)
if err != nil {
return err
}
if val == nil || fmt.Sprintf("%v", val) == "" {
if wftest.IsJSONEmpty(val) {
return fmt.Errorf("response JSON %q: expected non-empty, got %v", path, val)
}
return nil
Expand All @@ -199,24 +198,3 @@ func (sc *ScenarioContext) theResponseHeaderShouldBe(header, expected string) er
}
return nil
}

// jsonPath traverses a JSON body using a dot-separated path (e.g., "user.name").
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
}
46 changes: 46 additions & 0 deletions wftest/json_path.go
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
}
37 changes: 37 additions & 0 deletions wftest/yaml_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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)
}
}
return
}

Expand Down
43 changes: 43 additions & 0 deletions wftest/yaml_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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", `
Expand Down
9 changes: 9 additions & 0 deletions wftest/yaml_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,13 @@ type ResponseAssert struct {
Status int `yaml:"status"`
// Body is a substring expected in the response body.
Body string `yaml:"body"`
// JSON maps dot-path keys to expected values for exact JSON path equality checks.
// Example: {"message": "ok", "data.id": "abc123"}
JSON map[string]any `yaml:"json"`
// JSONNotEmpty lists dot-paths that must be present and non-empty in the JSON body.
// Example: ["data", "meta"]
JSONNotEmpty []string `yaml:"json_not_empty"`
// Headers maps response header names to expected values.
// Example: {"Content-Type": "application/json"}
Headers map[string]string `yaml:"headers"`
}
Loading