Some HTTP middleware tests
The Go stdlib httptest
package already has some good examples of how to build tests for HTTP handlers. I had fun writing a few pretty trivial ones for my own future reference, specifically for testing middleware.
Some thing was logged
func TestLoggingMiddleware(t *testing.T) {
var buf bytes.Buffer
log.SetOutput(&buf)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/some-route", nil)
handlerfn := loggingMiddleware(noopHandler)
handlerfn(w, r)
if buf.String() == "" {
t.Fatal("expected a message, received none")
}
}
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("passed through logging middleware at %v...", time.Now())
next(w, r)
})
}
func noopHandler(w http.ResponseWriter, r *http.Request) {}
This method uses the stdlib logger and a call to SetOutput
to redirect log output to a buffer where it can be captured and analyzed in test assertions. Another way would be to use an interface for the logger so implementations could be switched out for a mock that captures the logs into a buffer instead of writing them to stdout. There’s also a pretty interesting StackOverflow thread and thread comments with discussion on using os.Pipe
for this.
Some cookie was added to the response
func TestAuthMiddleware(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/some-route", nil)
handlerfn := authMiddleware(noopHandler)
handlerfn(w, r)
if nil == w {
t.Fatal("no response received")
}
cookies := w.Result().Cookies()
if len(cookies) == 0 {
t.Fatal("expected cookie, but none were set")
}
expectedCookieName := "some-cookie-name"
var found bool
for _, c := range cookies {
if c.Name == expectedCookieName {
found = true
break
}
}
if !found {
t.Fatalf("expected cookie '%s' to be set, but was not set", expectedCookieName)
}
}
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "some-cookie-name",
Value: "some-cookie-value",
})
next(w, r)
})
}
func noopHandler(w http.ResponseWriter, r *http.Request) {}
Here a new recorder is created using the httptest
package so that once the request has been sent through the call to handlerfn(w, r)
which includes our middleware, we can check to see if the cookie we’re looking for has been set. A good next step would be to post sets of credentials and check the cookie is only set for users that exist.
Some request was blocked or allowed through to an inner handler
type key int
const infoKey key = iota
const cookieName = "some-auth-cookie"
func TestGatekeeperMiddleware(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/some-protected-route", nil)
var gotIn bool
handlerfn := gatekeeperMiddleware(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotIn = true
}),
)
handlerfn(w, r)
if gotIn {
t.Fatal("expected to be blocked without cookie")
}
r.AddCookie(&http.Cookie{
Name: cookieName,
})
handlerfn(w, r)
if !gotIn {
t.Fatal("expected to be allowed through with cookie")
}
}
func gatekeeperMiddleware(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var found bool
for _, c := range r.Cookies() {
if c.Name == cookieName {
found = true
break
}
}
if !found {
http.Error(w, "not authorized", 401)
return
}
next(w, r)
})
}
A bool var declared at the beginning of the test is set to true
if the request makes it all the way to the inner handler. We can test that the middleware acting as gatekeeper behaves correctly with and without the cookie.
Some value was added to the context
type key int
const infoKey key = iota
func TestAddInfoMiddleware(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/some-route", nil)
var rdata interface{}
handlerfn := addInfoMiddleware(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rdata = r.Context().Value(infoKey)
}),
)
handlerfn(w, r)
var rdataInfo string
var ok bool
if rdataInfo, ok = rdata.(string); !ok {
t.Fatal("expected to be able to cast context data into string")
}
expected := "some-value"
if rdataInfo != expected {
t.Fatalf("expected context data to be '%s', received '%s'", expected, rdataInfo)
}
}
func addInfoMiddleware(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), infoKey, "some-value")
next(w, r.WithContext(ctx))
})
}
By wrapping the middleware that adds a value to the context around a http.HandlerFunc
that retrieves the data from the context, we can test to see if the value we wanted to be set was set by the middleware.