Skip to content

Commit

Permalink
hook to supporting adding arbitrary events to diagrams, e.g. database
Browse files Browse the repository at this point in the history
and queue data
  • Loading branch information
Stein Fletcher committed Feb 10, 2019
1 parent 21a4c0a commit ccc0dbd
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 51 deletions.
94 changes: 70 additions & 24 deletions apitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import (
"net/textproto"
"net/url"
"runtime/debug"
"sort"
"strings"
"testing"
"time"
)

const systemUnderTestDefaultName = "sut"
Expand All @@ -31,15 +33,28 @@ type APITest struct {
response *Response
observers []Observe
mocksObserver Observe
recorderHook RecorderHook
mocks []*Mock
t *testing.T
httpClient *http.Client
transport *Transport
}

type InboundRequest struct {
request *http.Request
timestamp time.Time
}

type FinalResponse struct {
response *http.Response
timestamp time.Time
}

// Observe will be called by with the request and response on completion
type Observe func(*http.Response, *http.Request, *APITest)

type RecorderHook func(*Recorder, string)

// New creates a new api test. The name is optional and will appear in test reports
func New(name ...string) *APITest {
apiTest := &APITest{}
Expand Down Expand Up @@ -101,6 +116,13 @@ func (a *APITest) ObserveMocks(observer Observe) *APITest {
return a
}

// RecorderHook allows the consumer to provider a function that will receive the recorder instance before the
// test runs. This can be used to inject custom events which can then be rendered in diagrams
func (a *APITest) RecorderHook(hook RecorderHook) *APITest {
a.recorderHook = hook
return a
}

// Request returns the request spec
func (a *APITest) Request() *Request {
return a.request
Expand Down Expand Up @@ -357,12 +379,13 @@ func (r *Response) Assert(fn func(*http.Response, *http.Request) error) *Respons

// End runs the test and all defined assertions
func (r *Response) End() {
r.finalize()
r.execute()
}

type mockInteraction struct {
request *http.Request
response *http.Response
request *http.Request
response *http.Response
timestamp time.Time
}

func (r *Response) Report(formatter ...ReportFormatter) {
Expand All @@ -380,41 +403,57 @@ func (r *Response) Report(formatter ...ReportFormatter) {
}
apiTest.mocksObserver = func(mockRes *http.Response, mockReq *http.Request, a *APITest) {
capturedMockInteractions = append(capturedMockInteractions, &mockInteraction{
request: copyHttpRequest(mockReq),
response: copyHttpResponse(mockRes),
request: copyHttpRequest(mockReq),
response: copyHttpResponse(mockRes),
timestamp: time.Now().UTC(),
})
}

r.finalize()
recorder := NewTestRecorder()
if apiTest.recorderHook != nil {
apiTest.recorderHook(recorder, apiTest.request.GetHost())
}

execTime := time.Now().UTC()
r.execute()
finishTime := time.Now().UTC()

recorder := NewTestRecorder().
recorder.
AddTitle(fmt.Sprintf("%s %s", capturedInboundReq.Method, capturedInboundReq.URL.String())).
AddSubTitle(apiTest.name).
AddHttpRequest(HttpRequest{
Source: quoted(consumerName),
Target: quoted(apiTest.request.GetHost()),
Value: capturedInboundReq,
Source: quoted(consumerName),
Target: quoted(apiTest.request.GetHost()),
Value: capturedInboundReq,
Timestamp: execTime,
})

for _, interaction := range capturedMockInteractions {
recorder.AddHttpRequest(HttpRequest{
Source: quoted(apiTest.request.GetHost()),
Target: quoted(interaction.request.Host),
Value: interaction.request,
Source: quoted(apiTest.request.GetHost()),
Target: quoted(interaction.request.Host),
Value: interaction.request,
Timestamp: interaction.timestamp,
})
if interaction.response != nil {
recorder.AddHttpResponse(HttpResponse{
Source: quoted(interaction.request.Host),
Target: quoted(apiTest.request.GetHost()),
Value: interaction.response,
Source: quoted(interaction.request.Host),
Target: quoted(apiTest.request.GetHost()),
Value: interaction.response,
Timestamp: interaction.timestamp,
})
}
}

recorder.AddHttpResponse(HttpResponse{
Source: quoted(apiTest.request.GetHost()),
Target: quoted(consumerName),
Value: capturedFinalRes,
Source: quoted(apiTest.request.GetHost()),
Target: quoted(consumerName),
Value: capturedFinalRes,
Timestamp: finishTime,
})

sort.Slice(recorder.Events, func(i, j int) bool {
return recorder.Events[i].GetTime().Before(recorder.Events[j].GetTime())
})

meta := map[string]interface{}{}
Expand All @@ -433,7 +472,7 @@ func (r *Response) Report(formatter ...ReportFormatter) {
}
}

func (r *Response) finalize() {
func (r *Response) execute() {
apiTest := r.apiTest
if len(apiTest.mocks) > 0 {
apiTest.transport = newTransport(
Expand Down Expand Up @@ -657,14 +696,21 @@ func debugLog(prefix, header, msg string) {
}

func copyHttpResponse(response *http.Response) *http.Response {
all, _ := ioutil.ReadAll(response.Body)
response.Body = ioutil.NopCloser(bytes.NewBuffer(all))
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(all)),
Body: ioutil.NopCloser(bytes.NewBuffer(resBodyBytes)),
Proto: response.Proto,
ProtoMinor: response.ProtoMinor,
ProtoMajor: response.ProtoMajor,
Expand Down Expand Up @@ -711,5 +757,5 @@ func copyHttpRequest(request *http.Request) *http.Request {
}

func quoted(in string) string {
return fmt.Sprintf("%q", in)
return string(fmt.Sprintf("%q", in))
}
18 changes: 11 additions & 7 deletions diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"strconv"
"time"
)

type (
Expand All @@ -27,8 +28,9 @@ type (
}

LogEntry struct {
Header string
Body string
Header string
Body string
Timestamp time.Time
}

SequenceDiagramFormatter struct {
Expand Down Expand Up @@ -57,15 +59,15 @@ func (r *osFileSystem) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}

func (r *WebSequenceDiagramDSL) AddRequestRow(source, target, description string) {
func (r *WebSequenceDiagramDSL) AddRequestRow(source string, target string, description string) {
r.addRow("->", source, target, description)
}

func (r *WebSequenceDiagramDSL) AddResponseRow(source, target, description string) {
func (r *WebSequenceDiagramDSL) AddResponseRow(source string, target string, description string) {
r.addRow("->>", source, target, description)
}

func (r *WebSequenceDiagramDSL) addRow(operation, source, target, description string) {
func (r *WebSequenceDiagramDSL) addRow(operation, source string, target string, description string) {
r.count += 1
r.data.WriteString(fmt.Sprintf("%s%s%s: (%d) %s\n",
source,
Expand Down Expand Up @@ -160,20 +162,22 @@ func NewHTMLTemplateModel(r *Recorder) (HTMLTemplateModel, error) {
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})
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})
logs = append(logs, LogEntry{Header: v.Header, Body: v.Body, Timestamp: v.Timestamp})
default:
panic("received unknown event type")
}
Expand Down
31 changes: 30 additions & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,66 @@ require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Joker/jade v1.0.0 // indirect
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 // indirect
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect
github.com/aymerick/raymond v2.0.2+incompatible // indirect
github.com/davecgh/go-spew v1.1.1
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2 v0.0.0-20181225140029-79872a7b2769 // indirect
github.com/gavv/monotime v0.0.0-20171021193802-6f8212e8d10d // indirect
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/golang/protobuf v1.2.0 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2
github.com/imkira/go-interpol v1.1.0 // indirect
github.com/iris-contrib/blackfriday v2.0.0+incompatible // indirect
github.com/iris-contrib/formBinder v0.0.0-20190104093907-fbd5963f41e1 // indirect
github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
github.com/iris-contrib/httpexpect v0.0.0-20180314041918-ebe99fcebbce // indirect
github.com/jmoiron/sqlx v1.2.0
github.com/json-iterator/go v1.1.5 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
github.com/kataras/golog v0.0.0-20180321173939-03be10146386 // indirect
github.com/kataras/iris v11.1.0+incompatible
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect
github.com/klauspost/compress v1.4.1 // indirect
github.com/klauspost/cpuid v1.2.0 // indirect
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.2.8 // indirect
github.com/lib/pq v1.0.0
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/moul/http2curl v1.0.0 // indirect
github.com/onsi/ginkgo v1.7.0 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/ryanuber/columnize v2.1.0+incompatible // indirect
github.com/satori/go.uuid v1.2.0
github.com/sergi/go-diff v1.0.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/steinfletcher/api-test-jsonpath v0.0.1
github.com/steinfletcher/apitest v0.0.12
github.com/ugorji/go/codec v0.0.0-20190126102652-8fd0f8d918c8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.1.0 // indirect
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
github.com/yudai/gojsondiff v1.0.0 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
github.com/yudai/pp v2.0.1+incompatible // indirect
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b // indirect
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 // indirect
google.golang.org/appengine v1.4.0 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
)
Loading

0 comments on commit ccc0dbd

Please sign in to comment.