Skip to content

Commit

Permalink
Support header string + array for request & response
Browse files Browse the repository at this point in the history
Also, support ":control" in header for the flattened version of the values.

Added documentation for body:control

see #73980

Co-Authored-By: Philipp Hempel <phempel@programmfabrik.de>
  • Loading branch information
martinrode and phempel committed Nov 1, 2024
1 parent af151cc commit 534d380
Show file tree
Hide file tree
Showing 28 changed files with 387 additions and 218 deletions.
12 changes: 2 additions & 10 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: execute unit tests
- name: execute unit tests, build & apitest
shell: bash
run: make test

- name: build executable for test suite
shell: bash
run: make build

- name: execute apitest
shell: bash
run: make apitest
run: make all

- name: Notify slack channel about a failure
if: ${{ failure() }}
Expand Down
38 changes: 5 additions & 33 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,44 +1,16 @@
GOOS ?= linux
GOARCH ?= amd64
GIT_COMMIT_SHA ?= $(shell git rev-list -1 HEAD)
LD_FLAGS = -ldflags="-X main.buildCommit=${GIT_COMMIT_SHA}"

all: test build apitest

deps:
go mod download github.com/clbanning/mxj
go get ./...

vet:
test:
go vet ./...

fmt:
go fmt ./...

test: deps fmt vet
go test -race -cover ./...

webtest:
go test -coverprofile=testcoverage.out
go tool cover -html=testcoverage.out

apitest:
apitest: build
./apitest -c apitest.test.yml --stop-on-fail -d test/

gox: deps
go get github.com/mitchellh/gox
gox ${LDFLAGS} -parallel=4 -output="./bin/apitest_{{.OS}}_{{.Arch}}"

