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.

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.