tsweb: refactor JSONHandler to take status code from error if it is present (#905)

This change is to make JSONHandler error handling intuitive, as before there would be two sources of HTTP status code when HTTPErrors were generated: one as the first return value of the handler function, and one nested inside the HTTPError. Previously, it took the first return value as the status code, and ignored the code inside the HTTPError. Now, it should expect the first return value to be 0 if there is an error, and it takes the status code of the HTTPError to set as the response code.

Signed-off-by: Daniel Chung <daniel@tailscale.com>
This commit is contained in:
chungdaniel
2020-11-10 09:52:26 -05:00
committed by GitHub
parent 6e52633c53
commit e7ac9a4b90
3 changed files with 69 additions and 23 deletions

View File

@ -11,6 +11,8 @@ import (
"net/http/httptest"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
type Data struct {
@ -40,11 +42,11 @@ func TestNewJSONHandler(t *testing.T) {
if d.Status == status {
t.Logf("ok: %s", d.Status)
} else {
t.Fatalf("wrong status: %s %s", d.Status, status)
t.Fatalf("wrong status: got: %s, want: %s", d.Status, status)
}
if w.Code != code {
t.Fatalf("wrong status code: %d %d", w.Code, code)
t.Fatalf("wrong status code: got: %d, want: %d", w.Code, code)
}
if w.Header().Get("Content-Type") != "application/json" {
@ -67,7 +69,7 @@ func TestNewJSONHandler(t *testing.T) {
t.Run("403 HTTPError", func(t *testing.T) {
h := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
return http.StatusForbidden, nil, fmt.Errorf("forbidden")
return 0, nil, Error(http.StatusForbidden, "forbidden", nil)
})
w := httptest.NewRecorder()
@ -90,11 +92,11 @@ func TestNewJSONHandler(t *testing.T) {
h31 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
body := new(Data)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
return http.StatusBadRequest, nil, err
return 0, nil, Error(http.StatusBadRequest, err.Error(), err)
}
if body.Name == "" {
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "name is empty", nil)
return 0, nil, Error(http.StatusBadRequest, "name is empty", nil)
}
return http.StatusOK, nil, nil
@ -126,13 +128,13 @@ func TestNewJSONHandler(t *testing.T) {
h32 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
body := new(Data)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
return http.StatusBadRequest, nil, err
return 0, nil, Error(http.StatusBadRequest, err.Error(), err)
}
if body.Name == "root" {
return http.StatusInternalServerError, nil, fmt.Errorf("invalid name")
return 0, nil, fmt.Errorf("invalid name")
}
if body.Price == 0 {
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "price is empty", nil)
return 0, nil, Error(http.StatusBadRequest, "price is empty", nil)
}
return http.StatusOK, &Data{Price: body.Price * 2}, nil
@ -159,7 +161,7 @@ func TestNewJSONHandler(t *testing.T) {
}
})
t.Run("500 internal server error", func(t *testing.T) {
t.Run("500 internal server error (unspecified error, not of type HTTPError)", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
h32.ServeHTTPReturn(w, r)
@ -189,4 +191,41 @@ func TestNewJSONHandler(t *testing.T) {
}).ServeHTTPReturn(w, r)
checkStatus(w, "error", http.StatusInternalServerError)
})
t.Run("403 forbidden, status returned by JSONHandlerFunc and HTTPError agree", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/", nil)
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
return http.StatusForbidden, nil, Error(http.StatusForbidden, "403 forbidden", nil)
}).ServeHTTPReturn(w, r)
want := &Response{
Status: "error",
Data: &Data{},
Error: "403 forbidden",
}
got := checkStatus(w, "error", http.StatusForbidden)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf(diff)
}
})
t.Run("403 forbidden, status returned by JSONHandlerFunc and HTTPError do not agree", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/", nil)
err := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
return http.StatusInternalServerError, nil, Error(http.StatusForbidden, "403 forbidden", nil)
}).ServeHTTPReturn(w, r)
if !strings.HasPrefix(err.Error(), "[unexpected]") {
t.Fatalf("returned error should have `[unexpected]` to note the disagreeing status codes: %v", err)
}
want := &Response{
Status: "error",
Data: &Data{},
Error: "403 forbidden",
}
got := checkStatus(w, "error", http.StatusForbidden)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("(-want,+got):\n%s", diff)
}
})
}