Skip to content

Commit

Permalink
Add web package
Browse files Browse the repository at this point in the history
  • Loading branch information
themue committed Dec 10, 2021
1 parent 15113ed commit f392df0
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
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
Expand Down
4 changes: 2 additions & 2 deletions environments/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ func MakeHelloWorldHandler(assert *asserts.Asserts, who string) http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
reply := "Hello, " + who + "!"
w.Header().Add(environments.HeaderContentType, environments.ContentTypePlain)
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(reply)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
}

Expand All @@ -146,12 +146,12 @@ func MakeHeaderCookiesHandler(assert *asserts.Asserts) http.HandlerFunc {
Name: "Cookie-Out",
Value: cookieOut,
})
w.WriteHeader(http.StatusOK)
w.Header().Set(environments.HeaderContentType, environments.ContentTypePlain)
w.Header().Set("Header-Out", headerOut)
if _, err := w.Write([]byte("Done!")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
}

Expand Down
14 changes: 14 additions & 0 deletions web/doc.go
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
108 changes: 108 additions & 0 deletions web/web.go
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
161 changes: 161 additions & 0 deletions web/web_test.go
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

0 comments on commit f392df0

Please sign in to comment.