snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
1
server/vendor/github.com/steinfletcher/apitest-jsonpath/.gitignore
generated
vendored
Normal file
1
server/vendor/github.com/steinfletcher/apitest-jsonpath/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea/
|
||||
2
server/vendor/github.com/steinfletcher/apitest-jsonpath/Makefile
generated
vendored
Normal file
2
server/vendor/github.com/steinfletcher/apitest-jsonpath/Makefile
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test:
|
||||
go test -v .
|
||||
196
server/vendor/github.com/steinfletcher/apitest-jsonpath/README.md
generated
vendored
Normal file
196
server/vendor/github.com/steinfletcher/apitest-jsonpath/README.md
generated
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||

|
||||
|
||||
# apitest-jsonpath
|
||||
|
||||
This library provides jsonpath assertions for [apitest](https://github.com/steinfletcher/apitest).
|
||||
|
||||
# Installation
|
||||
|
||||
```bash
|
||||
go get -u github.com/steinfletcher/apitest-jsonpath
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Equal
|
||||
|
||||
`Equal` checks for value equality when the json path expression returns a single result. Given the response is `{"id": 12345}`
|
||||
|
||||
```go
|
||||
apitest.New(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Equal(`$.id`, float64(12345))).
|
||||
End()
|
||||
```
|
||||
|
||||
We can also provide more complex expected values. Given the response `{"message": "hello", "id": 12345}`.
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Equal(`$`, map[string]interface{}{"message": "hello", "id": float64(12345)})).
|
||||
End()
|
||||
```
|
||||
|
||||
### NotEqual
|
||||
|
||||
`NotEqual` checks that the json path expression value is not equal to given value
|
||||
|
||||
```go
|
||||
apitest.New(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.NotEqual(`$.a`, float64(56789))).
|
||||
End()
|
||||
```
|
||||
|
||||
we can also provide more complex expected values
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.NotEqual(`$`, map[string]interface{}{"a": "hello", "b": float64(56789)})).
|
||||
End()
|
||||
```
|
||||
|
||||
given the response is `{"a": "hello", "b": 12345}`
|
||||
|
||||
### Contains
|
||||
|
||||
When the jsonpath expression returns an array, use `Contains` to assert that the expected value is contained in the result. Given the response is `{"a": 12345, "b": [{"key": "c", "value": "result"}]}`, we can assert on the result like so
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
|
||||
End()
|
||||
```
|
||||
|
||||
### Present / NotPresent
|
||||
|
||||
Use `Present` and `NotPresent` to check the presence of a field in the response without evaluating its value.
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Present(`$.a`)).
|
||||
Assert(jsonpath.NotPresent(`$.password`)).
|
||||
End()
|
||||
```
|
||||
|
||||
### Matches
|
||||
|
||||
Use `Matches` to check that a single path element of type string, number or bool matches a regular expression.
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Matches(`$.a`, `^[abc]{1,3}$`)).
|
||||
End()
|
||||
```
|
||||
|
||||
### Len
|
||||
|
||||
Use `Len` to check to the length of the returned value. Given the response is `{"items": [1, 2, 3]}`, we can assert on the length of items like so
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/articles?category=golang").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Len(`$.items`, 3)).
|
||||
End()
|
||||
```
|
||||
|
||||
### GreaterThan
|
||||
|
||||
Use `GreaterThan` to enforce a minimum length on the returned value.
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/articles?category=golang").
|
||||
Expect(t).
|
||||
Assert(jsonpath.GreaterThan(`$.items`, 2)).
|
||||
End()
|
||||
```
|
||||
|
||||
### LessThan
|
||||
|
||||
Use `LessThan` to enforce a maximum length on the returned value.
|
||||
|
||||
```go
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/articles?category=golang").
|
||||
Expect(t).
|
||||
Assert(jsonpath.LessThan(`$.items`, 4)).
|
||||
End()
|
||||
```
|
||||
|
||||
### JWT matchers
|
||||
|
||||
`JWTHeaderEqual` and `JWTPayloadEqual` can be used to assert on the contents of the JWT in the response (it does not verify a JWT).
|
||||
|
||||
```go
|
||||
func TestX(t *testing.T) {
|
||||
apitest.New().
|
||||
HandlerFunc(myHandler).
|
||||
Post("/login").
|
||||
Expect(t).
|
||||
Assert(jsonpath.JWTPayloadEqual(fromAuthHeader, `$.sub`, "1234567890")).
|
||||
Assert(jsonpath.JWTHeaderEqual(fromAuthHeader, `$.alg`, "HS256")).
|
||||
End()
|
||||
}
|
||||
|
||||
func fromAuthHeader(res *http.Response) (string, error) {
|
||||
return res.Header.Get("Authorization"), nil
|
||||
}
|
||||
```
|
||||
|
||||
### Chain
|
||||
|
||||
`Chain` is used to provide several assertions at once
|
||||
|
||||
```go
|
||||
Assert(
|
||||
jsonpath.Chain().
|
||||
Equal("a", "1").
|
||||
NotEqual("b", "2").
|
||||
Present("c").
|
||||
End(),
|
||||
).
|
||||
```
|
||||
|
||||
### Root
|
||||
|
||||
`Root` is used to avoid duplicated paths in body expectations. For example, instead of writing:
|
||||
|
||||
```go
|
||||
Assert(jsonpath.Equal("a.b.c.d", "a").
|
||||
Assert(jsonpath.Equal("a.b.c.e", "b").
|
||||
Assert(jsonpath.Equal("a.b.c.f", "c").
|
||||
```
|
||||
|
||||
it is possible to define a root path like so
|
||||
|
||||
```go
|
||||
Assert(
|
||||
jsonpath.Root("$.a.b.c").
|
||||
Equal("d", "a").
|
||||
Equal("e", "b").
|
||||
Equal("f", "c").
|
||||
End(),
|
||||
).
|
||||
```
|
||||
73
server/vendor/github.com/steinfletcher/apitest-jsonpath/http/http.go
generated
vendored
Normal file
73
server/vendor/github.com/steinfletcher/apitest-jsonpath/http/http.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func CopyResponse(response *http.Response) *http.Response {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resBodyBytes []byte
|
||||
if response.Body != nil {
|
||||
resBodyBytes, _ = ioutil.ReadAll(response.Body)
|
||||
response.Body = ioutil.NopCloser(bytes.NewBuffer(resBodyBytes))
|
||||
}
|
||||
|
||||
resCopy := &http.Response{
|
||||
Header: map[string][]string{},
|
||||
StatusCode: response.StatusCode,
|
||||
Status: response.Status,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer(resBodyBytes)),
|
||||
Proto: response.Proto,
|
||||
ProtoMinor: response.ProtoMinor,
|
||||
ProtoMajor: response.ProtoMajor,
|
||||
ContentLength: response.ContentLength,
|
||||
}
|
||||
|
||||
for name, values := range response.Header {
|
||||
resCopy.Header[name] = values
|
||||
}
|
||||
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func CopyRequest(request *http.Request) *http.Request {
|
||||
resCopy := &http.Request{
|
||||
Method: request.Method,
|
||||
Host: request.Host,
|
||||
Proto: request.Proto,
|
||||
ProtoMinor: request.ProtoMinor,
|
||||
ProtoMajor: request.ProtoMajor,
|
||||
ContentLength: request.ContentLength,
|
||||
RemoteAddr: request.RemoteAddr,
|
||||
}
|
||||
resCopy = resCopy.WithContext(request.Context())
|
||||
|
||||
if request.Body != nil {
|
||||
bodyBytes, _ := ioutil.ReadAll(request.Body)
|
||||
resCopy.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
|
||||
if request.URL != nil {
|
||||
r2URL := new(url.URL)
|
||||
*r2URL = *request.URL
|
||||
resCopy.URL = r2URL
|
||||
}
|
||||
|
||||
headers := make(http.Header)
|
||||
for k, values := range request.Header {
|
||||
for _, hValue := range values {
|
||||
headers.Add(k, hValue)
|
||||
}
|
||||
}
|
||||
resCopy.Header = headers
|
||||
|
||||
return resCopy
|
||||
}
|
||||
|
||||
169
server/vendor/github.com/steinfletcher/apitest-jsonpath/jsonpath.go
generated
vendored
Normal file
169
server/vendor/github.com/steinfletcher/apitest-jsonpath/jsonpath.go
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
httputil "github.com/steinfletcher/apitest-jsonpath/http"
|
||||
"github.com/steinfletcher/apitest-jsonpath/jsonpath"
|
||||
"net/http"
|
||||
"reflect"
|
||||
regex "regexp"
|
||||
)
|
||||
|
||||
// Contains is a convenience function to assert that a jsonpath expression extracts a value in an array
|
||||
func Contains(expression string, expected interface{}) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.Contains(expression, expected, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Equal is a convenience function to assert that a jsonpath expression extracts a value
|
||||
func Equal(expression string, expected interface{}) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.Equal(expression, expected, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual is a function to check json path expression value is not equal to given value
|
||||
func NotEqual(expression string, expected interface{}) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.NotEqual(expression, expected, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Len asserts that value is the expected length, determined by reflect.Len
|
||||
func Len(expression string, expectedLength int) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.Length(expression, expectedLength, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// GreaterThan asserts that value is greater than the given length, determined by reflect.Len
|
||||
func GreaterThan(expression string, minimumLength int) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.GreaterThan(expression, minimumLength, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// LessThan asserts that value is less than the given length, determined by reflect.Len
|
||||
func LessThan(expression string, maximumLength int) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.LessThan(expression, maximumLength, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Present asserts that value returned by the expression is present
|
||||
func Present(expression string) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.Present(expression, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// NotPresent asserts that value returned by the expression is not present
|
||||
func NotPresent(expression string) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
return jsonpath.NotPresent(expression, res.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Matches asserts that the value matches the given regular expression
|
||||
func Matches(expression string, regexp string) func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
pattern, err := regex.Compile(regexp)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("invalid pattern: '%s'", regexp))
|
||||
}
|
||||
value, _ := jsonpath.JsonPath(res.Body, expression)
|
||||
if value == nil {
|
||||
return errors.New(fmt.Sprintf("no match for pattern: '%s'", expression))
|
||||
}
|
||||
kind := reflect.ValueOf(value).Kind()
|
||||
switch kind {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Uintptr,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
if !pattern.Match([]byte(fmt.Sprintf("%v", value))) {
|
||||
return errors.New(fmt.Sprintf("value '%v' does not match pattern '%v'", value, regexp))
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New(fmt.Sprintf("unable to match using type: %s", kind.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chain creates a new assertion chain
|
||||
func Chain() *AssertionChain {
|
||||
return &AssertionChain{rootExpression: ""}
|
||||
}
|
||||
|
||||
// Root creates a new assertion chain prefixed with the given expression
|
||||
func Root(expression string) *AssertionChain {
|
||||
return &AssertionChain{rootExpression: expression + "."}
|
||||
}
|
||||
|
||||
// AssertionChain supports chaining assertions and root expressions
|
||||
type AssertionChain struct {
|
||||
rootExpression string
|
||||
assertions []func(*http.Response, *http.Request) error
|
||||
}
|
||||
|
||||
// Equal adds an Equal assertion to the chain
|
||||
func (r *AssertionChain) Equal(expression string, expected interface{}) *AssertionChain {
|
||||
r.assertions = append(r.assertions, Equal(r.rootExpression+expression, expected))
|
||||
return r
|
||||
}
|
||||
|
||||
// NotEqual adds an NotEqual assertion to the chain
|
||||
func (r *AssertionChain) NotEqual(expression string, expected interface{}) *AssertionChain {
|
||||
r.assertions = append(r.assertions, NotEqual(r.rootExpression+expression, expected))
|
||||
return r
|
||||
}
|
||||
|
||||
// Contains adds an Contains assertion to the chain
|
||||
func (r *AssertionChain) Contains(expression string, expected interface{}) *AssertionChain {
|
||||
r.assertions = append(r.assertions, Contains(r.rootExpression+expression, expected))
|
||||
return r
|
||||
}
|
||||
|
||||
// Present adds an Present assertion to the chain
|
||||
func (r *AssertionChain) Present(expression string) *AssertionChain {
|
||||
r.assertions = append(r.assertions, Present(r.rootExpression+expression))
|
||||
return r
|
||||
}
|
||||
|
||||
// NotPresent adds an NotPresent assertion to the chain
|
||||
func (r *AssertionChain) NotPresent(expression string) *AssertionChain {
|
||||
r.assertions = append(r.assertions, NotPresent(r.rootExpression+expression))
|
||||
return r
|
||||
}
|
||||
|
||||
// Matches adds an Matches assertion to the chain
|
||||
func (r *AssertionChain) Matches(expression, regexp string) *AssertionChain {
|
||||
r.assertions = append(r.assertions, Matches(r.rootExpression+expression, regexp))
|
||||
return r
|
||||
}
|
||||
|
||||
// End returns an func(*http.Response, *http.Request) error which is a combination of the registered assertions
|
||||
func (r *AssertionChain) End() func(*http.Response, *http.Request) error {
|
||||
return func(res *http.Response, req *http.Request) error {
|
||||
for _, assertion := range r.assertions {
|
||||
if err := assertion(httputil.CopyResponse(res), httputil.CopyRequest(req)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
200
server/vendor/github.com/steinfletcher/apitest-jsonpath/jsonpath/jsonpath.go
generated
vendored
Normal file
200
server/vendor/github.com/steinfletcher/apitest-jsonpath/jsonpath/jsonpath.go
generated
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PaesslerAG/jsonpath"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Contains(expression string, expected interface{}, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, found := IncludesElement(value, expected)
|
||||
if !ok {
|
||||
return errors.New(fmt.Sprintf("\"%s\" could not be applied builtin len()", expected))
|
||||
}
|
||||
if !found {
|
||||
return errors.New(fmt.Sprintf("\"%s\" does not contain \"%s\"", value, expected))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Equal(expression string, expected interface{}, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ObjectsAreEqual(value, expected) {
|
||||
return errors.New(fmt.Sprintf("\"%s\" not equal to \"%s\"", value, expected))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotEqual(expression string, expected interface{}, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ObjectsAreEqual(value, expected) {
|
||||
return errors.New(fmt.Sprintf("\"%s\" value is equal to \"%s\"", expression, expected))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Length(expression string, expectedLength int, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Len() != expectedLength {
|
||||
return errors.New(fmt.Sprintf("\"%d\" not equal to \"%d\"", v.Len(), expectedLength))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GreaterThan(expression string, minimumLength int, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Len() < minimumLength {
|
||||
return errors.New(fmt.Sprintf("\"%d\" is greater than \"%d\"", v.Len(), minimumLength))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LessThan(expression string, maximumLength int, data io.Reader) error {
|
||||
value, err := JsonPath(data, expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Len() > maximumLength {
|
||||
return errors.New(fmt.Sprintf("\"%d\" is less than \"%d\"", v.Len(), maximumLength))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Present(expression string, data io.Reader) error {
|
||||
value, _ := JsonPath(data, expression)
|
||||
if isEmpty(value) {
|
||||
return errors.New(fmt.Sprintf("value not present for expression: '%s'", expression))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotPresent(expression string, data io.Reader) error {
|
||||
value, _ := JsonPath(data, expression)
|
||||
if !isEmpty(value) {
|
||||
return errors.New(fmt.Sprintf("value present for expression: '%s'", expression))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func JsonPath(reader io.Reader, expression string) (interface{}, error) {
|
||||
v := interface{}(nil)
|
||||
b, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := jsonpath.Get(expression, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("evaluating '%s' resulted in error: '%s'", expression, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// courtesy of github.com/stretchr/testify
|
||||
func IncludesElement(list interface{}, element interface{}) (ok, found bool) {
|
||||
listValue := reflect.ValueOf(list)
|
||||
elementValue := reflect.ValueOf(element)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ok = false
|
||||
found = false
|
||||
}
|
||||
}()
|
||||
|
||||
if reflect.TypeOf(list).Kind() == reflect.String {
|
||||
return true, strings.Contains(listValue.String(), elementValue.String())
|
||||
}
|
||||
|
||||
if reflect.TypeOf(list).Kind() == reflect.Map {
|
||||
mapKeys := listValue.MapKeys()
|
||||
for i := 0; i < len(mapKeys); i++ {
|
||||
if ObjectsAreEqual(mapKeys[i].Interface(), element) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
for i := 0; i < listValue.Len(); i++ {
|
||||
if ObjectsAreEqual(listValue.Index(i).Interface(), element) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
func ObjectsAreEqual(expected, actual interface{}) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
exp, ok := expected.([]byte)
|
||||
if !ok {
|
||||
return reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
|
||||
act, ok := actual.([]byte)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if exp == nil || act == nil {
|
||||
return exp == nil && act == nil
|
||||
}
|
||||
return bytes.Equal(exp, act)
|
||||
}
|
||||
|
||||
func isEmpty(object interface{}) bool {
|
||||
if object == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
objValue := reflect.ValueOf(object)
|
||||
|
||||
switch objValue.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||
return objValue.Len() == 0
|
||||
case reflect.Ptr:
|
||||
if objValue.IsNil() {
|
||||
return true
|
||||
}
|
||||
deref := objValue.Elem().Interface()
|
||||
return isEmpty(deref)
|
||||
default:
|
||||
zero := reflect.Zero(objValue.Type())
|
||||
return reflect.DeepEqual(object, zero.Interface())
|
||||
}
|
||||
}
|
||||
68
server/vendor/github.com/steinfletcher/apitest-jsonpath/jwt.go
generated
vendored
Normal file
68
server/vendor/github.com/steinfletcher/apitest-jsonpath/jwt.go
generated
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/steinfletcher/apitest-jsonpath/jsonpath"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
jwtHeaderIndex = 0
|
||||
jwtPayloadIndex = 1
|
||||
)
|
||||
|
||||
func JWTHeaderEqual(tokenSelector func(*http.Response) (string, error), expression string, expected interface{}) func(*http.Response, *http.Request) error {
|
||||
return jwtEqual(tokenSelector, expression, expected, jwtHeaderIndex)
|
||||
}
|
||||
|
||||
func JWTPayloadEqual(tokenSelector func(*http.Response) (string, error), expression string, expected interface{}) func(*http.Response, *http.Request) error {
|
||||
return jwtEqual(tokenSelector, expression, expected, jwtPayloadIndex)
|
||||
}
|
||||
|
||||
func jwtEqual(tokenSelector func(*http.Response) (string, error), expression string, expected interface{}, index int) func(*http.Response, *http.Request) error {
|
||||
return func(response *http.Response, request *http.Request) error {
|
||||
token, err := tokenSelector(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
splitErr := errors.New("Invalid token: token should contain header, payload and secret")
|
||||
return splitErr
|
||||
}
|
||||
|
||||
decodedPayload, PayloadErr := base64Decode(parts[index])
|
||||
if PayloadErr != nil {
|
||||
return fmt.Errorf("Invalid jwt: %s", PayloadErr.Error())
|
||||
}
|
||||
|
||||
value, err := jsonpath.JsonPath(bytes.NewReader(decodedPayload), expression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !jsonpath.ObjectsAreEqual(value, expected) {
|
||||
return errors.New(fmt.Sprintf("\"%s\" not equal to \"%s\"", value, expected))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func base64Decode(src string) ([]byte, error) {
|
||||
if l := len(src) % 4; l > 0 {
|
||||
src += strings.Repeat("=", 4-l)
|
||||
}
|
||||
|
||||
decoded, err := base64.URLEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("Decoding Error %s", err)
|
||||
return nil, errMsg
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
2
server/vendor/github.com/steinfletcher/apitest/.codacy.yml
generated
vendored
Normal file
2
server/vendor/github.com/steinfletcher/apitest/.codacy.yml
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
exclude_paths:
|
||||
- 'examples/**/*'
|
||||
6
server/vendor/github.com/steinfletcher/apitest/.gitignore
generated
vendored
Normal file
6
server/vendor/github.com/steinfletcher/apitest/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/bin/
|
||||
.idea/
|
||||
coverage.out
|
||||
resources/_gen/images/
|
||||
.DS_Store
|
||||
.sequence
|
||||
18
server/vendor/github.com/steinfletcher/apitest/CONTRIBUTING.md
generated
vendored
Normal file
18
server/vendor/github.com/steinfletcher/apitest/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Contributing to api-test
|
||||
|
||||
Thank you for taking the time to contribute. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
# Working with forked go repos
|
||||
|
||||
If you haven't worked with forked go repos before, take a look at this blog post for some excellent advice about [contributing to go open source git repositories](https://splice.com/blog/contributing-open-source-git-repositories-go/).
|
||||
|
||||
# PRs
|
||||
|
||||
- Please open PRs against master.
|
||||
- We prefer single commit PRs, but sometimes for multiple commits are justified - use your best judgement.
|
||||
- Please add/modify tests to cover the proposed code changes.
|
||||
- If the PR contains a new feature, please document it in the README.
|
||||
|
||||
# Documentation
|
||||
|
||||
For simple typo fixes and documentation improvements feel free to raise a PR without raising an issue in github. For anything more complicated please file an issue.
|
||||
22
server/vendor/github.com/steinfletcher/apitest/LICENSE
generated
vendored
Normal file
22
server/vendor/github.com/steinfletcher/apitest/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2019 Stein Fletcher <steindfletcher@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
13
server/vendor/github.com/steinfletcher/apitest/Makefile
generated
vendored
Normal file
13
server/vendor/github.com/steinfletcher/apitest/Makefile
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.PHONY: test test-examples docs fmt vet
|
||||
|
||||
test:
|
||||
bash -c 'diff -u <(echo -n) <(go fmt $(go list ./...))'
|
||||
go vet ./...
|
||||
go test ./... -v -covermode=atomic -coverprofile=coverage.out
|
||||
|
||||
test-examples:
|
||||
cd examples && go test -v ./... && \
|
||||
cd sequence-diagrams-with-sqlite-database && make test && cd ..
|
||||
|
||||
docs:
|
||||
cd docs && hugo server -w && cd -
|
||||
342
server/vendor/github.com/steinfletcher/apitest/README.md
generated
vendored
Normal file
342
server/vendor/github.com/steinfletcher/apitest/README.md
generated
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
<p align="center">
|
||||
<img src="https://apitest.dev/static/images/dummy.svg" width="150">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://godoc.org/github.com/steinfletcher/apitest"><img src="https://godoc.org/github.com/steinfletcher/apitest?status.svg" alt="Godoc" /></a>
|
||||
<a href="https://coveralls.io/github/steinfletcher/apitest?branch=master&service=github"><img src="https://coveralls.io/repos/github/steinfletcher/apitest/badge.svg?branch=master" alt="Coverage Status"/></a>
|
||||
<a href="https://circleci.com/gh/steinfletcher/apitest"><img src="https://circleci.com/gh/steinfletcher/apitest.svg?style=shield" alt="Build Status" /></a>
|
||||
<a href="https://goreportcard.com/report/github.com/steinfletcher/apitest"><img src="https://goreportcard.com/badge/github.com/steinfletcher/apitest" alt="Go Report Card" /></a>
|
||||
<a href="https://github.com/avelino/awesome-go/#testing"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go" /></a>
|
||||
</p>
|
||||
|
||||
# apitest
|
||||
|
||||
A simple and extensible behavioural testing library. Supports mocking external http calls and renders sequence diagrams on completion.
|
||||
|
||||
In behavioural tests the internal structure of the app is not known by the tests. Data is input to the system and the outputs are expected to meet certain conditions.
|
||||
|
||||
Join the conversation at #apitest on [https://gophers.slack.com](https://gophers.slack.com).
|
||||
|
||||
<span>Logo by <a target="_blank" href="https://twitter.com/egonelbre">@egonelbre</a><span>
|
||||
|
||||
## Documentation
|
||||
|
||||
Please visit [https://apitest.dev](https://apitest.dev) for the latest documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get -u github.com/steinfletcher/apitest
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Framework and library integration examples
|
||||
|
||||
| Example | Comment |
|
||||
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| [gin](https://github.com/steinfletcher/apitest/tree/master/examples/gin) | popular martini-like web framework |
|
||||
| [graphql](https://github.com/steinfletcher/apitest/tree/master/examples/graphql) | using gqlgen.com to generate a graphql server |
|
||||
| [gorilla](https://github.com/steinfletcher/apitest/tree/master/examples/gorilla) | the gorilla web toolkit |
|
||||
| [iris](https://github.com/steinfletcher/apitest/tree/master/examples/iris) | iris web framework |
|
||||
| [echo](https://github.com/steinfletcher/apitest/tree/master/examples/echo) | High performance, extensible, minimalist Go web framework |
|
||||
| [fiber](https://github.com/steinfletcher/apitest/tree/master/examples/fiber) | Express inspired web framework written in Go |
|
||||
| [httprouter](https://github.com/steinfletcher/apitest/tree/master/examples/httprouter) | High performance HTTP request router that scales well |
|
||||
| [mocks](https://github.com/steinfletcher/apitest/tree/master/examples/mocks) | example mocking out external http calls |
|
||||
| [sequence diagrams](https://github.com/steinfletcher/apitest/tree/master/examples/sequence-diagrams) | generate sequence diagrams from tests. See the [demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/) |
|
||||
| [Ginkgo](https://github.com/steinfletcher/apitest/tree/master/examples/ginkgo) | Ginkgo BDD test framework|
|
||||
|
||||
### Companion libraries
|
||||
|
||||
| Library | Comment |
|
||||
| ----------------------------------------------------------------------- | -----------------------------------------------|
|
||||
| [JSONPath](https://github.com/steinfletcher/apitest-jsonpath) | JSONPath assertion addons |
|
||||
| [CSS Selectors](https://github.com/steinfletcher/apitest-css-selector) | CSS selector assertion addons |
|
||||
| [PlantUML](https://github.com/steinfletcher/apitest-plantuml) | Export sequence diagrams as plantUML |
|
||||
| [DynamoDB](https://github.com/steinfletcher/apitest-dynamodb) | Add DynamoDB interactions to sequence diagrams |
|
||||
|
||||
### Credits
|
||||
|
||||
This library was influenced by the following software packages:
|
||||
|
||||
* [YatSpec](https://github.com/bodar/yatspec) for creating sequence diagrams from tests
|
||||
* [MockMVC](https://spring.io) and [superagent](https://github.com/visionmedia/superagent) for the concept and behavioural testing approach
|
||||
* [Gock](https://github.com/h2non/gock) for the approach to mocking HTTP services in Go
|
||||
* [Baloo](https://github.com/h2non/baloo) for API design
|
||||
|
||||
### Code snippets
|
||||
|
||||
#### JSON body matcher
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.New().
|
||||
Handler(handler).
|
||||
Get("/user/1234").
|
||||
Expect(t).
|
||||
Body(`{"id": "1234", "name": "Tate"}`).
|
||||
Status(http.StatusCreated).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### JSONPath
|
||||
|
||||
For asserting on parts of the response body JSONPath may be used. A separate module must be installed which provides these assertions - `go get -u github.com/steinfletcher/apitest-jsonpath`. This is packaged separately to keep this library dependency free.
|
||||
|
||||
Given the response is `{"a": 12345, "b": [{"key": "c", "value": "result"}]}`
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
and `jsonpath.Equals` checks for value equality
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(jsonpath.Equal(`$.a`, float64(12345))).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom assert functions
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Assert(func(res *http.Response, req *http.Request) error {
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
return nil
|
||||
}).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Assert cookies
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Patch("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Cookies(apitest.Cookie"ABC").Value("12345")).
|
||||
CookiePresent("Session-Token").
|
||||
CookieNotPresent("XXX").
|
||||
Cookies(
|
||||
apitest.Cookie("ABC").Value("12345"),
|
||||
apitest.Cookie("DEF").Value("67890"),
|
||||
).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Assert headers
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Headers(map[string]string{"ABC": "12345"}).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Mocking external http calls
|
||||
|
||||
```go
|
||||
var getUser = apitest.NewMock().
|
||||
Get("/user/12345").
|
||||
RespondWith().
|
||||
Body(`{"name": "jon", "id": "1234"}`).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
|
||||
var getPreferences = apitest.NewMock().
|
||||
Get("/preferences/12345").
|
||||
RespondWith().
|
||||
Body(`{"is_contactable": true}`).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.New().
|
||||
Mocks(getUser, getPreferences).
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Body(`{"name": "jon", "id": "1234"}`).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Generating sequence diagrams from tests
|
||||
|
||||
```go
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.New().
|
||||
Report(apitest.SequenceDiagram()).
|
||||
Mocks(getUser, getPreferences).
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
Body(`{"name": "jon", "id": "1234"}`).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
It is possible to override the default storage location by passing the formatter instance `Report(apitest.NewSequenceDiagramFormatter(".sequence-diagrams"))`.
|
||||
You can bring your own formatter too if you want to produce custom output. By default a sequence diagram is rendered on a html page. See the [demo](http://demo-html.apitest.dev.s3-website-eu-west-1.amazonaws.com/)
|
||||
|
||||
#### Debugging http requests and responses generated by api test and any mocks
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.New().
|
||||
Debug().
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Provide basic auth in the request
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
BasicAuth("username", "password").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Provide cookies in the request
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
Cookies(apitest.Cookie("ABC").Value("12345")).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Provide headers in the request
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Delete("/hello").
|
||||
Headers(map[string]string{"My-Header": "12345"}).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Provide query parameters in the request
|
||||
|
||||
`Query`, `QueryParams` and `QueryCollection` can all be used in combination
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
QueryParams(map[string]string{"a": "1", "b": "2"}).
|
||||
Query("c", "d").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
Providing `{"a": {"b", "c", "d"}` results in parameters encoded as `a=b&a=c&a=d`.
|
||||
`QueryCollection` can be used in combination with `Query`
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Get("/hello").
|
||||
QueryCollection(map[string][]string{"a": {"b", "c", "d"}}).
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Provide a url encoded form body in the request
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Post("/hello").
|
||||
FormData("a", "1").
|
||||
FormData("b", "2").
|
||||
FormData("b", "3").
|
||||
FormData("c", "4", "5", "6").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Capture the request and response data
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.New().
|
||||
Observe(func(res *http.Response, req *http.Request, apiTest *apitest.APITest) {
|
||||
// do something with res and req
|
||||
}).
|
||||
Handler(handler).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
#### Intercept the request
|
||||
|
||||
This is useful for mutating the request before it is sent to the system under test.
|
||||
|
||||
```go
|
||||
func TestApi(t *testing.T) {
|
||||
apitest.Handler(handler).
|
||||
Intercept(func(req *http.Request) {
|
||||
req.URL.RawQuery = "a[]=xxx&a[]=yyy"
|
||||
}).
|
||||
Get("/hello").
|
||||
Expect(t).
|
||||
Status(http.StatusOK).
|
||||
End()
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
View the [contributing guide](CONTRIBUTING.md).
|
||||
1128
server/vendor/github.com/steinfletcher/apitest/apitest.go
generated
vendored
Normal file
1128
server/vendor/github.com/steinfletcher/apitest/apitest.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
332
server/vendor/github.com/steinfletcher/apitest/assert.go
generated
vendored
Normal file
332
server/vendor/github.com/steinfletcher/apitest/assert.go
generated
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// TestingT is an interface to wrap the native *testing.T interface, this allows integration with GinkgoT() interface
|
||||
// GinkgoT interface defined in https://github.com/onsi/ginkgo/blob/55c858784e51c26077949c81b6defb6b97b76944/ginkgo_dsl.go#L91
|
||||
type TestingT interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
Fatal(args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// Verifier is the assertion interface allowing consumers to inject a custom assertion implementation.
|
||||
// It also allows failure scenarios to be tested within apitest
|
||||
type Verifier interface {
|
||||
Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
|
||||
True(t TestingT, value bool, msgAndArgs ...interface{}) bool
|
||||
JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool
|
||||
Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool
|
||||
NoError(t TestingT, err error, msgAndArgs ...interface{}) bool
|
||||
}
|
||||
|
||||
// DefaultVerifier is a verifier that uses some code from https://github.com/stretchr/testify to perform assertions
|
||||
type DefaultVerifier struct{}
|
||||
|
||||
var _ Verifier = DefaultVerifier{}
|
||||
|
||||
func (a DefaultVerifier) True(t TestingT, value bool, msgAndArgs ...interface{}) bool {
|
||||
if !value {
|
||||
return a.Fail(t, "Should be true", msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// JSONEq asserts that two JSON strings are equivalent
|
||||
func (a DefaultVerifier) JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool {
|
||||
var expectedJSONAsInterface, actualJSONAsInterface interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil {
|
||||
return a.Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil {
|
||||
return a.Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...)
|
||||
}
|
||||
|
||||
return a.Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...)
|
||||
}
|
||||
|
||||
func (a DefaultVerifier) Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
if err := validateEqualArgs(expected, actual); err != nil {
|
||||
return a.Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)",
|
||||
expected, actual, err), msgAndArgs...)
|
||||
}
|
||||
|
||||
if !objectsAreEqual(expected, actual) {
|
||||
diff := diff(expected, actual)
|
||||
expected, actual = formatUnequalValues(expected, actual)
|
||||
return a.Fail(t, fmt.Sprintf("Not equal: \n"+
|
||||
"expected: %s\n"+
|
||||
"actual : %s%s", expected, actual, diff), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Fail reports a failure
|
||||
func (a DefaultVerifier) Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
|
||||
content := []labeledContent{
|
||||
{"Error Trace", strings.Join(callerInfo(), "\n\t\t\t")},
|
||||
{"Error", failureMessage},
|
||||
}
|
||||
|
||||
// Add test name if the Go version supports it
|
||||
if n, ok := t.(interface {
|
||||
Name() string
|
||||
}); ok {
|
||||
content = append(content, labeledContent{"Test", n.Name()})
|
||||
}
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
if len(message) > 0 {
|
||||
content = append(content, labeledContent{"Messages", message})
|
||||
}
|
||||
|
||||
t.Errorf("\n%s", ""+labeledOutput(content...))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NoError asserts that a function returned no error
|
||||
func (a DefaultVerifier) NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
if err != nil {
|
||||
return a.Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func formatUnequalValues(expected, actual interface{}) (e string, a string) {
|
||||
if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
|
||||
return fmt.Sprintf("%T(%s)", expected, truncatingFormat(expected)),
|
||||
fmt.Sprintf("%T(%s)", actual, truncatingFormat(actual))
|
||||
}
|
||||
switch expected.(type) {
|
||||
case time.Duration:
|
||||
return fmt.Sprintf("%v", expected), fmt.Sprintf("%v", actual)
|
||||
}
|
||||
return truncatingFormat(expected), truncatingFormat(actual)
|
||||
}
|
||||
|
||||
func truncatingFormat(data interface{}) string {
|
||||
value := fmt.Sprintf("%#v", data)
|
||||
max := bufio.MaxScanTokenSize - 100 // Give us some space the type info too if needed.
|
||||
if len(value) > max {
|
||||
value = value[0:max] + "<... truncated>"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func objectsAreEqual(expected, actual interface{}) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
exp, ok := expected.([]byte)
|
||||
if !ok {
|
||||
return reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
|
||||
act, ok := actual.([]byte)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if exp == nil || act == nil {
|
||||
return exp == nil && act == nil
|
||||
}
|
||||
return bytes.Equal(exp, act)
|
||||
}
|
||||
|
||||
func isFunction(arg interface{}) bool {
|
||||
if arg == nil {
|
||||
return false
|
||||
}
|
||||
return reflect.TypeOf(arg).Kind() == reflect.Func
|
||||
}
|
||||
|
||||
func validateEqualArgs(expected, actual interface{}) error {
|
||||
if expected == nil && actual == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isFunction(expected) || isFunction(actual) {
|
||||
return errors.New("cannot take func type as argument")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
|
||||
if len(msgAndArgs) == 0 || msgAndArgs == nil {
|
||||
return ""
|
||||
}
|
||||
if len(msgAndArgs) == 1 {
|
||||
msg := msgAndArgs[0]
|
||||
if msgAsStr, ok := msg.(string); ok {
|
||||
return msgAsStr
|
||||
}
|
||||
return fmt.Sprintf("%+v", msg)
|
||||
}
|
||||
if len(msgAndArgs) > 1 {
|
||||
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func labeledOutput(content ...labeledContent) string {
|
||||
longestLabel := 0
|
||||
for _, v := range content {
|
||||
if len(v.label) > longestLabel {
|
||||
longestLabel = len(v.label)
|
||||
}
|
||||
}
|
||||
var output string
|
||||
for _, v := range content {
|
||||
output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func indentMessageLines(message string, longestLabelLen int) string {
|
||||
outBuf := new(bytes.Buffer)
|
||||
|
||||
for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
|
||||
if i != 0 {
|
||||
outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t")
|
||||
}
|
||||
outBuf.WriteString(scanner.Text())
|
||||
}
|
||||
|
||||
return outBuf.String()
|
||||
}
|
||||
|
||||
func callerInfo() []string {
|
||||
var pc uintptr
|
||||
var ok bool
|
||||
var file string
|
||||
var line int
|
||||
var name string
|
||||
|
||||
callers := []string{}
|
||||
for i := 0; ; i++ {
|
||||
pc, file, line, ok = runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if file == "<autogenerated>" {
|
||||
break
|
||||
}
|
||||
|
||||
f := runtime.FuncForPC(pc)
|
||||
if f == nil {
|
||||
break
|
||||
}
|
||||
name = f.Name()
|
||||
|
||||
if name == "testing.tRunner" {
|
||||
break
|
||||
}
|
||||
|
||||
parts := strings.Split(file, "/")
|
||||
file = parts[len(parts)-1]
|
||||
if len(parts) > 1 {
|
||||
dir := parts[len(parts)-2]
|
||||
if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" {
|
||||
callers = append(callers, fmt.Sprintf("%s:%d", file, line))
|
||||
}
|
||||
}
|
||||
|
||||
segments := strings.Split(name, ".")
|
||||
name = segments[len(segments)-1]
|
||||
if isTest(name, "Test") ||
|
||||
isTest(name, "Benchmark") ||
|
||||
isTest(name, "Example") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return callers
|
||||
}
|
||||
|
||||
func isTest(name, prefix string) bool {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
return false
|
||||
}
|
||||
if len(name) == len(prefix) { // "Test" is ok
|
||||
return true
|
||||
}
|
||||
r, _ := utf8.DecodeRuneInString(name[len(prefix):])
|
||||
return !unicode.IsLower(r)
|
||||
}
|
||||
|
||||
type labeledContent struct {
|
||||
label string
|
||||
content string
|
||||
}
|
||||
|
||||
// NoopVerifier is a verifier that does not perform verification
|
||||
type NoopVerifier struct{}
|
||||
|
||||
func (n NoopVerifier) True(t TestingT, v bool, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var _ Verifier = NoopVerifier{}
|
||||
|
||||
// Equal does not perform any assertion and always returns true
|
||||
func (n NoopVerifier) Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// JSONEq does not perform any assertion and always returns true
|
||||
func (n NoopVerifier) JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fail does not perform any assertion and always returns true
|
||||
func (n NoopVerifier) Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NoError asserts that a function returned no error
|
||||
func (n NoopVerifier) NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSuccess is a convenience function to assert on a range of happy path status codes
|
||||
var IsSuccess Assert = func(response *http.Response, request *http.Request) error {
|
||||
if response.StatusCode >= 200 && response.StatusCode < 400 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not success. Status code=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
// IsClientError is a convenience function to assert on a range of client error status codes
|
||||
var IsClientError Assert = func(response *http.Response, request *http.Request) error {
|
||||
if response.StatusCode >= 400 && response.StatusCode < 500 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not a client error. Status code=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
// IsServerError is a convenience function to assert on a range of server error status codes
|
||||
var IsServerError Assert = func(response *http.Response, request *http.Request) error {
|
||||
if response.StatusCode >= 500 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not a server error. Status code=%d", response.StatusCode)
|
||||
}
|
||||
193
server/vendor/github.com/steinfletcher/apitest/cookies.go
generated
vendored
Normal file
193
server/vendor/github.com/steinfletcher/apitest/cookies.go
generated
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cookie used to represent an http cookie
|
||||
type Cookie struct {
|
||||
name *string
|
||||
value *string
|
||||
path *string
|
||||
domain *string
|
||||
expires *time.Time
|
||||
maxAge *int
|
||||
secure *bool
|
||||
httpOnly *bool
|
||||
}
|
||||
|
||||
// NewCookie creates a new Cookie with the provided name
|
||||
func NewCookie(name string) *Cookie {
|
||||
return &Cookie{
|
||||
name: &name,
|
||||
}
|
||||
}
|
||||
|
||||
// Value sets the value of the Cookie
|
||||
func (cookie *Cookie) Value(value string) *Cookie {
|
||||
cookie.value = &value
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Path sets the path of the Cookie
|
||||
func (cookie *Cookie) Path(path string) *Cookie {
|
||||
cookie.path = &path
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Domain sets the domain of the Cookie
|
||||
func (cookie *Cookie) Domain(domain string) *Cookie {
|
||||
cookie.domain = &domain
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Expires sets the expires time of the Cookie
|
||||
func (cookie *Cookie) Expires(expires time.Time) *Cookie {
|
||||
cookie.expires = &expires
|
||||
return cookie
|
||||
}
|
||||
|
||||
// MaxAge sets the maxage of the Cookie
|
||||
func (cookie *Cookie) MaxAge(maxAge int) *Cookie {
|
||||
cookie.maxAge = &maxAge
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Secure sets the secure bool of the Cookie
|
||||
func (cookie *Cookie) Secure(secure bool) *Cookie {
|
||||
cookie.secure = &secure
|
||||
return cookie
|
||||
}
|
||||
|
||||
// HttpOnly sets the httpOnly bool of the Cookie
|
||||
func (cookie *Cookie) HttpOnly(httpOnly bool) *Cookie {
|
||||
cookie.httpOnly = &httpOnly
|
||||
return cookie
|
||||
}
|
||||
|
||||
// ToHttpCookie transforms the Cookie to an http cookie
|
||||
func (cookie *Cookie) ToHttpCookie() *http.Cookie {
|
||||
httpCookie := http.Cookie{}
|
||||
|
||||
if cookie.name != nil {
|
||||
httpCookie.Name = *cookie.name
|
||||
}
|
||||
|
||||
if cookie.value != nil {
|
||||
httpCookie.Value = *cookie.value
|
||||
}
|
||||
|
||||
if cookie.path != nil {
|
||||
httpCookie.Path = *cookie.path
|
||||
}
|
||||
|
||||
if cookie.domain != nil {
|
||||
httpCookie.Domain = *cookie.domain
|
||||
}
|
||||
|
||||
if cookie.expires != nil {
|
||||
httpCookie.Expires = *cookie.expires
|
||||
}
|
||||
|
||||
if cookie.maxAge != nil {
|
||||
httpCookie.MaxAge = *cookie.maxAge
|
||||
}
|
||||
|
||||
if cookie.secure != nil {
|
||||
httpCookie.Secure = *cookie.secure
|
||||
}
|
||||
|
||||
if cookie.httpOnly != nil {
|
||||
httpCookie.HttpOnly = *cookie.httpOnly
|
||||
}
|
||||
|
||||
return &httpCookie
|
||||
}
|
||||
|
||||
// FromHTTPCookie transforms an http cookie into a Cookie
|
||||
func FromHTTPCookie(httpCookie *http.Cookie) *Cookie {
|
||||
return NewCookie(httpCookie.Name).
|
||||
Value(httpCookie.Value).
|
||||
Path(httpCookie.Path).
|
||||
Domain(httpCookie.Domain).
|
||||
Expires(httpCookie.Expires).
|
||||
MaxAge(httpCookie.MaxAge).
|
||||
Secure(httpCookie.Secure).
|
||||
HttpOnly(httpCookie.HttpOnly)
|
||||
}
|
||||
|
||||
// Compares cookies based on only the provided fields from Cookie.
|
||||
// Supported fields are Name, Value, Domain, Path, Expires, MaxAge, Secure and HttpOnly
|
||||
func compareCookies(expectedCookie *Cookie, actualCookie *http.Cookie) (bool, []string) {
|
||||
cookieFound := *expectedCookie.name == actualCookie.Name
|
||||
compareErrors := make([]string, 0)
|
||||
if cookieFound {
|
||||
compareErrors = compareValue(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = compareDomain(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = comparePath(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = compareExpires(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = compareMaxAge(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = compareSecure(expectedCookie, actualCookie, compareErrors)
|
||||
compareErrors = compareHttpOnly(expectedCookie, actualCookie, compareErrors)
|
||||
}
|
||||
|
||||
return cookieFound, compareErrors
|
||||
}
|
||||
|
||||
func compareHttpOnly(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.httpOnly != nil && *expectedCookie.httpOnly != actualCookie.HttpOnly {
|
||||
compareErrors = append(compareErrors, formatError("HttpOnly", *expectedCookie.httpOnly, actualCookie.HttpOnly))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func compareSecure(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.secure != nil && *expectedCookie.secure != actualCookie.Secure {
|
||||
compareErrors = append(compareErrors, formatError("Secure", *expectedCookie.secure, actualCookie.Secure))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func compareMaxAge(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.maxAge != nil && *expectedCookie.maxAge != actualCookie.MaxAge {
|
||||
compareErrors = append(compareErrors, formatError("MaxAge", *expectedCookie.maxAge, actualCookie.MaxAge))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func compareExpires(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.expires != nil && !(*expectedCookie.expires).Equal(actualCookie.Expires) {
|
||||
compareErrors = append(compareErrors, formatError("Expires", *expectedCookie.expires, actualCookie.Expires))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func comparePath(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.path != nil && *expectedCookie.path != actualCookie.Path {
|
||||
compareErrors = append(compareErrors, formatError("Path", *expectedCookie.path, actualCookie.Path))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func compareDomain(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.domain != nil && *expectedCookie.domain != actualCookie.Domain {
|
||||
compareErrors = append(compareErrors, formatError("Domain", *expectedCookie.domain, actualCookie.Domain))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func compareValue(expectedCookie *Cookie, actualCookie *http.Cookie, compareErrors []string) []string {
|
||||
if expectedCookie.value != nil && *expectedCookie.value != actualCookie.Value {
|
||||
compareErrors = append(compareErrors, formatError("Value", *expectedCookie.value, actualCookie.Value))
|
||||
}
|
||||
return compareErrors
|
||||
}
|
||||
|
||||
func formatError(name string, expectedValue, actualValue interface{}) string {
|
||||
return fmt.Sprintf("Mismatched field %s. Expected %v but received %v",
|
||||
name,
|
||||
expectedValue,
|
||||
actualValue)
|
||||
}
|
||||
295
server/vendor/github.com/steinfletcher/apitest/diagram.go
generated
vendored
Normal file
295
server/vendor/github.com/steinfletcher/apitest/diagram.go
generated
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
htmlTemplate "html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
htmlTemplateModel struct {
|
||||
Title string
|
||||
SubTitle string
|
||||
StatusCode int
|
||||
BadgeClass string
|
||||
LogEntries []logEntry
|
||||
WebSequenceDSL string
|
||||
MetaJSON htmlTemplate.JS
|
||||
}
|
||||
|
||||
logEntry struct {
|
||||
Header string
|
||||
Body string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// SequenceDiagramFormatter implementation of a ReportFormatter
|
||||
SequenceDiagramFormatter struct {
|
||||
storagePath string
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
fileSystem interface {
|
||||
create(name string) (*os.File, error)
|
||||
mkdirAll(path string, perm os.FileMode) error
|
||||
}
|
||||
|
||||
osFileSystem struct{}
|
||||
|
||||
webSequenceDiagramDSL struct {
|
||||
data bytes.Buffer
|
||||
count int
|
||||
meta map[string]interface{}
|
||||
}
|
||||
)
|
||||
|
||||
func (r *osFileSystem) create(name string) (*os.File, error) {
|
||||
return os.Create(name)
|
||||
}
|
||||
|
||||
func (r *osFileSystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
func (r *webSequenceDiagramDSL) addRequestRow(source string, target string, description string) {
|
||||
r.addRow("->", source, target, description)
|
||||
}
|
||||
|
||||
func (r *webSequenceDiagramDSL) addResponseRow(source string, target string, description string) {
|
||||
r.addRow("->>", source, target, description)
|
||||
}
|
||||
|
||||
func (r *webSequenceDiagramDSL) addRow(operation, source string, target string, description string) {
|
||||
if name, ok := r.meta["consumerName"]; ok {
|
||||
if n, ok := name.(string); ok {
|
||||
source = strings.ReplaceAll(source, ConsumerDefaultName, n)
|
||||
target = strings.ReplaceAll(target, ConsumerDefaultName, n)
|
||||
}
|
||||
}
|
||||
if name, ok := r.meta["systemUnderTestName"]; ok {
|
||||
if n, ok := name.(string); ok {
|
||||
source = strings.ReplaceAll(source, SystemUnderTestDefaultName, n)
|
||||
target = strings.ReplaceAll(target, SystemUnderTestDefaultName, n)
|
||||
}
|
||||
}
|
||||
r.count++
|
||||
r.data.WriteString(fmt.Sprintf("%s%s%s: (%d) %s\n",
|
||||
quoted(source),
|
||||
operation,
|
||||
quoted(target),
|
||||
r.count,
|
||||
description),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *webSequenceDiagramDSL) toString() string {
|
||||
return r.data.String()
|
||||
}
|
||||
|
||||
// Format formats the events received by the recorder
|
||||
func (r *SequenceDiagramFormatter) Format(recorder *Recorder) {
|
||||
output, err := newHTMLTemplateModel(recorder)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmpl, err := htmlTemplate.New("sequenceDiagram").
|
||||
Funcs(*templateFunc).
|
||||
Parse(reportTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = tmpl.Execute(&out, output)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%s.html", recorder.Meta["hash"])
|
||||
err = r.fs.mkdirAll(r.storagePath, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
saveFilesTo := fmt.Sprintf("%s/%s", r.storagePath, fileName)
|
||||
|
||||
f, err := r.fs.create(saveFilesTo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s, _ := filepath.Abs(saveFilesTo)
|
||||
_, err = f.WriteString(out.String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Created sequence diagram (%s): %s\n", fileName, filepath.FromSlash(s))
|
||||
}
|
||||
|
||||
// SequenceDiagram produce a sequence diagram at the given path or .sequence by default
|
||||
func SequenceDiagram(path ...string) *SequenceDiagramFormatter {
|
||||
var storagePath string
|
||||
if len(path) == 0 {
|
||||
storagePath = ".sequence"
|
||||
} else {
|
||||
storagePath = path[0]
|
||||
}
|
||||
return &SequenceDiagramFormatter{storagePath: storagePath, fs: &osFileSystem{}}
|
||||
}
|
||||
|
||||
var templateFunc = &htmlTemplate.FuncMap{
|
||||
"inc": func(i int) int {
|
||||
return i + 1
|
||||
},
|
||||
}
|
||||
|
||||
func formatDiagramRequest(req *http.Request) string {
|
||||
out := req.URL.Path
|
||||
if req.URL.RawQuery != "" {
|
||||
out = fmt.Sprintf("%s %s?%s", req.Method, out, req.URL.RawQuery)
|
||||
}
|
||||
if len(out) > 65 {
|
||||
return fmt.Sprintf("%s...", out[:65])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func badgeCSSClass(status int) string {
|
||||
class := "badge badge-success"
|
||||
if status >= 400 && status < 500 {
|
||||
class = "badge badge-warning"
|
||||
} else if status >= 500 {
|
||||
class = "badge badge-danger"
|
||||
}
|
||||
return class
|
||||
}
|
||||
|
||||
func newHTMLTemplateModel(r *Recorder) (htmlTemplateModel, error) {
|
||||
if len(r.Events) == 0 {
|
||||
return htmlTemplateModel{}, errors.New("no events are defined")
|
||||
}
|
||||
var logs []logEntry
|
||||
webSequenceDiagram := &webSequenceDiagramDSL{meta: r.Meta}
|
||||
|
||||
for _, event := range r.Events {
|
||||
switch v := event.(type) {
|
||||
case HttpRequest:
|
||||
httpReq := v.Value
|
||||
webSequenceDiagram.addRequestRow(v.Source, v.Target, formatDiagramRequest(httpReq))
|
||||
entry, err := newHTTPRequestLogEntry(httpReq)
|
||||
if err != nil {
|
||||
return htmlTemplateModel{}, err
|
||||
}
|
||||
entry.Timestamp = v.Timestamp
|
||||
logs = append(logs, entry)
|
||||
case HttpResponse:
|
||||
webSequenceDiagram.addResponseRow(v.Source, v.Target, strconv.Itoa(v.Value.StatusCode))
|
||||
entry, err := newHTTPResponseLogEntry(v.Value)
|
||||
if err != nil {
|
||||
return htmlTemplateModel{}, err
|
||||
}
|
||||
entry.Timestamp = v.Timestamp
|
||||
logs = append(logs, entry)
|
||||
case MessageRequest:
|
||||
webSequenceDiagram.addRequestRow(v.Source, v.Target, v.Header)
|
||||
logs = append(logs, logEntry{Header: v.Header, Body: v.Body, Timestamp: v.Timestamp})
|
||||
case MessageResponse:
|
||||
webSequenceDiagram.addResponseRow(v.Source, v.Target, v.Header)
|
||||
logs = append(logs, logEntry{Header: v.Header, Body: v.Body, Timestamp: v.Timestamp})
|
||||
default:
|
||||
panic("received unknown event type")
|
||||
}
|
||||
}
|
||||
|
||||
status, err := r.ResponseStatus()
|
||||
if err != nil {
|
||||
return htmlTemplateModel{}, err
|
||||
}
|
||||
|
||||
jsonMeta, err := json.Marshal(r.Meta)
|
||||
if err != nil {
|
||||
return htmlTemplateModel{}, err
|
||||
}
|
||||
|
||||
return htmlTemplateModel{
|
||||
WebSequenceDSL: webSequenceDiagram.toString(),
|
||||
LogEntries: logs,
|
||||
Title: r.Title,
|
||||
SubTitle: r.SubTitle,
|
||||
StatusCode: status,
|
||||
BadgeClass: badgeCSSClass(status),
|
||||
MetaJSON: htmlTemplate.JS(jsonMeta),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHTTPRequestLogEntry(req *http.Request) (logEntry, error) {
|
||||
reqHeader, err := httputil.DumpRequest(req, false)
|
||||
if err != nil {
|
||||
return logEntry{}, err
|
||||
}
|
||||
body, err := formatBodyContent(req.Body, func(replacementBody io.ReadCloser) {
|
||||
req.Body = replacementBody
|
||||
})
|
||||
if err != nil {
|
||||
return logEntry{}, err
|
||||
}
|
||||
return logEntry{Header: string(reqHeader), Body: body}, err
|
||||
}
|
||||
|
||||
func newHTTPResponseLogEntry(res *http.Response) (logEntry, error) {
|
||||
resDump, err := httputil.DumpResponse(res, false)
|
||||
if err != nil {
|
||||
return logEntry{}, err
|
||||
}
|
||||
body, err := formatBodyContent(res.Body, func(replacementBody io.ReadCloser) {
|
||||
res.Body = replacementBody
|
||||
})
|
||||
if err != nil {
|
||||
return logEntry{}, err
|
||||
}
|
||||
return logEntry{Header: string(resDump), Body: body}, err
|
||||
}
|
||||
|
||||
func formatBodyContent(bodyReadCloser io.ReadCloser, replaceBody func(replacementBody io.ReadCloser)) (string, error) {
|
||||
if bodyReadCloser == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(bodyReadCloser)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
replaceBody(ioutil.NopCloser(bytes.NewReader(body)))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if json.Valid(body) {
|
||||
jsonEncodeErr := json.Indent(buf, body, "", " ")
|
||||
if jsonEncodeErr != nil {
|
||||
return "", jsonEncodeErr
|
||||
}
|
||||
s := buf.String()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
_, err = buf.Write(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func quoted(in string) string {
|
||||
return fmt.Sprintf("%q", in)
|
||||
}
|
||||
27
server/vendor/github.com/steinfletcher/apitest/difflib/LICENSE
generated
vendored
Normal file
27
server/vendor/github.com/steinfletcher/apitest/difflib/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2013, Patrick Mezard
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
The names of its contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
785
server/vendor/github.com/steinfletcher/apitest/difflib/difflib.go
generated
vendored
Normal file
785
server/vendor/github.com/steinfletcher/apitest/difflib/difflib.go
generated
vendored
Normal file
@@ -0,0 +1,785 @@
|
||||
// Package difflib is a partial port of Python difflib module.
|
||||
//
|
||||
// It provides tools to compare sequences of strings and generate textual diffs.
|
||||
//
|
||||
// The following class and functions have been ported:
|
||||
//
|
||||
// - SequenceMatcher
|
||||
//
|
||||
// - unified_diff
|
||||
//
|
||||
// - context_diff
|
||||
//
|
||||
// Getting unified diffs was the main goal of the port. Keep in mind this code
|
||||
// is mostly suitable to output text differences in a human friendly way, there
|
||||
// are no guarantees generated diffs are consumable by patch(1).
|
||||
package difflib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
red = "\033[31m"
|
||||
green = "\033[32m"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func calculateRatio(matches, length int) float64 {
|
||||
if length > 0 {
|
||||
return 2.0 * float64(matches) / float64(length)
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
A int
|
||||
B int
|
||||
Size int
|
||||
}
|
||||
|
||||
type OpCode struct {
|
||||
Tag byte
|
||||
I1 int
|
||||
I2 int
|
||||
J1 int
|
||||
J2 int
|
||||
}
|
||||
|
||||
// SequenceMatcher compares sequence of strings. The basic
|
||||
// algorithm predates, and is a little fancier than, an algorithm
|
||||
// published in the late 1980's by Ratcliff and Obershelp under the
|
||||
// hyperbolic name "gestalt pattern matching". The basic idea is to find
|
||||
// the longest contiguous matching subsequence that contains no "junk"
|
||||
// elements (R-O doesn't address junk). The same idea is then applied
|
||||
// recursively to the pieces of the sequences to the left and to the right
|
||||
// of the matching subsequence. This does not yield minimal edit
|
||||
// sequences, but does tend to yield matches that "look right" to people.
|
||||
//
|
||||
// SequenceMatcher tries to compute a "human-friendly diff" between two
|
||||
// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
|
||||
// longest *contiguous* & junk-free matching subsequence. That's what
|
||||
// catches peoples' eyes. The Windows(tm) windiff has another interesting
|
||||
// notion, pairing up elements that appear uniquely in each sequence.
|
||||
// That, and the method here, appear to yield more intuitive difference
|
||||
// reports than does diff. This method appears to be the least vulnerable
|
||||
// to synching up on blocks of "junk lines", though (like blank lines in
|
||||
// ordinary text files, or maybe "<P>" lines in HTML files). That may be
|
||||
// because this is the only method of the 3 that has a *concept* of
|
||||
// "junk" <wink>.
|
||||
//
|
||||
// Timing: Basic R-O is cubic time worst case and quadratic time expected
|
||||
// case. SequenceMatcher is quadratic time for the worst case and has
|
||||
// expected-case behavior dependent in a complicated way on how many
|
||||
// elements the sequences have in common; best case time is linear.
|
||||
type SequenceMatcher struct {
|
||||
a []string
|
||||
b []string
|
||||
b2j map[string][]int
|
||||
IsJunk func(string) bool
|
||||
autoJunk bool
|
||||
bJunk map[string]struct{}
|
||||
matchingBlocks []Match
|
||||
fullBCount map[string]int
|
||||
bPopular map[string]struct{}
|
||||
opCodes []OpCode
|
||||
}
|
||||
|
||||
func NewMatcher(a, b []string) *SequenceMatcher {
|
||||
m := SequenceMatcher{autoJunk: true}
|
||||
m.SetSeqs(a, b)
|
||||
return &m
|
||||
}
|
||||
|
||||
func NewMatcherWithJunk(a, b []string, autoJunk bool,
|
||||
isJunk func(string) bool) *SequenceMatcher {
|
||||
|
||||
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
|
||||
m.SetSeqs(a, b)
|
||||
return &m
|
||||
}
|
||||
|
||||
// Set two sequences to be compared.
|
||||
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
||||
m.SetSeq1(a)
|
||||
m.SetSeq2(b)
|
||||
}
|
||||
|
||||
// Set the first sequence to be compared. The second sequence to be compared is
|
||||
// not changed.
|
||||
//
|
||||
// SequenceMatcher computes and caches detailed information about the second
|
||||
// sequence, so if you want to compare one sequence S against many sequences,
|
||||
// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
|
||||
// sequences.
|
||||
//
|
||||
// See also SetSeqs() and SetSeq2().
|
||||
func (m *SequenceMatcher) SetSeq1(a []string) {
|
||||
if &a == &m.a {
|
||||
return
|
||||
}
|
||||
m.a = a
|
||||
m.matchingBlocks = nil
|
||||
m.opCodes = nil
|
||||
}
|
||||
|
||||
// Set the second sequence to be compared. The first sequence to be compared is
|
||||
// not changed.
|
||||
func (m *SequenceMatcher) SetSeq2(b []string) {
|
||||
if &b == &m.b {
|
||||
return
|
||||
}
|
||||
m.b = b
|
||||
m.matchingBlocks = nil
|
||||
m.opCodes = nil
|
||||
m.fullBCount = nil
|
||||
m.chainB()
|
||||
}
|
||||
|
||||
func (m *SequenceMatcher) chainB() {
|
||||
// Populate line -> index mapping
|
||||
b2j := map[string][]int{}
|
||||
for i, s := range m.b {
|
||||
indices := b2j[s]
|
||||
indices = append(indices, i)
|
||||
b2j[s] = indices
|
||||
}
|
||||
|
||||
// Purge junk elements
|
||||
m.bJunk = map[string]struct{}{}
|
||||
if m.IsJunk != nil {
|
||||
junk := m.bJunk
|
||||
for s, _ := range b2j {
|
||||
if m.IsJunk(s) {
|
||||
junk[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
for s, _ := range junk {
|
||||
delete(b2j, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Purge remaining popular elements
|
||||
popular := map[string]struct{}{}
|
||||
n := len(m.b)
|
||||
if m.autoJunk && n >= 200 {
|
||||
ntest := n/100 + 1
|
||||
for s, indices := range b2j {
|
||||
if len(indices) > ntest {
|
||||
popular[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
for s, _ := range popular {
|
||||
delete(b2j, s)
|
||||
}
|
||||
}
|
||||
m.bPopular = popular
|
||||
m.b2j = b2j
|
||||
}
|
||||
|
||||
func (m *SequenceMatcher) isBJunk(s string) bool {
|
||||
_, ok := m.bJunk[s]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Find longest matching block in a[alo:ahi] and b[blo:bhi].
|
||||
//
|
||||
// If IsJunk is not defined:
|
||||
//
|
||||
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
|
||||
// alo <= i <= i+k <= ahi
|
||||
// blo <= j <= j+k <= bhi
|
||||
// and for all (i',j',k') meeting those conditions,
|
||||
// k >= k'
|
||||
// i <= i'
|
||||
// and if i == i', j <= j'
|
||||
//
|
||||
// In other words, of all maximal matching blocks, return one that
|
||||
// starts earliest in a, and of all those maximal matching blocks that
|
||||
// start earliest in a, return the one that starts earliest in b.
|
||||
//
|
||||
// If IsJunk is defined, first the longest matching block is
|
||||
// determined as above, but with the additional restriction that no
|
||||
// junk element appears in the block. Then that block is extended as
|
||||
// far as possible by matching (only) junk elements on both sides. So
|
||||
// the resulting block never matches on junk except as identical junk
|
||||
// happens to be adjacent to an "interesting" match.
|
||||
//
|
||||
// If no blocks match, return (alo, blo, 0).
|
||||
func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
|
||||
// CAUTION: stripping common prefix or suffix would be incorrect.
|
||||
// E.g.,
|
||||
// ab
|
||||
// acab
|
||||
// Longest matching block is "ab", but if common prefix is
|
||||
// stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
|
||||
// strip, so ends up claiming that ab is changed to acab by
|
||||
// inserting "ca" in the middle. That's minimal but unintuitive:
|
||||
// "it's obvious" that someone inserted "ac" at the front.
|
||||
// Windiff ends up at the same place as diff, but by pairing up
|
||||
// the unique 'b's and then matching the first two 'a's.
|
||||
besti, bestj, bestsize := alo, blo, 0
|
||||
|
||||
// find longest junk-free match
|
||||
// during an iteration of the loop, j2len[j] = length of longest
|
||||
// junk-free match ending with a[i-1] and b[j]
|
||||
j2len := map[int]int{}
|
||||
for i := alo; i != ahi; i++ {
|
||||
// look at all instances of a[i] in b; note that because
|
||||
// b2j has no junk keys, the loop is skipped if a[i] is junk
|
||||
newj2len := map[int]int{}
|
||||
for _, j := range m.b2j[m.a[i]] {
|
||||
// a[i] matches b[j]
|
||||
if j < blo {
|
||||
continue
|
||||
}
|
||||
if j >= bhi {
|
||||
break
|
||||
}
|
||||
k := j2len[j-1] + 1
|
||||
newj2len[j] = k
|
||||
if k > bestsize {
|
||||
besti, bestj, bestsize = i-k+1, j-k+1, k
|
||||
}
|
||||
}
|
||||
j2len = newj2len
|
||||
}
|
||||
|
||||
// Extend the best by non-junk elements on each end. In particular,
|
||||
// "popular" non-junk elements aren't in b2j, which greatly speeds
|
||||
// the inner loop above, but also means "the best" match so far
|
||||
// doesn't contain any junk *or* popular non-junk elements.
|
||||
for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
|
||||
m.a[besti-1] == m.b[bestj-1] {
|
||||
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
|
||||
}
|
||||
for besti+bestsize < ahi && bestj+bestsize < bhi &&
|
||||
!m.isBJunk(m.b[bestj+bestsize]) &&
|
||||
m.a[besti+bestsize] == m.b[bestj+bestsize] {
|
||||
bestsize += 1
|
||||
}
|
||||
|
||||
// Now that we have a wholly interesting match (albeit possibly
|
||||
// empty!), we may as well suck up the matching junk on each
|
||||
// side of it too. Can't think of a good reason not to, and it
|
||||
// saves post-processing the (possibly considerable) expense of
|
||||
// figuring out what to do with it. In the case of an empty
|
||||
// interesting match, this is clearly the right thing to do,
|
||||
// because no other kind of match is possible in the regions.
|
||||
for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
|
||||
m.a[besti-1] == m.b[bestj-1] {
|
||||
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
|
||||
}
|
||||
for besti+bestsize < ahi && bestj+bestsize < bhi &&
|
||||
m.isBJunk(m.b[bestj+bestsize]) &&
|
||||
m.a[besti+bestsize] == m.b[bestj+bestsize] {
|
||||
bestsize += 1
|
||||
}
|
||||
|
||||
return Match{A: besti, B: bestj, Size: bestsize}
|
||||
}
|
||||
|
||||
// Return list of triples describing matching subsequences.
|
||||
//
|
||||
// Each triple is of the form (i, j, n), and means that
|
||||
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
|
||||
// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
|
||||
// adjacent triples in the list, and the second is not the last triple in the
|
||||
// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
|
||||
// adjacent equal blocks.
|
||||
//
|
||||
// The last triple is a dummy, (len(a), len(b), 0), and is the only
|
||||
// triple with n==0.
|
||||
func (m *SequenceMatcher) GetMatchingBlocks() []Match {
|
||||
if m.matchingBlocks != nil {
|
||||
return m.matchingBlocks
|
||||
}
|
||||
|
||||
var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
|
||||
matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
|
||||
match := m.findLongestMatch(alo, ahi, blo, bhi)
|
||||
i, j, k := match.A, match.B, match.Size
|
||||
if match.Size > 0 {
|
||||
if alo < i && blo < j {
|
||||
matched = matchBlocks(alo, i, blo, j, matched)
|
||||
}
|
||||
matched = append(matched, match)
|
||||
if i+k < ahi && j+k < bhi {
|
||||
matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
|
||||
|
||||
// It's possible that we have adjacent equal blocks in the
|
||||
// matching_blocks list now.
|
||||
nonAdjacent := []Match{}
|
||||
i1, j1, k1 := 0, 0, 0
|
||||
for _, b := range matched {
|
||||
// Is this block adjacent to i1, j1, k1?
|
||||
i2, j2, k2 := b.A, b.B, b.Size
|
||||
if i1+k1 == i2 && j1+k1 == j2 {
|
||||
// Yes, so collapse them -- this just increases the length of
|
||||
// the first block by the length of the second, and the first
|
||||
// block so lengthened remains the block to compare against.
|
||||
k1 += k2
|
||||
} else {
|
||||
// Not adjacent. Remember the first block (k1==0 means it's
|
||||
// the dummy we started with), and make the second block the
|
||||
// new block to compare against.
|
||||
if k1 > 0 {
|
||||
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
|
||||
}
|
||||
i1, j1, k1 = i2, j2, k2
|
||||
}
|
||||
}
|
||||
if k1 > 0 {
|
||||
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
|
||||
}
|
||||
|
||||
nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
|
||||
m.matchingBlocks = nonAdjacent
|
||||
return m.matchingBlocks
|
||||
}
|
||||
|
||||
// Return list of 5-tuples describing how to turn a into b.
|
||||
//
|
||||
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
|
||||
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
|
||||
// tuple preceding it, and likewise for j1 == the previous j2.
|
||||
//
|
||||
// The tags are characters, with these meanings:
|
||||
//
|
||||
// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
|
||||
//
|
||||
// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
|
||||
//
|
||||
// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
|
||||
//
|
||||
// 'e' (equal): a[i1:i2] == b[j1:j2]
|
||||
func (m *SequenceMatcher) GetOpCodes() []OpCode {
|
||||
if m.opCodes != nil {
|
||||
return m.opCodes
|
||||
}
|
||||
i, j := 0, 0
|
||||
matching := m.GetMatchingBlocks()
|
||||
opCodes := make([]OpCode, 0, len(matching))
|
||||
for _, m := range matching {
|
||||
// invariant: we've pumped out correct diffs to change
|
||||
// a[:i] into b[:j], and the next matching block is
|
||||
// a[ai:ai+size] == b[bj:bj+size]. So we need to pump
|
||||
// out a diff to change a[i:ai] into b[j:bj], pump out
|
||||
// the matching block, and move (i,j) beyond the match
|
||||
ai, bj, size := m.A, m.B, m.Size
|
||||
tag := byte(0)
|
||||
if i < ai && j < bj {
|
||||
tag = 'r'
|
||||
} else if i < ai {
|
||||
tag = 'd'
|
||||
} else if j < bj {
|
||||
tag = 'i'
|
||||
}
|
||||
if tag > 0 {
|
||||
opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
|
||||
}
|
||||
i, j = ai+size, bj+size
|
||||
// the list of matching blocks is terminated by a
|
||||
// sentinel with size 0
|
||||
if size > 0 {
|
||||
opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
|
||||
}
|
||||
}
|
||||
m.opCodes = opCodes
|
||||
return m.opCodes
|
||||
}
|
||||
|
||||
// Isolate change clusters by eliminating ranges with no changes.
|
||||
//
|
||||
// Return a generator of groups with up to n lines of context.
|
||||
// Each group is in the same format as returned by GetOpCodes().
|
||||
func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
|
||||
if n < 0 {
|
||||
n = 3
|
||||
}
|
||||
codes := m.GetOpCodes()
|
||||
if len(codes) == 0 {
|
||||
codes = []OpCode{OpCode{'e', 0, 1, 0, 1}}
|
||||
}
|
||||
// Fixup leading and trailing groups if they show no changes.
|
||||
if codes[0].Tag == 'e' {
|
||||
c := codes[0]
|
||||
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
|
||||
codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
|
||||
}
|
||||
if codes[len(codes)-1].Tag == 'e' {
|
||||
c := codes[len(codes)-1]
|
||||
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
|
||||
codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
|
||||
}
|
||||
nn := n + n
|
||||
groups := [][]OpCode{}
|
||||
group := []OpCode{}
|
||||
for _, c := range codes {
|
||||
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
|
||||
// End the current group and start a new one whenever
|
||||
// there is a large range with no changes.
|
||||
if c.Tag == 'e' && i2-i1 > nn {
|
||||
group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
|
||||
j1, min(j2, j1+n)})
|
||||
groups = append(groups, group)
|
||||
group = []OpCode{}
|
||||
i1, j1 = max(i1, i2-n), max(j1, j2-n)
|
||||
}
|
||||
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
|
||||
}
|
||||
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// Return a measure of the sequences' similarity (float in [0,1]).
|
||||
//
|
||||
// Where T is the total number of elements in both sequences, and
|
||||
// M is the number of matches, this is 2.0*M / T.
|
||||
// Note that this is 1 if the sequences are identical, and 0 if
|
||||
// they have nothing in common.
|
||||
//
|
||||
// .Ratio() is expensive to compute if you haven't already computed
|
||||
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
|
||||
// want to try .QuickRatio() or .RealQuickRation() first to get an
|
||||
// upper bound.
|
||||
func (m *SequenceMatcher) Ratio() float64 {
|
||||
matches := 0
|
||||
for _, m := range m.GetMatchingBlocks() {
|
||||
matches += m.Size
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() relatively quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute.
|
||||
func (m *SequenceMatcher) QuickRatio() float64 {
|
||||
// viewing a and b as multisets, set matches to the cardinality
|
||||
// of their intersection; this counts the number of matches
|
||||
// without regard to order, so is clearly an upper bound
|
||||
if m.fullBCount == nil {
|
||||
m.fullBCount = map[string]int{}
|
||||
for _, s := range m.b {
|
||||
m.fullBCount[s] = m.fullBCount[s] + 1
|
||||
}
|
||||
}
|
||||
|
||||
// avail[x] is the number of times x appears in 'b' less the
|
||||
// number of times we've seen it in 'a' so far ... kinda
|
||||
avail := map[string]int{}
|
||||
matches := 0
|
||||
for _, s := range m.a {
|
||||
n, ok := avail[s]
|
||||
if !ok {
|
||||
n = m.fullBCount[s]
|
||||
}
|
||||
avail[s] = n - 1
|
||||
if n > 0 {
|
||||
matches += 1
|
||||
}
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() very quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute than either .Ratio() or .QuickRatio().
|
||||
func (m *SequenceMatcher) RealQuickRatio() float64 {
|
||||
la, lb := len(m.a), len(m.b)
|
||||
return calculateRatio(min(la, lb), la+lb)
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format
|
||||
func formatRangeUnified(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
beginning := start + 1 // lines start numbering with one
|
||||
length := stop - start
|
||||
if length == 1 {
|
||||
return fmt.Sprintf("%d", beginning)
|
||||
}
|
||||
if length == 0 {
|
||||
beginning -= 1 // empty ranges begin at line just before the range
|
||||
}
|
||||
return fmt.Sprintf("%d,%d", beginning, length)
|
||||
}
|
||||
|
||||
// Unified diff parameters
|
||||
type UnifiedDiff struct {
|
||||
A []string // First sequence lines
|
||||
FromFile string // First file name
|
||||
FromDate string // First file time
|
||||
B []string // Second sequence lines
|
||||
ToFile string // Second file name
|
||||
ToDate string // Second file time
|
||||
Eol string // Headers end of line, defaults to LF
|
||||
Context int // Number of context lines
|
||||
}
|
||||
|
||||
// Compare two sequences of lines; generate the delta as a unified diff.
|
||||
//
|
||||
// Unified diffs are a compact way of showing line changes and a few
|
||||
// lines of context. The number of context lines is set by 'n' which
|
||||
// defaults to three.
|
||||
//
|
||||
// By default, the diff control lines (those with ---, +++, or @@) are
|
||||
// created with a trailing newline. This is helpful so that inputs
|
||||
// created from file.readlines() result in diffs that are suitable for
|
||||
// file.writelines() since both the inputs and outputs have trailing
|
||||
// newlines.
|
||||
//
|
||||
// For inputs that do not have trailing newlines, set the lineterm
|
||||
// argument to "" so that the output will be uniformly newline free.
|
||||
//
|
||||
// The unidiff format normally has a header for filenames and modification
|
||||
// times. Any or all of these may be specified using strings for
|
||||
// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
|
||||
// The modification times are normally expressed in the ISO 8601 format.
|
||||
func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
|
||||
buf := bufio.NewWriter(writer)
|
||||
defer buf.Flush()
|
||||
wf := func(format string, args ...interface{}) error {
|
||||
_, err := buf.WriteString(fmt.Sprintf(format, args...))
|
||||
return err
|
||||
}
|
||||
ws := func(s string) error {
|
||||
_, err := buf.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(diff.Eol) == 0 {
|
||||
diff.Eol = "\n"
|
||||
}
|
||||
|
||||
started := false
|
||||
m := NewMatcher(diff.A, diff.B)
|
||||
for _, g := range m.GetGroupedOpCodes(diff.Context) {
|
||||
if !started {
|
||||
started = true
|
||||
fromDate := ""
|
||||
if len(diff.FromDate) > 0 {
|
||||
fromDate = "\t" + diff.FromDate
|
||||
}
|
||||
toDate := ""
|
||||
if len(diff.ToDate) > 0 {
|
||||
toDate = "\t" + diff.ToDate
|
||||
}
|
||||
if diff.FromFile != "" || diff.ToFile != "" {
|
||||
err := wf("%s %s%s%s", colorize(red, "---"), diff.FromFile, fromDate, diff.Eol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wf("%s %s%s%s", colorize(green, "+++"), diff.ToFile, toDate, diff.Eol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
first, last := g[0], g[len(g)-1]
|
||||
range1 := formatRangeUnified(first.I1, last.I2)
|
||||
range2 := formatRangeUnified(first.J1, last.J2)
|
||||
if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range g {
|
||||
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
|
||||
if c.Tag == 'e' {
|
||||
for _, line := range diff.A[i1:i2] {
|
||||
if err := ws(" " + line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c.Tag == 'r' || c.Tag == 'd' {
|
||||
for _, line := range diff.A[i1:i2] {
|
||||
if err := ws(colorize(red, "-"+line)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Tag == 'r' || c.Tag == 'i' {
|
||||
for _, line := range diff.B[j1:j2] {
|
||||
if err := ws(colorize(green, "+"+line)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func colorize(color, message string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return message
|
||||
}
|
||||
return color + message + "\033[0m"
|
||||
}
|
||||
|
||||
// Like WriteUnifiedDiff but returns the diff a string.
|
||||
func GetUnifiedDiffString(diff UnifiedDiff) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
err := WriteUnifiedDiff(w, diff)
|
||||
return string(w.Bytes()), err
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format.
|
||||
func formatRangeContext(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
beginning := start + 1 // lines start numbering with one
|
||||
length := stop - start
|
||||
if length == 0 {
|
||||
beginning -= 1 // empty ranges begin at line just before the range
|
||||
}
|
||||
if length <= 1 {
|
||||
return fmt.Sprintf("%d", beginning)
|
||||
}
|
||||
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
|
||||
}
|
||||
|
||||
type ContextDiff UnifiedDiff
|
||||
|
||||
// Compare two sequences of lines; generate the delta as a context diff.
|
||||
//
|
||||
// Context diffs are a compact way of showing line changes and a few
|
||||
// lines of context. The number of context lines is set by diff.Context
|
||||
// which defaults to three.
|
||||
//
|
||||
// By default, the diff control lines (those with *** or ---) are
|
||||
// created with a trailing newline.
|
||||
//
|
||||
// For inputs that do not have trailing newlines, set the diff.Eol
|
||||
// argument to "" so that the output will be uniformly newline free.
|
||||
//
|
||||
// The context diff format normally has a header for filenames and
|
||||
// modification times. Any or all of these may be specified using
|
||||
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
|
||||
// The modification times are normally expressed in the ISO 8601 format.
|
||||
// If not specified, the strings default to blanks.
|
||||
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
|
||||
buf := bufio.NewWriter(writer)
|
||||
defer buf.Flush()
|
||||
var diffErr error
|
||||
wf := func(format string, args ...interface{}) {
|
||||
_, err := buf.WriteString(fmt.Sprintf(format, args...))
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
ws := func(s string) {
|
||||
_, err := buf.WriteString(s)
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if len(diff.Eol) == 0 {
|
||||
diff.Eol = "\n"
|
||||
}
|
||||
|
||||
prefix := map[byte]string{
|
||||
'i': "+ ",
|
||||
'd': "- ",
|
||||
'r': "! ",
|
||||
'e': " ",
|
||||
}
|
||||
|
||||
started := false
|
||||
m := NewMatcher(diff.A, diff.B)
|
||||
for _, g := range m.GetGroupedOpCodes(diff.Context) {
|
||||
if !started {
|
||||
started = true
|
||||
fromDate := ""
|
||||
if len(diff.FromDate) > 0 {
|
||||
fromDate = "\t" + diff.FromDate
|
||||
}
|
||||
toDate := ""
|
||||
if len(diff.ToDate) > 0 {
|
||||
toDate = "\t" + diff.ToDate
|
||||
}
|
||||
if diff.FromFile != "" || diff.ToFile != "" {
|
||||
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
|
||||
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
|
||||
}
|
||||
}
|
||||
|
||||
first, last := g[0], g[len(g)-1]
|
||||
ws("***************" + diff.Eol)
|
||||
|
||||
range1 := formatRangeContext(first.I1, last.I2)
|
||||
wf("*** %s ****%s", range1, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'd' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'i' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.A[cc.I1:cc.I2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
range2 := formatRangeContext(first.J1, last.J2)
|
||||
wf("--- %s ----%s", range2, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'i' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'd' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.B[cc.J1:cc.J2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffErr
|
||||
}
|
||||
|
||||
// Like WriteContextDiff but returns the diff a string.
|
||||
func GetContextDiffString(diff ContextDiff) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
err := WriteContextDiff(w, diff)
|
||||
return string(w.Bytes()), err
|
||||
}
|
||||
|
||||
// Split a string on "\n" while preserving them. The output can be used
|
||||
// as input for UnifiedDiff and ContextDiff structures.
|
||||
func SplitLines(s string) []string {
|
||||
lines := strings.SplitAfter(s, "\n")
|
||||
lines[len(lines)-1] += "\n"
|
||||
return lines
|
||||
}
|
||||
1190
server/vendor/github.com/steinfletcher/apitest/mocks.go
generated
vendored
Normal file
1190
server/vendor/github.com/steinfletcher/apitest/mocks.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
145
server/vendor/github.com/steinfletcher/apitest/report.go
generated
vendored
Normal file
145
server/vendor/github.com/steinfletcher/apitest/report.go
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// ReportFormatter represents the report formatter
|
||||
ReportFormatter interface {
|
||||
// Format formats the events received from the recorder
|
||||
Format(*Recorder)
|
||||
}
|
||||
|
||||
// Event represents a reporting event
|
||||
Event interface {
|
||||
GetTime() time.Time
|
||||
}
|
||||
|
||||
// Recorder represents all of the report data
|
||||
Recorder struct {
|
||||
Title string
|
||||
SubTitle string
|
||||
Meta map[string]interface{}
|
||||
Events []Event
|
||||
}
|
||||
|
||||
// MessageRequest represents a request interaction
|
||||
MessageRequest struct {
|
||||
Source string
|
||||
Target string
|
||||
Header string
|
||||
Body string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// MessageResponse represents a response interaction
|
||||
MessageResponse struct {
|
||||
Source string
|
||||
Target string
|
||||
Header string
|
||||
Body string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// HttpRequest represents an http request
|
||||
HttpRequest struct {
|
||||
Source string
|
||||
Target string
|
||||
Value *http.Request
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// HttpResponse represents an http response
|
||||
HttpResponse struct {
|
||||
Source string
|
||||
Target string
|
||||
Value *http.Response
|
||||
Timestamp time.Time
|
||||
}
|
||||
)
|
||||
|
||||
// GetTime gets the time of the HttpRequest interaction
|
||||
func (r HttpRequest) GetTime() time.Time { return r.Timestamp }
|
||||
|
||||
// GetTime gets the time of the HttpResponse interaction
|
||||
func (r HttpResponse) GetTime() time.Time { return r.Timestamp }
|
||||
|
||||
// GetTime gets the time of the MessageRequest interaction
|
||||
func (r MessageRequest) GetTime() time.Time { return r.Timestamp }
|
||||
|
||||
// GetTime gets the time of the MessageResponse interaction
|
||||
func (r MessageResponse) GetTime() time.Time { return r.Timestamp }
|
||||
|
||||
// NewTestRecorder creates a new TestRecorder
|
||||
func NewTestRecorder() *Recorder {
|
||||
return &Recorder{}
|
||||
}
|
||||
|
||||
// AddHttpRequest add an http request to recorder
|
||||
func (r *Recorder) AddHttpRequest(req HttpRequest) *Recorder {
|
||||
r.Events = append(r.Events, req)
|
||||
return r
|
||||
}
|
||||
|
||||
// AddHttpResponse add an HttpResponse to the recorder
|
||||
func (r *Recorder) AddHttpResponse(req HttpResponse) *Recorder {
|
||||
r.Events = append(r.Events, req)
|
||||
return r
|
||||
}
|
||||
|
||||
// AddMessageRequest add a MessageRequest to the recorder
|
||||
func (r *Recorder) AddMessageRequest(m MessageRequest) *Recorder {
|
||||
r.Events = append(r.Events, m)
|
||||
return r
|
||||
}
|
||||
|
||||
// AddMessageResponse add a MessageResponse to the recorder
|
||||
func (r *Recorder) AddMessageResponse(m MessageResponse) *Recorder {
|
||||
r.Events = append(r.Events, m)
|
||||
return r
|
||||
}
|
||||
|
||||
// AddTitle add a Title to the recorder
|
||||
func (r *Recorder) AddTitle(title string) *Recorder {
|
||||
r.Title = title
|
||||
return r
|
||||
}
|
||||
|
||||
// AddSubTitle add a SubTitle to the recorder
|
||||
func (r *Recorder) AddSubTitle(subTitle string) *Recorder {
|
||||
r.SubTitle = subTitle
|
||||
return r
|
||||
}
|
||||
|
||||
// AddMeta add Meta to the recorder
|
||||
func (r *Recorder) AddMeta(meta map[string]interface{}) *Recorder {
|
||||
r.Meta = meta
|
||||
return r
|
||||
}
|
||||
|
||||
// ResponseStatus get response status of the recorder, returning an error when this wasn't possible
|
||||
func (r *Recorder) ResponseStatus() (int, error) {
|
||||
if len(r.Events) == 0 {
|
||||
return -1, errors.New("no events are defined")
|
||||
}
|
||||
|
||||
switch v := r.Events[len(r.Events)-1].(type) {
|
||||
case HttpResponse:
|
||||
return v.Value.StatusCode, nil
|
||||
case MessageResponse:
|
||||
return -1, nil
|
||||
default:
|
||||
return -1, errors.New("final event should be a response type")
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the recorder to default starting state
|
||||
func (r *Recorder) Reset() {
|
||||
r.Title = ""
|
||||
r.SubTitle = ""
|
||||
r.Events = nil
|
||||
r.Meta = nil
|
||||
}
|
||||
129
server/vendor/github.com/steinfletcher/apitest/template.go
generated
vendored
Normal file
129
server/vendor/github.com/steinfletcher/apitest/template.go
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
package apitest
|
||||
|
||||
const reportTemplate = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.7/raphael.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
|
||||
<script src="https://bramp.github.io/js-sequence-diagrams/js/sequence-diagram-min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
#scroll-to-top-button {
|
||||
background-color: #555;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
bottom: 20px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.copy-to-clipboard-button {
|
||||
background-color: #fff;
|
||||
border: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- THIS CODE IS AUTOGENERATED. DO NOT EDIT -->
|
||||
<div class="container-fluid">
|
||||
<h2>{{printf "%.100s" .Title }}</h2>
|
||||
<span class="{{ .BadgeClass }}">{{ .StatusCode }}</span>
|
||||
<p class="lead">{{ .SubTitle }}</p>
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<div id="d" class="justify-content-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
<p class="lead">Event Log</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $i, $e := .LogEntries }}
|
||||
<tr id="log-{{$i}}">
|
||||
<th scope="row">{{ inc $i }}</th>
|
||||
<td>
|
||||
<pre>{{ $e.Header }}</pre>
|
||||
{{if $e.Body }}
|
||||
<pre style="max-height: 1000px; margin-bottom: 0; border: 1px solid #eee;"><code id="event-message-{{$i}}">{{ $e.Body }}</code></pre>
|
||||
<button class="copy-to-clipboard-button" data-clipboard-target="#event-message-{{$i}}">copy to clipboard</button>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button onclick="topFunction()" id="scroll-to-top-button" title="Go to top">Back to top</button>
|
||||
<script>
|
||||
Diagram.parse("{{ .WebSequenceDSL }}").drawSVG("d", {theme: 'simple', 'font-size': 14});
|
||||
</script>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
{{if $.MetaJSON }}<script type="application/json" id="metaJson">{{$.MetaJSON}}</script>{{end}}
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.1/build/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
<script>new ClipboardJS('.copy-to-clipboard-button');</script>
|
||||
<script>
|
||||
var elements = document.getElementsByTagName('text')
|
||||
var regex = /\((\d{1,3})\)/ // match elements containing (0), (1), etc.
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
if (elements[i].innerHTML) {
|
||||
var found = elements[i].innerHTML.match(regex);
|
||||
if (found && found.length > 0) {
|
||||
const logIndex = parseInt(found[1], 10) - 1;
|
||||
elements[i].style.cursor = 'pointer';
|
||||
elements[i].addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
document.getElementById("log-" + logIndex).scrollIntoView();
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scrollToTopBtn = document.getElementById("scroll-to-top-button");
|
||||
window.onscroll = function () {
|
||||
scrollFunction()
|
||||
};
|
||||
function scrollFunction() {
|
||||
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
|
||||
scrollToTopBtn.style.display = "block";
|
||||
} else {
|
||||
scrollToTopBtn.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function topFunction() {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
Reference in New Issue
Block a user