clean:
rm -rfv ./apitest ./bin/* ./testcoverage.out

ci: deps
go build $(LD_FLAGS) -o bin/apitest_$(GOOS)_$(GOARCH) *.go

build: deps
go build $(LD_FLAGS)

build-linux: deps
GOOS=linux GOARCH=amd64 go build -o apitest-linux
build:
go build

.PHONY: all test apitest webtest gox build clean
.PHONY: all test apitest build clean
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
"query_params_from_store": {
"format": "formatFromDatastore",
// If the datastore key starts with an ?, wo do not throw an error if the key could not be found, but just
// do not set the query param. If the key "a" is not found it datastore, the queryparameter test will not be set
// do not set the query param. If the key "a" is not found it datastore, the query parameter test will not be set
"test": "?a"
},

// Additional headers that should be added to the request
"header": {
"header1": "value",
"header2": "value"
"header2": ["value1", "value2"]
},

// Cookies can be added to the request
Expand Down Expand Up @@ -270,7 +270,7 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
}
],

// With header_from_you set a header to the value of the dat astore field
// With header_from_store you set a header to the value of the datastore field
// In this example we set the "Content-Type" header to the value "application/json"
// As "application/json" is stored as string in the datastore on index "contentType"
"header_from_store": {
Expand Down Expand Up @@ -298,13 +298,22 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
// Expected http status code. See api documentation vor the right ones
"statuscode": 200,

// If you expect certain response headers, you can define them here. A single key can have mulitble headers (as defined in rfc2616)
// If you expect certain response headers, you can define them here. A single key can have multiple headers (as defined in rfc2616)
"header": {
"key1": [
"val1",
"val2",
"val3"
],

// Headers sharing the same key are concatenated using ";", if the comparison value is a simple string,
// thus "key1" can also be checked like this:
"key1": "val1;val2;val3"

// :control in header is always applied to the flat format
"key1:control": {
// see below, this is not applied against the array
},
"x-easydb-token": [
"csdklmwerf8ßwji02kopwfjko2"
]
Expand Down Expand Up @@ -808,6 +817,9 @@ In the example we use the jsonObject `test` and define some control structures o
}
```

### `body:control`

All controls, which are defined below, can also be applied to the complete response body itself by setting `body:control`. The control check functions work the same as on any other key. This can be combined with other controls inside the body.

## Available controls

Expand Down Expand Up @@ -1931,10 +1943,7 @@ The datastore stores all responses in a list. We can retrieve the response (as a
{
"statuscode": 200,
"header": {
"foo": [
"bar",
"baz"
]
"foo": "bar;baz"
},
"body": "..."
}
Expand Down Expand Up @@ -2658,7 +2667,7 @@ The endpoint `bounce` returns the binary of the request body, as well as the req
"param1": "abc"
},
"header": {
"header1": 123
"header1": "123"
},
"body": {
"file": "@path/to/file.jpg"
Expand Down
10 changes: 5 additions & 5 deletions api_testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Case struct {
index int
dataStore *datastore.Datastore

standardHeader map[string]*string
standardHeader map[string]any // can be string or []string
standardHeaderFromStore map[string]string

ServerURL string `json:"server_url"`
Expand Down Expand Up @@ -471,7 +471,7 @@ func (testCase Case) loadRequest() (api.Request, error) {
func (testCase Case) loadExpectedResponse() (res api.Response, err error) {
// unspecified response is interpreted as status_code 200
if testCase.ResponseData == nil {
return api.NewResponse(http.StatusOK, nil, nil, nil, nil, nil, res.Format)
return api.NewResponse(http.StatusOK, nil, nil, nil, nil, res.Format)
}
spec, err := testCase.loadResponseSerialization(testCase.ResponseData)
if err != nil {
Expand Down Expand Up @@ -523,10 +523,10 @@ func (testCase Case) loadRequestSerialization() (api.Request, error) {
spec.ServerURL = testCase.ServerURL
}
if len(spec.Headers) == 0 {
spec.Headers = make(map[string]*string)
spec.Headers = make(map[string]any)
}
for k, v := range testCase.standardHeader {
if spec.Headers[k] == nil {
if _, exist := spec.Headers[k]; !exist {
spec.Headers[k] = v
}
}
Expand All @@ -535,7 +535,7 @@ func (testCase Case) loadRequestSerialization() (api.Request, error) {
spec.HeaderFromStore = make(map[string]string)
}
for k, v := range testCase.standardHeaderFromStore {
if spec.HeaderFromStore[k] == "" {
if _, exist := spec.HeaderFromStore[k]; !exist {
spec.HeaderFromStore[k] = v
}
}
Expand Down
13 changes: 6 additions & 7 deletions api_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"os"
Expand All @@ -14,7 +14,6 @@ import (
"sync/atomic"
"time"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/programmfabrik/apitest/internal/httpproxy"
Expand Down Expand Up @@ -43,8 +42,8 @@ type Suite struct {
Tests []any `json:"tests"`
Store map[string]any `json:"store"`

StandardHeader map[string]*string `yaml:"header" json:"header"`
StandardHeaderFromStore map[string]string `yaml:"header_from_store" json:"header_from_store"`
StandardHeader map[string]any `yaml:"header" json:"header"`
StandardHeaderFromStore map[string]string `yaml:"header_from_store" json:"header_from_store"`

Config TestToolConfig
datastore *datastore.Datastore
Expand Down Expand Up @@ -104,7 +103,7 @@ func NewTestSuite(config TestToolConfig, manifestPath string, manifestDir string
if httpServerReplaceHost != "" {
_, err = url.Parse("//" + httpServerReplaceHost)
if err != nil {
return nil, errors.Wrap(err, "set http_server_host failed (command argument)")
return nil, fmt.Errorf("set http_server_host failed (command argument): %w", err)
}
}
if suitePreload.HttpServer != nil {
Expand All @@ -115,7 +114,7 @@ func NewTestSuite(config TestToolConfig, manifestPath string, manifestDir string
// We need to append it as the golang URL parser is not smart enough to differenciate between hostname and protocol
_, err = url.Parse("//" + preloadHTTPAddrStr)
if err != nil {
return nil, errors.Wrap(err, "set http_server_host failed (manifesr addr)")
return nil, fmt.Errorf("set http_server_host failed (manifesr addr): %w", err)
}
}
suitePreload.HTTPServerHost = httpServerReplaceHost
Expand Down Expand Up @@ -440,7 +439,7 @@ func (ats *Suite) loadManifest() ([]byte, error) {
}
defer manifestFile.Close()

manifestTmpl, err := ioutil.ReadAll(manifestFile)
manifestTmpl, err := io.ReadAll(manifestFile)
if err != nil {
return res, fmt.Errorf("error loading manifest (%s): %s", ats.manifestPath, err)
}
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/programmfabrik/go-test-utils v0.0.0-20191114143449-b8e16b04adb1 h1:NbjvVAvjVfIse7/zgFpP0P93Hj3o4lRJCzGgkNQ87Gc=
github.com/programmfabrik/go-test-utils v0.0.0-20191114143449-b8e16b04adb1/go.mod h1:6Tg7G+t9KYiFa0sU8PpISt9RUgIpgrEI+tXvWz3tSIU=
github.com/programmfabrik/golib v0.0.0-20240226091422-733aede66819 h1:lJ+a0MLo4Dn2UTF0Q/nh9msLqP8MaNEL/RbJLop022g=
github.com/programmfabrik/golib v0.0.0-20240226091422-733aede66819/go.mod h1:qb4pSUhPsZ/UfvM/MBNwKHb6W7xL85uSi4od9emNHHw=
github.com/programmfabrik/golib v0.0.0-20240701125551-843bc5e3be55 h1:VBYGpSvjwHSa5ARrs6uPlUOJF1+n6rFWn49+++h20IU=
github.com/programmfabrik/golib v0.0.0-20240701125551-843bc5e3be55/go.mod h1:qb4pSUhPsZ/UfvM/MBNwKHb6W7xL85uSi4od9emNHHw=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
Expand Down
3 changes: 1 addition & 2 deletions http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
Expand Down Expand Up @@ -155,7 +154,7 @@ func bounceJSON(w http.ResponseWriter, r *http.Request) {
bodyJSON, errorBody any
)

bodyBytes, err = ioutil.ReadAll(r.Body)
bodyBytes, err = io.ReadAll(r.Body)

if utf8.Valid(bodyBytes) {
if len(bodyBytes) > 0 {
Expand Down
16 changes: 7 additions & 9 deletions internal/httpproxy/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ package httpproxy
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"strconv"

"github.com/pkg/errors"

"github.com/programmfabrik/apitest/internal/handlerutil"
)

Expand Down Expand Up @@ -63,9 +61,9 @@ func (st *store) write(w http.ResponseWriter, r *http.Request) {
offset := len(st.Data)

if r.Body != nil {
reqData.Body, err = ioutil.ReadAll(r.Body)
reqData.Body, err = io.ReadAll(r.Body)
if err != nil {
handlerutil.RespondWithErr(w, http.StatusInternalServerError, errors.Errorf("Could not read request body: %s", err))
handlerutil.RespondWithErr(w, http.StatusInternalServerError, fmt.Errorf("Could not read request body: %w", err))
return
}
}
Expand All @@ -76,7 +74,7 @@ func (st *store) write(w http.ResponseWriter, r *http.Request) {
Offset int `json:"offset"`
}{offset})
if err != nil {
handlerutil.RespondWithErr(w, http.StatusInternalServerError, errors.Errorf("Could not encode response: %s", err))
handlerutil.RespondWithErr(w, http.StatusInternalServerError, fmt.Errorf("Could not encode response: %w", err))
}
}

Expand All @@ -94,14 +92,14 @@ func (st *store) read(w http.ResponseWriter, r *http.Request) {
if offsetStr != "" {
offset, err = strconv.Atoi(offsetStr)
if err != nil {
handlerutil.RespondWithErr(w, http.StatusBadRequest, errors.Errorf("Invalid offset %s", offsetStr))
handlerutil.RespondWithErr(w, http.StatusBadRequest, fmt.Errorf("Invalid offset %s", offsetStr))
return
}
}

count := len(st.Data)
if offset >= count {
handlerutil.RespondWithErr(w, http.StatusBadRequest, errors.Errorf("Offset (%d) is higher than count (%d)", offset, count))
handlerutil.RespondWithErr(w, http.StatusBadRequest, fmt.Errorf("Offset (%d) is higher than count (%d)", offset, count))
return
}

Expand All @@ -126,6 +124,6 @@ func (st *store) read(w http.ResponseWriter, r *http.Request) {

_, err = w.Write(req.Body)
if err != nil {
handlerutil.RespondWithErr(w, http.StatusInternalServerError, errors.Errorf("Could not encode response: %s", err))
handlerutil.RespondWithErr(w, http.StatusInternalServerError, fmt.Errorf("Could not encode response: %w", err))
}
}
Loading

0 comments on commit 534d380

Please sign in to comment.