-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
289 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
# Changelog | ||
|
||
## v0.6.0 | ||
|
||
* (A) New web package for handler tests | ||
|
||
## v0.5.2 | ||
|
||
* (C) Optimize output of last change | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Tideland Go Audit - Web | ||
// | ||
// Copyright (C) 2012-2021 Frank Mueller / Tideland / Oldenburg / Germany | ||
// | ||
// All rights reserved. Use of this source code is governed | ||
// by the new BSD license. | ||
|
||
// Package web helps testing web handlers. Those can be registered, | ||
// standard web requests can be sent for execution and a response collects | ||
// the response for analysis. Automation helps to execute steps upfront | ||
// passing the request to the handler. | ||
package web // import "tideland.dev/go/audit/web" | ||
|
||
// EOF |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// Tideland Go Audit - Web | ||
// | ||
// Copyright (C) 2012-2021 Frank Mueller / Tideland / Oldenburg / Germany | ||
// | ||
// All rights reserved. Use of this source code is governed | ||
// by the new BSD license. | ||
|
||
package web // import "tideland.dev/go/audit/web" | ||
|
||
//-------------------- | ||
// IMPORTS | ||
//-------------------- | ||
|
||
import ( | ||
"net/http" | ||
) | ||
|
||
//-------------------- | ||
// RESPONSE | ||
//-------------------- | ||
|
||
// Response contains the response of the simulated HTTP request. | ||
type Response struct { | ||
header http.Header | ||
statusCode int | ||
body []byte | ||
} | ||
|
||
// newResponse creates a new initialized response. | ||
func newResponse() *Response { | ||
return &Response{ | ||
header: make(http.Header), | ||
statusCode: http.StatusOK, | ||
} | ||
} | ||
|
||
// Header returns the header values of the response. | ||
func (r *Response) Header() http.Header { | ||
return r.header | ||
} | ||
|
||
// WriteHeader writes the status code of the response. | ||
func (r *Response) WriteHeader(statusCode int) { | ||
if len(r.body) == 0 { | ||
r.statusCode = statusCode | ||
} | ||
} | ||
|
||
// StatusCode returns the status code of the response. | ||
func (r *Response) StatusCode() int { | ||
return r.statusCode | ||
} | ||
|
||
// Write implements the io.Writer interface. | ||
func (r *Response) Write(bs []byte) (int, error) { | ||
r.body = append(r.body, bs...) | ||
return len(r.body), nil | ||
} | ||
|
||
// Body returns a copy of the body of the response. | ||
func (r *Response) Body() []byte { | ||
bs := make([]byte, len(r.body)) | ||
copy(bs, r.body) | ||
return bs | ||
} | ||
|
||
//-------------------- | ||
// SIMULATOR | ||
//-------------------- | ||
|
||
// Preprocessor will be executed before a request is passed to the | ||
// handler. | ||
type Preprocessor func(r *http.Request) error | ||
|
||
// Simulator locally simulates HTTP requests to handler. | ||
type Simulator struct { | ||
h http.Handler | ||
pps []Preprocessor | ||
} | ||
|
||
// NewSimulator creates a new local HTTP request simulator. | ||
func NewSimulator(h http.Handler, pps ...Preprocessor) *Simulator { | ||
return &Simulator{ | ||
h: h, | ||
pps: pps, | ||
} | ||
} | ||
|
||
// NewFuncSimulator is a convenient variant of NewSimulator just for | ||
// a http.HandlerFunc. | ||
func NewFuncSimulator(f http.HandlerFunc, pps ...Preprocessor) *Simulator { | ||
return NewSimulator(f, pps...) | ||
} | ||
|
||
// Do executes first all registered preprocessors and then lets | ||
// the handler executes it. The build response is returned. | ||
func (s *Simulator) Do(r *http.Request) (*Response, error) { | ||
for _, pp := range s.pps { | ||
if err := pp(r); err != nil { | ||
return nil, err | ||
} | ||
} | ||
rw := newResponse() | ||
s.h.ServeHTTP(rw, r) | ||
return rw, nil | ||
} | ||
|
||
// EOF |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
// Tideland Go Audit - Web - Unit Tests | ||
// | ||
// Copyright (C) 2012-2021 Frank Mueller / Tideland / Oldenburg / Germany | ||
// | ||
// All rights reserved. Use of this source code is governed | ||
// by the new BSD license. | ||
|
||
package web_test | ||
|
||
//-------------------- | ||
// IMPORTS | ||
//-------------------- | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
"tideland.dev/go/audit/asserts" | ||
"tideland.dev/go/audit/web" | ||
) | ||
|
||
//-------------------- | ||
// TESTS | ||
//-------------------- | ||
|
||
// TestSimpleRequests tests handling of requests without preprocessors. | ||
func TestSimpleRequests(t *testing.T) { | ||
assert := asserts.NewTesting(t, asserts.FailStop) | ||
h := &echoHandler{assert} | ||
s := web.NewSimulator(h) | ||
|
||
tests := []struct { | ||
method string | ||
body io.Reader | ||
expected string | ||
}{ | ||
{http.MethodGet, nil, "m(GET) p(/test/) ct() a() b()"}, | ||
{http.MethodPost, strings.NewReader("posting data"), "m(POST) p(/test/) ct() a() b(posting data)"}, | ||
{http.MethodPut, strings.NewReader("posting data"), "m(PUT) p(/test/) ct() a() b(posting data)"}, | ||
{http.MethodDelete, nil, "m(DELETE) p(/test/) ct() a() b()"}, | ||
} | ||
for i, test := range tests { | ||
assert.Logf("no %d: method %q", i, test.method) | ||
req, err := http.NewRequest(test.method, "http://localhost:8080/test/", test.body) | ||
assert.NoError(err) | ||
|
||
resp, err := s.Do(req) | ||
assert.NoError(err) | ||
assert.Equal(resp.StatusCode(), http.StatusOK) | ||
|
||
body := resp.Body() | ||
|
||
assert.Equal(string(body), test.expected) | ||
} | ||
} | ||
|
||
// TestResponseCode verifies that the status code cannot be changed after | ||
// writing to the response body. | ||
func TestResponseCode(t *testing.T) { | ||
assert := asserts.NewTesting(t, asserts.FailStop) | ||
|
||
// Correctly set status before body. | ||
s := web.NewFuncSimulator(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusPartialContent) | ||
fmt.Fprint(w, "body") | ||
}) | ||
req, err := http.NewRequest(http.MethodGet, "https://localhost:8080/", nil) | ||
assert.NoError(err) | ||
resp, err := s.Do(req) | ||
assert.NoError(err) | ||
assert.Equal(resp.StatusCode(), http.StatusPartialContent) | ||
|
||
// Illegally set status after body. | ||
s = web.NewFuncSimulator(func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprint(w, "body") | ||
w.WriteHeader(http.StatusPartialContent) | ||
}) | ||
req, err = http.NewRequest(http.MethodGet, "https://localhost:8080/", nil) | ||
assert.NoError(err) | ||
resp, err = s.Do(req) | ||
assert.NoError(err) | ||
assert.Equal(resp.StatusCode(), http.StatusOK) | ||
} | ||
|
||
// TestPreprocessors verifies that preprocessors are correctly modifying | ||
// a request as wanted. | ||
func TestPreprocessors(t *testing.T) { | ||
assert := asserts.NewTesting(t, asserts.FailStop) | ||
ppContentType := func(r *http.Request) error { | ||
if r.Body != nil { | ||
r.Header.Add("Content-Type", "text/plain") | ||
} | ||
return nil | ||
} | ||
ppAccept := func(r *http.Request) error { | ||
r.Header.Add("Accept", "text/plain") | ||
return nil | ||
} | ||
h := &echoHandler{assert} | ||
s := web.NewSimulator(h, ppContentType, ppAccept) | ||
|
||
tests := []struct { | ||
method string | ||
body io.Reader | ||
expected string | ||
}{ | ||
{http.MethodGet, nil, "m(GET) p(/test/) ct() a(text/plain) b()"}, | ||
{http.MethodPost, strings.NewReader("posting data"), "m(POST) p(/test/) ct(text/plain) a(text/plain) b(posting data)"}, | ||
{http.MethodPut, strings.NewReader("posting data"), "m(PUT) p(/test/) ct(text/plain) a(text/plain) b(posting data)"}, | ||
{http.MethodDelete, nil, "m(DELETE) p(/test/) ct() a(text/plain) b()"}, | ||
} | ||
for i, test := range tests { | ||
assert.Logf("no %d: method %q", i, test.method) | ||
req, err := http.NewRequest(test.method, "http://localhost:8080/test/", test.body) | ||
assert.NoError(err) | ||
|
||
resp, err := s.Do(req) | ||
assert.NoError(err) | ||
assert.Equal(resp.StatusCode(), http.StatusOK) | ||
|
||
body := resp.Body() | ||
|
||
assert.Equal(string(body), test.expected) | ||
} | ||
} | ||
|
||
//-------------------- | ||
// HELPER | ||
//-------------------- | ||
|
||
// echoHandler simply echos some data of the request into the response for testing. | ||
type echoHandler struct { | ||
assert *asserts.Asserts | ||
} | ||
|
||
func (h *echoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
// Read the interesting request parts. | ||
m := r.Method | ||
p := r.URL.Path | ||
ct := r.Header.Get("Content-Type") | ||
a := r.Header.Get("Accept") | ||
|
||
var bs []byte | ||
var err error | ||
|
||
if r.Body != nil { | ||
bs, err = ioutil.ReadAll(r.Body) | ||
h.assert.NoError(err) | ||
} | ||
|
||
// Echo them. | ||
w.Header().Add("Content-Type", "text/plain") | ||
w.WriteHeader(http.StatusOK) | ||
fmt.Fprintf(w, "m(%s) p(%s) ct(%s) a(%s) b(%s)", m, p, ct, a, string(bs)) | ||
} | ||
|
||
// EOF |