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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1 @@
.idea/

View File

@@ -0,0 +1,2 @@
test:
go test -v .

View File

@@ -0,0 +1,196 @@
![Test](https://github.com/steinfletcher/apitest-jsonpath/workflows/Test/badge.svg)
# 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(),
).
```

View 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
}

View 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
}
}

View 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())
}
}

View 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
}

View File

@@ -0,0 +1,2 @@
exclude_paths:
- 'examples/**/*'

View File

@@ -0,0 +1,6 @@
/bin/
.idea/
coverage.out
resources/_gen/images/
.DS_Store
.sequence

View 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
View 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.

View 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 -

View 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

File diff suppressed because it is too large Load Diff

View 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)
}

View 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)
}

View 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)
}

View 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.

View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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>`