From 628937b5b9a309dbfa887f11c1974ac63367c0cd Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Mon, 22 Sep 2025 10:33:48 -0400 Subject: [PATCH 1/3] bring gdt-http up to modern gdt core Updates the processing of gdt-http to align with gdt-core and gdt-kube packages. Signed-off-by: Jay Pipes --- action.go | 204 +++++++++++++++++++++ assertions.go | 27 +-- defaults.go | 14 +- errors.go | 17 +- eval.go | 269 ++++++---------------------- eval_test.go | 67 ++++--- fixtures.go | 10 +- go.mod | 16 +- go.sum | 38 ++-- parse.go | 382 ++++++++++++++++++++++++++++++++++------ parse_test.go | 158 ++++++++++------- plugin.go | 28 ++- spec.go | 58 ++++-- testdata/get-books.yaml | 2 +- testdata/parse.yaml | 2 +- var.go | 79 +++++++++ 16 files changed, 921 insertions(+), 450 deletions(-) create mode 100644 action.go create mode 100644 var.go diff --git a/action.go b/action.go new file mode 100644 index 0000000..2b912bc --- /dev/null +++ b/action.go @@ -0,0 +1,204 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package http + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + nethttp "net/http" + "reflect" + "strings" + + gdtcontext "github.com/gdt-dev/core/context" + "github.com/gdt-dev/core/debug" +) + +// Action describes the the HTTP-specific action that is performed by the test. +type Action struct { + // URL being called by HTTP client. Used with the `Method` field. + URL string `yaml:"url,omitempty"` + // HTTP Method specified by HTTP client. Used with the `URL` shortcut field. + Method string `yaml:"method,omitempty"` + // Data is the payload to send along in request + Data interface{} `yaml:"data,omitempty"` + // Shortcut for URL and Method of "GET" + Get string `yaml:"get,omitempty"` + // Shortcut for URL and Method of "POST" + Post string `yaml:"post,omitempty"` + // Shortcut for URL and Method of "PUT" + Put string `yaml:"put,omitempty"` + // Shortcut for URL and Method of "PATCH" + Patch string `yaml:"patch,omitempty"` + // Shortcut for URL and Method of "DELETE" + Delete string `yaml:"delete,omitempty"` +} + +// Do performs a single HTTP request, returning the HTTP Response and any +// runtime error. +func (a *Action) Do( + ctx context.Context, + c *http.Client, + defaults *Defaults, +) (*http.Response, error) { + url, err := a.getURL(ctx, defaults) + if err != nil { + return nil, err + } + + debug.Printf(ctx, "http: > %s %s", a.Method, url) + var reqData io.Reader + if a.Data != nil { + a.processRequestData(ctx) + jsonBody, err := json.Marshal(a.Data) + if err != nil { + return nil, err + } + b := bytes.NewReader(jsonBody) + if b.Size() > 0 { + sendData, _ := io.ReadAll(b) + debug.Printf(ctx, "http: > %s", sendData) + b.Seek(0, 0) + } + reqData = b + } + + req, err := nethttp.NewRequest(a.Method, url, reqData) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + debug.Printf(ctx, "http: < %d", resp.StatusCode) + return resp, err +} + +// getURL returns the URL to use for the test's HTTP request. The test's url +// field is first queried to see if it is the special $LOCATION string. If it +// is, then we return the previous HTTP response's Location header. Otherwise, +// we construct the URL from the httpFile's base URL and the test's url field. +func (a *Action) getURL( + ctx context.Context, + defaults *Defaults, +) (string, error) { + if strings.ToUpper(a.URL) == "$LOCATION" { + pr := priorRunData(ctx) + if pr == nil || pr.Response == nil { + panic("test unit referenced $LOCATION before executing an HTTP request") + } + url, err := pr.Response.Location() + if err != nil { + return "", ErrExpectedLocationHeader + } + return url.String(), nil + } + base := defaults.BaseURLFromContext(ctx) + return base + a.URL, nil +} + +// processRequestData looks through the raw data interface{} that was +// unmarshaled during parse for any string values that look like JSONPath +// expressions. If we find any, we query the fixture registry to see if any +// fixtures have a value that matches the JSONPath expression. See +// gdt.fixtures:jsonFixture for more information on how this works +func (a *Action) processRequestData(ctx context.Context) { + if a.Data == nil { + return + } + // Get a pointer to the unmarshaled interface{} so we can mutate the + // contents pointed to + p := reflect.ValueOf(&a.Data) + + // We're interested in the value pointed to by the interface{}, which is + // why we do a double Elem() here. + v := p.Elem().Elem() + vt := v.Type() + + switch vt.Kind() { + case reflect.Slice: + for i := 0; i < v.Len(); i++ { + item := v.Index(i).Elem() + it := item.Type() + a.preprocessMap(ctx, item, it.Key(), it.Elem()) + } + case reflect.Map: + a.preprocessMap(ctx, v, vt.Key(), vt.Elem()) + } +} + +// processRequestDataMap processes a map pointed to by v, transforming any +// string keys or values of the map into the results of calling the fixture +// set's State() method. +func (a *Action) preprocessMap( + ctx context.Context, + m reflect.Value, + kt reflect.Type, + vt reflect.Type, +) error { + it := m.MapRange() + for it.Next() { + if kt.Kind() == reflect.String { + keyStr := it.Key().String() + fixtures := gdtcontext.Fixtures(ctx) + for _, f := range fixtures { + if !f.HasState(keyStr) { + continue + } + trKeyStr := f.State(keyStr) + keyStr = trKeyStr.(string) + } + + val := it.Value() + err := a.preprocessMapValue(ctx, m, reflect.ValueOf(keyStr), val, val.Type()) + if err != nil { + return err + } + } + } + return nil +} + +func (a *Action) preprocessMapValue( + ctx context.Context, + m reflect.Value, + k reflect.Value, + v reflect.Value, + vt reflect.Type, +) error { + if vt.Kind() == reflect.Interface { + v = v.Elem() + vt = v.Type() + } + + switch vt.Kind() { + case reflect.Slice: + for i := 0; i < v.Len(); i++ { + item := v.Index(i) + fmt.Println(item) + } + fmt.Printf("map element is an array.\n") + case reflect.Map: + return a.preprocessMap(ctx, v, vt.Key(), vt.Elem()) + case reflect.String: + valStr := v.String() + fixtures := gdtcontext.Fixtures(ctx) + for _, f := range fixtures { + if !f.HasState(valStr) { + continue + } + trValStr := f.State(valStr) + m.SetMapIndex(k, reflect.ValueOf(trValStr)) + } + default: + return nil + } + return nil +} diff --git a/assertions.go b/assertions.go index 289f46c..d3d08e9 100644 --- a/assertions.go +++ b/assertions.go @@ -5,11 +5,12 @@ package http import ( + "context" nethttp "net/http" "strings" - gdtjson "github.com/gdt-dev/gdt/assertion/json" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" + gdtjson "github.com/gdt-dev/core/assertion/json" ) // Expect contains one or more assertions about an HTTP response @@ -52,10 +53,6 @@ func headerEqual(r *nethttp.Response, exp string) bool { type assertions struct { // failures contains the set of error messages for failed assertions failures []error - // terminal indicates there was a failure in evaluating the assertions that - // should be considered a terminal condition (and therefore the test action - // should not be retried). - terminal bool // exp contains the expected conditions to assert against exp *Expect // r is the `nethttp.Response` we will evaluate @@ -77,18 +74,11 @@ func (a *assertions) Failures() []error { return a.failures } -// Terminal returns a bool indicating the assertions failed in a way that is -// not retryable. -func (a *assertions) Terminal() bool { - if a == nil { - return false - } - return a.terminal -} - // OK checks all the assertions against the supplied arguments and returns true // if all assertions pass. -func (a *assertions) OK() bool { +func (a *assertions) OK( + ctx context.Context, +) bool { if a.exp == nil { return true } @@ -103,8 +93,7 @@ func (a *assertions) OK() bool { } if exp.JSON != nil { ja := gdtjson.New(exp.JSON, a.b) - if !ja.OK() { - a.terminal = ja.Terminal() + if !ja.OK(ctx) { for _, f := range ja.Failures() { a.Fail(f) } @@ -138,7 +127,7 @@ func newAssertions( exp *Expect, r *nethttp.Response, b []byte, -) gdttypes.Assertions { +) api.Assertions { return &assertions{ failures: []error{}, exp: exp, diff --git a/defaults.go b/defaults.go index 5e3c72f..6015353 100644 --- a/defaults.go +++ b/defaults.go @@ -7,9 +7,9 @@ package http import ( "context" - gdtcontext "github.com/gdt-dev/gdt/context" - "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" + gdtcontext "github.com/gdt-dev/core/context" + "github.com/gdt-dev/core/parse" "gopkg.in/yaml.v3" ) @@ -29,21 +29,21 @@ type Defaults struct { func (d *Defaults) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(node) + return parse.ExpectedMapAt(node) } // maps/structs are stored in a top-level Node.Content field which is a // concatenated slice of Node pointers in pairs of key/values. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] if keyNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(keyNode) + return parse.ExpectedScalarAt(keyNode) } key := keyNode.Value valNode := node.Content[i+1] switch key { case "http": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return parse.ExpectedMapAt(valNode) } hd := httpDefaults{} if err := valNode.Decode(&hd); err != nil { @@ -80,7 +80,7 @@ func (d *Defaults) BaseURLFromContext(ctx context.Context) string { } // fromBaseDefaults returns an gdt-http plugin-specific Defaults from a Spec -func fromBaseDefaults(base *gdttypes.Defaults) *Defaults { +func fromBaseDefaults(base *api.Defaults) *Defaults { if base == nil { return nil } diff --git a/errors.go b/errors.go index 0df5670..f8a3f46 100644 --- a/errors.go +++ b/errors.go @@ -7,24 +7,17 @@ package http import ( "fmt" - gdterrors "github.com/gdt-dev/gdt/errors" + "github.com/gdt-dev/core/api" ) var ( - // ErrAliasOrURL is returned when the test author failed to provide either - // a URL and Method or specify one of the aliases like GET, POST, or DELETE - ErrAliasOrURL = fmt.Errorf( - "%w: either specify a URL and Method or specify one "+ - "of GET, POST, PUT, PATCH or DELETE", - gdterrors.ErrParse, - ) // ErrExpectedLocationHeader indicates that the user specified the special // `$LOCATION` string in the `url` or `GET` fields of the HTTP test spec // but there have been no previous HTTP responses to find a Location HTTP // Header within. ErrExpectedLocationHeader = fmt.Errorf( "%w: expected Location HTTP Header in previous response", - gdterrors.RuntimeError, + api.RuntimeError, ) ) @@ -33,7 +26,7 @@ var ( func HTTPStatusNotEqual(exp, got interface{}) error { return fmt.Errorf( "%w: expected HTTP status %v but got %v", - gdterrors.ErrNotEqual, exp, got, + api.ErrNotEqual, exp, got, ) } @@ -42,7 +35,7 @@ func HTTPStatusNotEqual(exp, got interface{}) error { func HTTPHeaderNotIn(element, container interface{}) error { return fmt.Errorf( "%w: expected HTTP headers %v to contain %v", - gdterrors.ErrNotIn, container, element, + api.ErrNotIn, container, element, ) } @@ -51,6 +44,6 @@ func HTTPHeaderNotIn(element, container interface{}) error { func HTTPNotInBody(element string) error { return fmt.Errorf( "%w: expected HTTP body to contain %v", - gdterrors.ErrNotIn, element, + api.ErrNotIn, element, ) } diff --git a/eval.go b/eval.go index 3ae6f8e..92ef835 100644 --- a/eval.go +++ b/eval.go @@ -5,100 +5,63 @@ package http import ( - "bytes" "context" - "encoding/json" - "fmt" "io" "io/ioutil" nethttp "net/http" - "reflect" - "strings" - "testing" - gdtcontext "github.com/gdt-dev/gdt/context" - gdtdebug "github.com/gdt-dev/gdt/debug" - "github.com/gdt-dev/gdt/result" - "github.com/stretchr/testify/require" + "github.com/gdt-dev/core/api" + gdtcontext "github.com/gdt-dev/core/context" + "github.com/gdt-dev/core/debug" ) -// RunData is data stored in the context about the run. It is fetched from the -// gdtcontext.PriorRun() function and evaluated for things like the special -// `$LOCATION` URL value. -type RunData struct { - Response *nethttp.Response -} - -// priorRunData returns any prior run cached data in the context. -func priorRunData(ctx context.Context) *RunData { - prData := gdtcontext.PriorRun(ctx) - httpData, ok := prData[pluginName] - if !ok { - return nil - } - if data, ok := httpData.(*RunData); ok { - return data - } - return nil -} +// Run executes the test described by the HTTP test. A new HTTP request and +// response pair is created during this call. +func (s *Spec) Eval(ctx context.Context) (*api.Result, error) { + c := client(ctx) + defaults := fromBaseDefaults(s.Defaults) + runData := &RunData{} -// getURL returns the URL to use for the test's HTTP request. The test's url -// field is first queried to see if it is the special $LOCATION string. If it -// is, then we return the previous HTTP response's Location header. Otherwise, -// we construct the URL from the httpFile's base URL and the test's url field. -func (s *Spec) getURL(ctx context.Context) (string, error) { - if strings.ToUpper(s.URL) == "$LOCATION" { - pr := priorRunData(ctx) - if pr == nil || pr.Response == nil { - panic("test unit referenced $LOCATION before executing an HTTP request") - } - url, err := pr.Response.Location() - if err != nil { - return "", ErrExpectedLocationHeader + resp, err := s.HTTP.Do(ctx, c, defaults) + if err != nil { + if err == api.ErrTimeoutExceeded { + return api.NewResult(api.WithFailures(api.ErrTimeoutExceeded)), nil } - return url.String(), nil + return nil, err } - d := fromBaseDefaults(s.Defaults) - base := d.BaseURLFromContext(ctx) - return base + s.URL, nil -} - -// processRequestData looks through the raw data interface{} that was -// unmarshaled during parse for any string values that look like JSONPath -// expressions. If we find any, we query the fixture registry to see if any -// fixtures have a value that matches the JSONPath expression. See -// gdt.fixtures:jsonFixture for more information on how this works -func (s *Spec) processRequestData(ctx context.Context) { - if s.Data == nil { - return + // Make sure we drain and close our response body... + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + // Only read the response body contents once and pass the byte + // buffer to the assertion functions + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err } - // Get a pointer to the unmarshaled interface{} so we can mutate the - // contents pointed to - p := reflect.ValueOf(&s.Data) - - // We're interested in the value pointed to by the interface{}, which is - // why we do a double Elem() here. - v := p.Elem().Elem() - vt := v.Type() - - switch vt.Kind() { - case reflect.Slice: - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Elem() - it := item.Type() - s.preprocessMap(ctx, item, it.Key(), it.Elem()) + if len(body) > 0 { + debug.Printf(ctx, "http: < %s", string(body)) + } + a := newAssertions(s.Assert, resp, body) + if a.OK(ctx) { + runData.Response = resp + res := api.NewResult() + res.SetData(pluginName, runData) + if err := saveVars(ctx, s.Var, body, res); err != nil { + return nil, err } - // ht.f.preprocessSliceValue(v, vt.Key(), vt.Elem()) - case reflect.Map: - s.preprocessMap(ctx, v, vt.Key(), vt.Elem()) + return res, nil } + return api.NewResult(api.WithFailures(a.Failures()...)), nil } // client returns the HTTP client to use when executing HTTP requests. If any // fixture provides a state with key "http.client", the fixture is asked for // the HTTP client. Otherwise, we use the net/http.DefaultClient -func (s *Spec) client(ctx context.Context) *nethttp.Client { +func client(ctx context.Context) *nethttp.Client { // query the fixture registry to determine if any of them contain an // http.client state attribute. fixtures := gdtcontext.Fixtures(ctx) @@ -114,152 +77,22 @@ func (s *Spec) client(ctx context.Context) *nethttp.Client { return nethttp.DefaultClient } -// processRequestDataMap processes a map pointed to by v, transforming any -// string keys or values of the map into the results of calling the fixture -// set's State() method. -func (s *Spec) preprocessMap( - ctx context.Context, - m reflect.Value, - kt reflect.Type, - vt reflect.Type, -) error { - it := m.MapRange() - for it.Next() { - if kt.Kind() == reflect.String { - keyStr := it.Key().String() - fixtures := gdtcontext.Fixtures(ctx) - for _, f := range fixtures { - if !f.HasState(keyStr) { - continue - } - trKeyStr := f.State(keyStr) - keyStr = trKeyStr.(string) - } - - val := it.Value() - err := s.preprocessMapValue(ctx, m, reflect.ValueOf(keyStr), val, val.Type()) - if err != nil { - return err - } - } - } - return nil +// RunData is data stored in the context about the run. It is fetched from the +// gdtcontext.PriorRun() function and evaluated for things like the special +// `$LOCATION` URL value. +type RunData struct { + Response *nethttp.Response } -func (s *Spec) preprocessMapValue( - ctx context.Context, - m reflect.Value, - k reflect.Value, - v reflect.Value, - vt reflect.Type, -) error { - if vt.Kind() == reflect.Interface { - v = v.Elem() - vt = v.Type() - } - - switch vt.Kind() { - case reflect.Slice: - for i := 0; i < v.Len(); i++ { - item := v.Index(i) - fmt.Println(item) - } - fmt.Printf("map element is an array.\n") - case reflect.Map: - return s.preprocessMap(ctx, v, vt.Key(), vt.Elem()) - case reflect.String: - valStr := v.String() - fixtures := gdtcontext.Fixtures(ctx) - for _, f := range fixtures { - if !f.HasState(valStr) { - continue - } - trValStr := f.State(valStr) - m.SetMapIndex(k, reflect.ValueOf(trValStr)) - } - default: +// priorRunData returns any prior run cached data in the context. +func priorRunData(ctx context.Context) *RunData { + data := gdtcontext.Run(ctx) + httpData, ok := data[pluginName] + if !ok { return nil } - return nil -} - -// Run executes the test described by the HTTP test. A new HTTP request and -// response pair is created during this call. -func (s *Spec) Eval(ctx context.Context, t *testing.T) *result.Result { - runData := &RunData{} - var rerr error - fails := []error{} - t.Run(s.Title(), func(t *testing.T) { - url, err := s.getURL(ctx) - if err != nil { - rerr = err - return - } - - gdtdebug.Println(ctx, t, "http: > %s %s", s.Method, url) - var body io.Reader - if s.Data != nil { - s.processRequestData(ctx) - jsonBody, err := json.Marshal(s.Data) - require.Nil(t, err) - b := bytes.NewReader(jsonBody) - if b.Size() > 0 { - sendData, _ := io.ReadAll(b) - gdtdebug.Println(ctx, t, "http: > %s", sendData) - b.Seek(0, 0) - } - body = b - } - - req, err := nethttp.NewRequest(s.Method, url, body) - if err != nil { - rerr = err - return - } - - // TODO(jaypipes): Allow customization of the HTTP client for proxying, - // TLS, etc - c := s.client(ctx) - - resp, err := c.Do(req) - if err != nil { - rerr = err - return - } - gdtdebug.Println(ctx, t, "http: < %d", resp.StatusCode) - - // Make sure we drain and close our response body... - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - // Only read the response body contents once and pass the byte - // buffer to the assertion functions - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - rerr = err - return - } - if len(b) > 0 { - gdtdebug.Println(ctx, t, "http: < %s", b) - } - exp := s.Assert - if exp != nil { - a := newAssertions(exp, resp, b) - fails = a.Failures() - - } - runData.Response = resp - }) - if rerr != nil { - return result.New( - result.WithRuntimeError(rerr), - ) - } else { - return result.New( - result.WithFailures(fails...), - result.WithData(pluginName, runData), - ) + if data, ok := httpData.(*RunData); ok { + return data } + return nil } diff --git a/eval_test.go b/eval_test.go index 6647d15..88b0502 100644 --- a/eval_test.go +++ b/eval_test.go @@ -12,9 +12,10 @@ import ( "path/filepath" "testing" - "github.com/gdt-dev/gdt" - gdterrors "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" + gdtcontext "github.com/gdt-dev/core/context" + gdtjsonfix "github.com/gdt-dev/core/fixture/json" + "github.com/gdt-dev/core/scenario" gdthttp "github.com/gdt-dev/http" "github.com/gdt-dev/http/test/server" "github.com/stretchr/testify/assert" @@ -43,12 +44,12 @@ func data() *dataset { return data } -func dataFixture() gdttypes.Fixture { +func dataFixture() api.Fixture { f, err := os.Open(dataFilePath) if err != nil { panic(err) } - fix, err := gdt.NewJSONFixture(f) + fix, err := gdtjsonfix.New(f) if err != nil { panic(err) } @@ -60,13 +61,17 @@ func TestFixturesNotSetup(t *testing.T) { assert := assert.New(t) fp := filepath.Join("testdata", "create-then-get.yaml") - s, err := gdt.From(fp) + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck + + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) require.NotNil(s) err = s.Run(context.TODO(), t) require.NotNil(err) - assert.ErrorIs(err, gdterrors.RuntimeError) + assert.ErrorIs(err, api.RuntimeError) } func setup(ctx context.Context) context.Context { @@ -75,8 +80,8 @@ func setup(ctx context.Context) context.Context { logger := log.New(os.Stdout, "books_api_http: ", log.LstdFlags) srv := server.NewControllerWithBooks(logger, data().Books) serverFixture := gdthttp.NewServerFixture(srv.Router(), false /* useTLS */) - ctx = gdt.RegisterFixture(ctx, "books_api", serverFixture) - ctx = gdt.RegisterFixture(ctx, "books_data", dataFixture()) + ctx = gdtcontext.RegisterFixture(ctx, "books_api", serverFixture) + ctx = gdtcontext.RegisterFixture(ctx, "books_data", dataFixture()) return ctx } @@ -84,14 +89,17 @@ func TestCreateThenGet(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "create-then-get.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - ctx := gdt.NewContext() - ctx = setup(ctx) - - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) require.NotNil(s) + ctx := gdtcontext.New() + ctx = setup(ctx) + s.Run(ctx, t) } @@ -99,14 +107,17 @@ func TestFailures(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "failures.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - ctx := gdt.NewContext() - ctx = setup(ctx) - - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) require.NotNil(s) + ctx := gdtcontext.New() + ctx = setup(ctx) + s.Run(ctx, t) } @@ -114,14 +125,17 @@ func TestGetBooks(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "get-books.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - ctx := gdt.NewContext() - ctx = setup(ctx) - - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) require.NotNil(s) + ctx := gdtcontext.New() + ctx = setup(ctx) + s.Run(ctx, t) } @@ -129,13 +143,16 @@ func TestPutMultipleBooks(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "put-multiple-books.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - ctx := gdt.NewContext() - ctx = setup(ctx) - - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) require.NotNil(s) + ctx := gdtcontext.New() + ctx = setup(ctx) + s.Run(ctx, t) } diff --git a/fixtures.go b/fixtures.go index 0c4b93f..c73d482 100644 --- a/fixtures.go +++ b/fixtures.go @@ -5,11 +5,12 @@ package http import ( + "context" nethttp "net/http" "net/http/httptest" "strings" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" ) const ( @@ -23,15 +24,16 @@ type httpServerFixture struct { useTLS bool } -func (f *httpServerFixture) Start() { +func (f *httpServerFixture) Start(ctx context.Context) error { if !f.useTLS { f.server = httptest.NewServer(f.handler) } else { f.server = httptest.NewTLSServer(f.handler) } + return nil } -func (f *httpServerFixture) Stop() { +func (f *httpServerFixture) Stop(ctx context.Context) { f.server.Close() } @@ -59,6 +61,6 @@ func (f *httpServerFixture) State(key string) interface{} { // http.Handler. The returned fixture exposes an "http.base_url" state key that // test cases of type "http" examine to determine the base URL the tests should // hit -func NewServerFixture(h nethttp.Handler, useTLS bool) gdttypes.Fixture { +func NewServerFixture(h nethttp.Handler, useTLS bool) api.Fixture { return &httpServerFixture{handler: h, useTLS: useTLS} } diff --git a/go.mod b/go.mod index 53c0ab4..eabccd8 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,23 @@ module github.com/gdt-dev/http -go 1.19 +go 1.24.3 require ( - github.com/gdt-dev/gdt v1.1.0 - github.com/google/uuid v1.3.0 - github.com/samber/lo v1.38.1 - github.com/stretchr/testify v1.8.4 + github.com/gdt-dev/core v1.10.0 + github.com/google/uuid v1.6.0 + github.com/samber/lo v1.51.0 + github.com/stretchr/testify v1.11.1 + github.com/theory/jsonpath v0.10.1 github.com/xeipuuv/gojsonschema v1.2.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/PaesslerAG/gval v1.0.0 // indirect - github.com/PaesslerAG/jsonpath v0.1.1 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index aab1a64..1267197 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,40 @@ -github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= -github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= -github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= -github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= -github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gdt-dev/gdt v1.1.0 h1:+eFYFSibOYCTFKqoACx2CefAbErmBmFWXG7kDioR5do= -github.com/gdt-dev/gdt v1.1.0/go.mod h1:StnyGjC/67u59La2u6fh3HwW9MmodVhKdXcLlkgvNSY= +github.com/gdt-dev/core v1.10.0 h1:yX0MG2Tt+O34JGDFWcS63LV47lWpizto4HAyR/ugAC0= +github.com/gdt-dev/core v1.10.0/go.mod h1:Bw8J6kUW0b7MUL8qW5e7qSbxb4SI9EAWQ0a4cAoPVpo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/theory/jsonpath v0.10.1 h1:Qa3alEtTTLIy2s60U2XzamS0XgQmF9zWIg42mEkSRVg= +github.com/theory/jsonpath v0.10.1/go.mod h1:ZOz+y6MxTEDcN/FOxf9AOgeHSoKHx2B+E0nD3HOtzGE= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parse.go b/parse.go index ebff999..6562a1d 100644 --- a/parse.go +++ b/parse.go @@ -5,119 +5,389 @@ package http import ( + "fmt" "strings" - "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" + gdtjson "github.com/gdt-dev/core/assertion/json" + "github.com/gdt-dev/core/parse" "github.com/samber/lo" + "github.com/theory/jsonpath" "gopkg.in/yaml.v3" ) +var validHTTPMethods = []string{ + "DELETE", + "GET", + "PATCH", + "POST", + "PUT", +} + +// InvalidHTTPMethodAt returns a parse error indicating the test author used an +// invalid method field value. +func InvalidHTTPMethodAt(method string, node *yaml.Node) error { + return &parse.Error{ + Line: node.Line, + Column: node.Column, + Message: fmt.Sprintf( + "invalid HTTP method specified: %s. valid values: %s", + method, strings.Join(validHTTPMethods, ","), + ), + } +} + +// EitherShortcutOrHTTPSpecAt returns a parse error indicating the test author +// included both a shortcut (e.g. `http.get` or just `GET`) AND the long-form +// `http` object in the same test spec. +func EitherShortcutOrHTTPSpecAt(node *yaml.Node) error { + return &parse.Error{ + Line: node.Line, + Column: node.Column, + Message: "either specify a full HTTPSpec in the `http` field or " + + "specify one of the shortcuts (e.g. `http.get` or `GET`", + } +} + +// MultipleHTTPMethods returns a parse error indicating the test author +// specified multiple HTTP methods either full-form or via shortcuts. +func MultipleHTTPMethods( + firstMethod string, + secondMethod string, + node *yaml.Node, +) error { + return &parse.Error{ + Line: node.Line, + Column: node.Column, + Message: fmt.Sprintf( + "multiple HTTP methods specified (%q, %q). "+ + "please specify a single HTTP method for each test spec.", + firstMethod, secondMethod, + ), + } +} + func (s *Spec) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(node) + return parse.ExpectedMapAt(node) } + vars := Variables{} + // We do an initial pass over the shortcut fields, then all the + // non-shortcut fields after that. + var hs *HTTPSpec + // maps/structs are stored in a top-level Node.Content field which is a // concatenated slice of Node pointers in pairs of key/values. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] if keyNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(keyNode) + return parse.ExpectedScalarAt(keyNode) } key := keyNode.Value valNode := node.Content[i+1] switch key { case "url": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) } - s.URL = strings.TrimSpace(valNode.Value) + url := strings.TrimSpace(valNode.Value) + hs = &HTTPSpec{} + hs.URL = url case "method": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) } - s.Method = strings.TrimSpace(valNode.Value) - case "GET": + method := strings.ToUpper(strings.TrimSpace(valNode.Value)) + if !lo.Contains(validHTTPMethods, s.Method) { + return InvalidHTTPMethodAt(valNode.Value, valNode) + } + if hs != nil { + if hs.Method != "" { + return MultipleHTTPMethods(hs.Method, method, valNode) + } + hs.Method = method + } else { + hs = &HTTPSpec{} + hs.Method = method + } + case "GET", "get", "http.get": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if hs != nil { + return MultipleHTTPMethods(hs.Method, "GET", valNode) } - s.GET = strings.TrimSpace(valNode.Value) - case "POST": + hs = &HTTPSpec{} + hs.Method = "GET" + hs.URL = url + case "POST", "post", "http.post": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) } - s.POST = strings.TrimSpace(valNode.Value) - case "PUT": + url := strings.TrimSpace(valNode.Value) + if hs != nil { + return MultipleHTTPMethods(hs.Method, "POST", valNode) + } + hs = &HTTPSpec{} + hs.Method = "POST" + hs.URL = url + case "PUT", "put", "http.put": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if hs != nil { + return MultipleHTTPMethods(hs.Method, "PUT", valNode) } - s.PUT = strings.TrimSpace(valNode.Value) - case "DELETE": + hs = &HTTPSpec{} + hs.Method = "PUT" + hs.URL = url + case "DELETE", "delete", "http.delete": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) } - s.DELETE = strings.TrimSpace(valNode.Value) - case "PATCH": + url := strings.TrimSpace(valNode.Value) + if hs != nil { + return MultipleHTTPMethods(hs.Method, "DELETE", valNode) + } + hs = &HTTPSpec{} + hs.Method = "DELETE" + hs.URL = url + case "PATCH", "patch", "http.patch": if valNode.Kind != yaml.ScalarNode { - return errors.ExpectedScalarAt(valNode) + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if hs != nil { + return MultipleHTTPMethods(hs.Method, "PATCH", valNode) } - s.PATCH = strings.TrimSpace(valNode.Value) + hs = &HTTPSpec{} + hs.Method = "PATCH" + hs.URL = url case "data": var data interface{} if err := valNode.Decode(&data); err != nil { return err } s.Data = data + } + } + + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "http": + if valNode.Kind != yaml.MappingNode { + return parse.ExpectedMapAt(valNode) + } + if hs != nil { + return EitherShortcutOrHTTPSpecAt(valNode) + } + if err := valNode.Decode(&hs); err != nil { + return err + } + s.HTTP = hs + case "var": + if valNode.Kind != yaml.MappingNode { + return parse.ExpectedMapAt(valNode) + } + var specVars Variables + if err := valNode.Decode(&specVars); err != nil { + return err + } + vars = lo.Assign(specVars, vars) case "assert": if valNode.Kind != yaml.MappingNode { - return errors.ExpectedMapAt(valNode) + return parse.ExpectedMapAt(valNode) } - var exp *Expect - if err := valNode.Decode(&exp); err != nil { + var e *Expect + if err := valNode.Decode(&e); err != nil { return err } - s.Assert = exp + s.Assert = e + case "http.get", "http.post", "http.delete", "http.put", "http.patch", + "GET", "POST", "DELETE", "PUT", "PATCH", + "get", "post", "delete", "put", "patch", + "url", "method", "data": + continue default: - if lo.Contains(gdttypes.BaseSpecFields, key) { + if lo.Contains(api.BaseSpecFields, key) { continue } - return errors.UnknownFieldAt(key, keyNode) + return parse.UnknownFieldAt(key, keyNode) } } - if err := validateMethodAndURL(s); err != nil { + if s.Data != nil { + hs.Data = s.Data + } + s.HTTP = hs + if len(vars) > 0 { + s.Var = vars + } + return nil +} + +func (s *HTTPSpec) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return parse.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + switch key { + case "get", "put", "post", "patch", "delete", + "GET", "PUT", "POST", "PATCH", "DELETE", + "url", "method", "data": + // Because Action is an embedded struct and we parse it below, just + // ignore these fields in the top-level `http:` field for now. + default: + return parse.UnknownFieldAt(key, keyNode) + } + } + var a Action + if err := node.Decode(&a); err != nil { return err } + s.Action = a return nil } -func validateMethodAndURL(s *Spec) error { - if s.URL == "" { - if s.GET != "" { - s.Method = "GET" - s.URL = s.GET - return nil - } else if s.POST != "" { - s.Method = "POST" - s.URL = s.POST - return nil - } else if s.PUT != "" { - s.Method = "PUT" - s.URL = s.PUT - return nil - } else if s.DELETE != "" { - s.Method = "DELETE" - s.URL = s.DELETE - return nil - } else if s.PATCH != "" { - s.Method = "PATCH" - s.URL = s.PATCH - return nil - } else { - return ErrAliasOrURL +func (a *Action) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return parse.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "url": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + a.URL = url + case "method": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + method := strings.ToUpper(strings.TrimSpace(valNode.Value)) + if !lo.Contains(validHTTPMethods, a.Method) { + return InvalidHTTPMethodAt(valNode.Value, valNode) + } + if a.Method != "" { + return MultipleHTTPMethods(a.Method, method, valNode) + } + a.Method = method + case "GET", "get", "http.get": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if a.Method != "" { + return MultipleHTTPMethods(a.Method, "GET", valNode) + } + a.Method = "GET" + a.URL = url + case "POST", "post", "http.post": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if a.Method != "" { + return MultipleHTTPMethods(a.Method, "POST", valNode) + } + a.Method = "POST" + a.URL = url + case "PUT", "put", "http.put": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if a.Method != "" { + return MultipleHTTPMethods(a.Method, "PUT", valNode) + } + a.Method = "PUT" + a.URL = url + case "DELETE", "delete", "http.delete": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if a.Method != "" { + return MultipleHTTPMethods(a.Method, "DELETE", valNode) + } + a.Method = "DELETE" + a.URL = url + case "PATCH", "patch", "http.patch": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + url := strings.TrimSpace(valNode.Value) + if a.Method != "" { + return MultipleHTTPMethods(a.Method, "PATCH", valNode) + } + a.Method = "PATCH" + a.URL = url + case "data": + var data interface{} + if err := valNode.Decode(&data); err != nil { + return err + } + a.Data = data } } - if s.Method == "" { - return ErrAliasOrURL + return nil +} + +// UnmarshalYAML is a custom unmarshaler that ensures that JSONPath expressions +// contained in the VarEntry are valid. +func (e *VarEntry) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return parse.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "from": + if valNode.Kind != yaml.ScalarNode { + return parse.ExpectedScalarAt(valNode) + } + var path string + if err := valNode.Decode(&path); err != nil { + return err + } + if len(path) == 0 || path[0] != '$' { + return gdtjson.JSONPathInvalidNoRoot(path, valNode) + } + if _, err := jsonpath.Parse(path); err != nil { + return gdtjson.JSONPathInvalid(path, err, valNode) + } + e.From = path + } } return nil } diff --git a/parse_test.go b/parse_test.go index 37cbe2b..8ae0139 100644 --- a/parse_test.go +++ b/parse_test.go @@ -5,16 +5,17 @@ package http_test import ( + "os" "path/filepath" "runtime" "strings" "testing" "time" - "github.com/gdt-dev/gdt" - gdtjson "github.com/gdt-dev/gdt/assertion/json" - "github.com/gdt-dev/gdt/errors" - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" + gdtjson "github.com/gdt-dev/core/assertion/json" + "github.com/gdt-dev/core/parse" + "github.com/gdt-dev/core/scenario" gdthttp "github.com/gdt-dev/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,9 +31,14 @@ func TestBadDefaults(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "parse", "fail", "bad-defaults.yaml") - s, err := gdt.From(fp) + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck + + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.NotNil(err) - assert.ErrorIs(err, errors.ErrExpectedMap) + assert.Error(err, &parse.Error{}) + assert.ErrorContains(err, "expected map") require.Nil(s) } @@ -41,10 +47,14 @@ func TestParseFailures(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "parse", "fail", "invalid.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.NotNil(err) - assert.ErrorIs(err, errors.ErrExpectedMap) + assert.Error(err, &parse.Error{}) + assert.ErrorContains(err, "expected map") require.Nil(s) } @@ -53,10 +63,13 @@ func TestMissingSchema(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "parse", "fail", "missing-schema.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - s, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.NotNil(err) - assert.ErrorIs(err, gdtjson.ErrJSONSchemaFileNotFound) + assert.ErrorContains(err, "unable to find JSONSchema file") require.Nil(s) } @@ -65,10 +78,12 @@ func TestParse(t *testing.T) { require := require.New(t) fp := filepath.Join("testdata", "parse.yaml") + f, err := os.Open(fp) + require.Nil(err) + defer f.Close() // nolint:errcheck - suite, err := gdt.From(fp) + s, err := scenario.FromReader(f, scenario.WithPath(fp)) require.Nil(err) - require.NotNil(suite) code404 := 404 code200 := 200 @@ -93,19 +108,19 @@ func TestParse(t *testing.T) { } schemaPath := strings.Join(pathParts, "") - require.Len(suite.Scenarios, 1) - s := suite.Scenarios[0] - - expTests := []gdttypes.Evaluable{ + expTests := []api.Evaluable{ &gdthttp.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 0, Name: "no such book was found", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, + }, + HTTP: &gdthttp.HTTPSpec{ + Action: gdthttp.Action{ + Method: "GET", + URL: "/books/nosuchbook", + }, }, - Method: "GET", - URL: "/books/nosuchbook", - GET: "/books/nosuchbook", Assert: &gdthttp.Expect{ JSON: &gdtjson.Expect{ Len: &len0, @@ -114,14 +129,17 @@ func TestParse(t *testing.T) { }, }, &gdthttp.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 1, Name: "list all books", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, + }, + HTTP: &gdthttp.HTTPSpec{ + Action: gdthttp.Action{ + Method: "GET", + URL: "/books", + }, }, - Method: "GET", - URL: "/books", - GET: "/books", Assert: &gdthttp.Expect{ JSON: &gdtjson.Expect{ Schema: schemaPath, @@ -130,20 +148,23 @@ func TestParse(t *testing.T) { }, }, &gdthttp.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 2, Name: "create a new book", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, - Method: "POST", - URL: "/books", - POST: "/books", - Data: map[string]interface{}{ - "title": "For Whom The Bell Tolls", - "published_on": publishedOn1940, - "pages": 480, - "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", - "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", + HTTP: &gdthttp.HTTPSpec{ + Action: gdthttp.Action{ + Method: "POST", + URL: "/books", + Data: map[string]any{ + "title": "For Whom The Bell Tolls", + "published_on": publishedOn1940, + "pages": 480, + "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", + "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", + }, + }, }, Assert: &gdthttp.Expect{ Status: &code201, @@ -153,14 +174,17 @@ func TestParse(t *testing.T) { }, }, &gdthttp.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 3, Name: "look up that created book", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, + }, + HTTP: &gdthttp.HTTPSpec{ + Action: gdthttp.Action{ + Method: "GET", + URL: "$LOCATION", + }, }, - Method: "GET", - URL: "$LOCATION", - GET: "$LOCATION", Assert: &gdthttp.Expect{ JSON: &gdtjson.Expect{ Paths: map[string]string{ @@ -175,28 +199,31 @@ func TestParse(t *testing.T) { }, }, &gdthttp.Spec{ - Spec: gdttypes.Spec{ + Spec: api.Spec{ Index: 4, Name: "create two books", - Defaults: &gdttypes.Defaults{}, + Defaults: &api.Defaults{}, }, - Method: "PUT", - URL: "/books", - PUT: "/books", - Data: []interface{}{ - map[string]interface{}{ - "title": "For Whom The Bell Tolls", - "published_on": publishedOn1940, - "pages": 480, - "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", - "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", - }, - map[string]interface{}{ - "title": "To Have and Have Not", - "published_on": publishedOn1937, - "pages": 257, - "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", - "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", + HTTP: &gdthttp.HTTPSpec{ + Action: gdthttp.Action{ + Method: "PUT", + URL: "/books", + Data: []any{ + map[string]any{ + "title": "For Whom The Bell Tolls", + "published_on": publishedOn1940, + "pages": 480, + "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", + "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", + }, + map[string]any{ + "title": "To Have and Have Not", + "published_on": publishedOn1937, + "pages": 257, + "author_id": "$.authors.by_name[\"Ernest Hemingway\"].id", + "publisher_id": "$.publishers.by_name[\"Charles Scribner's Sons\"].id", + }, + }, }, }, Assert: &gdthttp.Expect{ @@ -204,5 +231,12 @@ func TestParse(t *testing.T) { }, }, } - assert.Equal(expTests, s.Tests) + require.Len(s.Tests, len(expTests)) + for x, st := range s.Tests { + exp := expTests[x].(*gdthttp.Spec) + sth := st.(*gdthttp.Spec) + assert.Equal(exp.HTTP, sth.HTTP) + assert.Equal(exp.Var, sth.Var) + assert.Equal(exp.Assert, sth.Assert) + } } diff --git a/plugin.go b/plugin.go index 142c972..9ea80e4 100644 --- a/plugin.go +++ b/plugin.go @@ -5,14 +5,20 @@ package http import ( + "github.com/gdt-dev/core/api" + gdtplugin "github.com/gdt-dev/core/plugin" "gopkg.in/yaml.v3" +) - "github.com/gdt-dev/gdt" - gdttypes "github.com/gdt-dev/gdt/types" +var ( + // DefaultTimeout is the default timeout used for each individual test + // spec. Note that gdt's top-level Scenario.Run handles all timeout and + // retry behaviour. + DefaultTimeout = "5s" ) func init() { - gdt.RegisterPlugin(Plugin()) + gdtplugin.Register(Plugin()) } const ( @@ -21,9 +27,15 @@ const ( type plugin struct{} -func (p *plugin) Info() gdttypes.PluginInfo { - return gdttypes.PluginInfo{ +func (p *plugin) Info() api.PluginInfo { + return api.PluginInfo{ Name: pluginName, + Retry: &api.Retry{ + Exponential: true, + }, + Timeout: &api.Timeout{ + After: DefaultTimeout, + }, } } @@ -31,11 +43,11 @@ func (p *plugin) Defaults() yaml.Unmarshaler { return &Defaults{} } -func (p *plugin) Specs() []gdttypes.Evaluable { - return []gdttypes.Evaluable{&Spec{}} +func (p *plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} } // Plugin returns the HTTP gdt plugin -func Plugin() gdttypes.Plugin { +func Plugin() api.Plugin { return &plugin{} } diff --git a/spec.go b/spec.go index 14e5aa9..195d006 100644 --- a/spec.go +++ b/spec.go @@ -5,30 +5,44 @@ package http import ( - gdttypes "github.com/gdt-dev/gdt/types" + "github.com/gdt-dev/core/api" ) +// HTTPSpec is the complex type containing all of the HTTP-specific actions. +// Most users will use the `GET`, `http.get` and similar shortcut fields. +type HTTPSpec struct { + Action +} + // Spec describes a test of a single HTTP request and response type Spec struct { - gdttypes.Spec - // URL being called by HTTP client + api.Spec + // HTTP is the complex type containing all of the HTTP-specific actions and + // assertions. Most users will use the `URL`/`Method`, `GET`, `http.get` + // and similar shortcut fields. + HTTP *HTTPSpec `yaml:"http,omitempty"` + // Shortcut for `http.url` URL string `yaml:"url,omitempty"` - // HTTP Method specified by HTTP client + // Shortcut for `http.method` Method string `yaml:"method,omitempty"` - // Shortcut for URL and Method of "GET" + // Shortcut for `http.get` GET string `yaml:"GET,omitempty"` - // Shortcut for URL and Method of "POST" + // Shortcut for `http.post` POST string `yaml:"POST,omitempty"` - // Shortcut for URL and Method of "PUT" + // Shortcut for `http.put` PUT string `yaml:"PUT,omitempty"` - // Shortcut for URL and Method of "PATCH" + // Shortcut for `http.patch` PATCH string `yaml:"PATCH,omitempty"` - // Shortcut for URL and Method of "DELETE" + // Shortcut for `http.delete` DELETE string `yaml:"DELETE,omitempty"` - // JSON payload to send along in request - Data interface{} `yaml:"data,omitempty"` + // Shortcut for `http.data` + Data any `yaml:"data,omitempty"` // Assert is the assertions for the HTTP response Assert *Expect `yaml:"assert,omitempty"` + // Var allows the test author to save arbitrary data to the test scenario, + // facilitating the passing of variables between test specs potentially + // provided by different gdt Plugins. + Var Variables `yaml:"var,omitempty"` } // Title returns a good name for the Spec @@ -41,10 +55,28 @@ func (s *Spec) Title() string { return s.Method + ":" + s.URL } -func (s *Spec) SetBase(b gdttypes.Spec) { +func (s *Spec) Retry() *api.Retry { + if s.Spec.Retry != nil { + // The user may have overridden in the test spec file... + return s.Spec.Retry + } + if s.Method == "GET" { + // returning nil here means the plugin's default will be used... + return nil + } + // for POST/PUT/DELETE/PATCH, we don't want to retry... + return api.NoRetry +} + +func (s *Spec) Timeout() *api.Timeout { + // returning nil here means the plugin's default will be used... + return nil +} + +func (s *Spec) SetBase(b api.Spec) { s.Spec = b } -func (s *Spec) Base() *gdttypes.Spec { +func (s *Spec) Base() *api.Spec { return &s.Spec } diff --git a/testdata/get-books.yaml b/testdata/get-books.yaml index c848ec4..944ee5f 100644 --- a/testdata/get-books.yaml +++ b/testdata/get-books.yaml @@ -7,4 +7,4 @@ tests: assert: status: 200 json: - schema: testdata/schemas/get_books.json + schema: schemas/get_books.json diff --git a/testdata/parse.yaml b/testdata/parse.yaml index ac42fba..d2425a8 100644 --- a/testdata/parse.yaml +++ b/testdata/parse.yaml @@ -12,7 +12,7 @@ tests: assert: status: 200 json: - schema: testdata/schemas/get_books.json + schema: schemas/get_books.json - name: create a new book POST: /books data: diff --git a/var.go b/var.go new file mode 100644 index 0000000..573202f --- /dev/null +++ b/var.go @@ -0,0 +1,79 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package http + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/gdt-dev/core/api" + gdtjson "github.com/gdt-dev/core/assertion/json" + "github.com/gdt-dev/core/debug" + "github.com/theory/jsonpath" +) + +type VarEntry struct { + // From is a string that indicates where the value of the variable will be + // sourced from. This string is a JSONPath expression that contains + // instructions on how to extract a particular field from the HTTP response. + From string `yaml:"from"` +} + +// Variables allows the test author to save arbitrary data to the test scenario, +// facilitating the passing of variables between test specs potentially +// provided by different gdt Plugins. +type Variables map[string]VarEntry + +// saveVars examines the supplied Variables and what we got back from the +// Action.Do() call and sets any variables in the run data context key. +func saveVars( + ctx context.Context, + vars Variables, + body []byte, + res *api.Result, +) error { + if len(body) == 0 { + return nil + } + var bodyMap any + if err := json.Unmarshal(body, &bodyMap); err != nil { + return err + } + for varName, entry := range vars { + path := entry.From + extracted, err := extractFrom(path, bodyMap) + if err != nil { + return err + } + debug.Printf(ctx, "save.vars: %s -> %v", varName, extracted) + res.SetData(varName, extracted) + } + return nil +} + +func extractFrom(path string, out any) (any, error) { + var normalized any + switch out := out.(type) { + case map[string]any: + normalized = out + case []map[string]any: + normalized = out + default: + return nil, fmt.Errorf("unhandled extract type %T", out) + } + p, err := jsonpath.Parse(path) + if err != nil { + // Not terminal because during parse we validate the JSONPath + // expression is valid. + return nil, gdtjson.JSONPathNotFound(path, err) + } + nodes := p.Select(normalized) + if len(nodes) == 0 { + return nil, gdtjson.JSONPathNotFound(path, err) + } + got := nodes[0] + return got, nil +} From 78cdb86123d1fc74d2f118ea3f5281c7e7743b1f Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Mon, 22 Sep 2025 10:36:50 -0400 Subject: [PATCH 2/3] add updated Github actions workflows Signed-off-by: Jay Pipes --- .github/workflows/fmtcheck.yml | 36 ++++++++++++++++ .github/workflows/gate-tests.yml | 34 ---------------- .github/workflows/lint.yml | 33 +++++++++++++++ .github/workflows/test.yml | 70 ++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/fmtcheck.yml delete mode 100644 .github/workflows/gate-tests.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/fmtcheck.yml b/.github/workflows/fmtcheck.yml new file mode 100644 index 0000000..51b8e4c --- /dev/null +++ b/.github/workflows/fmtcheck.yml @@ -0,0 +1,36 @@ +name: fmtcheck + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + fmtcheck: + runs-on: ubuntu-latest + steps: + - name: harden runner + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: block + disable-sudo: true + allowed-endpoints: > + github.com:443 + api.github.com:443 + proxy.github.com:443 + proxy.golang.org:443 + raw.githubusercontent.com:443 + objects.githubusercontent.com:443 + proxy.golang.org:443 + - name: checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: setup go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: 1.21 + - name: check fmt + run: 'bash -c "diff -u <(echo -n) <(gofmt -d .)"' diff --git a/.github/workflows/gate-tests.yml b/.github/workflows/gate-tests.yml deleted file mode 100644 index 6553a6c..0000000 --- a/.github/workflows/gate-tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: gate tests - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: - contents: read - -jobs: - test: - strategy: - matrix: - go-version: [1.19.x, 1.20.x] - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - name: harden runner - uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 - with: - egress-policy: block - disable-sudo: true - allowed-endpoints: > - github.com:443 - proxy.golang.org:443 - - name: checkout code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: setup go - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 - with: - go-version: ${{ matrix.go }} - - run: go test -v ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..470f56e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,33 @@ +name: lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: read # needed for only-new-issues option below + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: harden runner + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + with: + egress-policy: audit + disable-sudo: true + - name: checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: setup go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: 1.24 + - name: lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: v2.2.0 + args: --timeout=5m0s --verbose + only-new-issues: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bff891c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,70 @@ +name: gate tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + test-mac-windows: + strategy: + matrix: + go: ['1.23'] + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: harden runner + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: block + disable-sudo: true + allowed-endpoints: > + github.com:443 + api.github.com:443 + proxy.github.com:443 + raw.githubusercontent.com:443 + objects.githubusercontent.com:443 + proxy.golang.org:443 + blob.core.windows.net:443 + - name: checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: setup go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: ${{ matrix.go }} + check-latest: true + - name: run tests + run: make test + test-linux: + strategy: + matrix: + go: ['1.21', '1.22', '1.23'] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: harden runner + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: block + disable-sudo: true + allowed-endpoints: > + github.com:443 + api.github.com:443 + proxy.github.com:443 + raw.githubusercontent.com:443 + objects.githubusercontent.com:443 + proxy.golang.org:443 + blob.core.windows.net:443 + - name: checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: setup go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: ${{ matrix.go }} + check-latest: true + - name: run tests + run: make test From e7983bc7b36684dbbed5ed2f3a8a6ef49f50de6f Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Mon, 22 Sep 2025 10:43:01 -0400 Subject: [PATCH 3/3] lint fixes Signed-off-by: Jay Pipes --- action.go | 26 +++++++++++++++++--------- eval.go | 4 ++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/action.go b/action.go index 2b912bc..48f0d78 100644 --- a/action.go +++ b/action.go @@ -10,7 +10,6 @@ import ( "encoding/json" "fmt" "io" - "net/http" nethttp "net/http" "reflect" "strings" @@ -43,9 +42,9 @@ type Action struct { // runtime error. func (a *Action) Do( ctx context.Context, - c *http.Client, + c *nethttp.Client, defaults *Defaults, -) (*http.Response, error) { +) (*nethttp.Response, error) { url, err := a.getURL(ctx, defaults) if err != nil { return nil, err @@ -54,7 +53,9 @@ func (a *Action) Do( debug.Printf(ctx, "http: > %s %s", a.Method, url) var reqData io.Reader if a.Data != nil { - a.processRequestData(ctx) + if err := a.processRequestData(ctx); err != nil { + return nil, err + } jsonBody, err := json.Marshal(a.Data) if err != nil { return nil, err @@ -63,7 +64,7 @@ func (a *Action) Do( if b.Size() > 0 { sendData, _ := io.ReadAll(b) debug.Printf(ctx, "http: > %s", sendData) - b.Seek(0, 0) + b.Seek(0, 0) // nolint:errcheck } reqData = b } @@ -109,9 +110,9 @@ func (a *Action) getURL( // expressions. If we find any, we query the fixture registry to see if any // fixtures have a value that matches the JSONPath expression. See // gdt.fixtures:jsonFixture for more information on how this works -func (a *Action) processRequestData(ctx context.Context) { +func (a *Action) processRequestData(ctx context.Context) error { if a.Data == nil { - return + return nil } // Get a pointer to the unmarshaled interface{} so we can mutate the // contents pointed to @@ -127,11 +128,18 @@ func (a *Action) processRequestData(ctx context.Context) { for i := 0; i < v.Len(); i++ { item := v.Index(i).Elem() it := item.Type() - a.preprocessMap(ctx, item, it.Key(), it.Elem()) + err := a.preprocessMap(ctx, item, it.Key(), it.Elem()) + if err != nil { + return err + } } case reflect.Map: - a.preprocessMap(ctx, v, vt.Key(), vt.Elem()) + err := a.preprocessMap(ctx, v, vt.Key(), vt.Elem()) + if err != nil { + return err + } } + return nil } // processRequestDataMap processes a map pointed to by v, transforming any diff --git a/eval.go b/eval.go index 92ef835..55394ae 100644 --- a/eval.go +++ b/eval.go @@ -32,8 +32,8 @@ func (s *Spec) Eval(ctx context.Context) (*api.Result, error) { // Make sure we drain and close our response body... defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() + io.Copy(ioutil.Discard, resp.Body) // nolint:errcheck + resp.Body.Close() // nolint:errcheck }() // Only read the response body contents once and pass the byte