check / check (push) Canceled after 0s
A panic in a JSON route's handler answered 500 with an empty body, because the router's recoverer sits outside the middleware that adds `@meta`. `JSONValidationMiddleware` now wraps the handler in the same recoverer, and answers the panic with the JSON error body. `/status.json` and the health check answered chi's empty 405 to any method they do not take. They now answer with the same JSON 405 as the routes under `/api/v1` and `/ip`. The home page's IP lookup card showed only the answer's `data`. It now shows the whole answer, `@meta` included. A table test checks `@meta` on every JSON route, in its successes and its errors. Model: opus-5-5
112 lines
3.4 KiB
Go
112 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestRoutesAnswerHEAD sends HEAD for a page and JSON routes through the
|
|
// server's router and checks that each answers as GET does. /ip and /ip/, the
|
|
// lookup of the caller's own address, are in the list because chi's GetHead
|
|
// does not reach them. Before the fix every route answered HEAD with 405
|
|
// (https://git.eeqj.de/sneak/routewatch/issues/111).
|
|
func TestRoutesAnswerHEAD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newServerWithTestRoutes(t)
|
|
|
|
for _, path := range []string{"/", statusJSONPath, statsPath, "/ip", "/ip/"} {
|
|
rec := httptest.NewRecorder()
|
|
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodHead, path, nil))
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("HEAD %s: status %d, want %d; body %s",
|
|
path, rec.Code, http.StatusOK, rec.Body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJSONRoutesAnswerUnmatchedRequestsWithJSONErrors sends a method the JSON
|
|
// routes do not take, and a path none of them matches, to each group of JSON
|
|
// routes through the server's router. It also sends a method they do not take
|
|
// to /status.json and the health check. Each must answer with the JSON error
|
|
// body, and a 405 must name the methods the routes take in its Allow header.
|
|
// Before the fix a 405 said "Internal server error" and a 404 was chi's plain
|
|
// text answer (https://git.eeqj.de/sneak/routewatch/issues/111).
|
|
// /status.json and the health check gave chi's empty 405
|
|
// (https://git.eeqj.de/sneak/routewatch/issues/201).
|
|
func TestJSONRoutesAnswerUnmatchedRequestsWithJSONErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newServerWithTestRoutes(t)
|
|
|
|
tests := []struct {
|
|
method string
|
|
path string
|
|
wantCode int
|
|
wantMsg string
|
|
}{
|
|
{http.MethodPost, statsPath, http.StatusMethodNotAllowed, methodNotAllowedMessage},
|
|
{http.MethodPost, "/ip/", http.StatusMethodNotAllowed, methodNotAllowedMessage},
|
|
{
|
|
http.MethodDelete, statusJSONPath,
|
|
http.StatusMethodNotAllowed, methodNotAllowedMessage,
|
|
},
|
|
{
|
|
http.MethodOptions, healthCheckPath,
|
|
http.StatusMethodNotAllowed, methodNotAllowedMessage,
|
|
},
|
|
{http.MethodGet, "/api/v1/nothing", http.StatusNotFound, notFoundMessage},
|
|
{http.MethodGet, "/ip/192.0.2.1/nothing", http.StatusNotFound, notFoundMessage},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := tt.method + " " + tt.path
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
t.Context(), tt.method, tt.path, nil))
|
|
|
|
if rec.Code != tt.wantCode {
|
|
t.Errorf("%s: status %d, want %d; body %s",
|
|
name, rec.Code, tt.wantCode, rec.Body)
|
|
}
|
|
|
|
checkJSONErrorCode(t, name, rec, tt.wantCode)
|
|
checkJSONErrorMessage(t, name, rec, tt.wantMsg)
|
|
|
|
allow := rec.Header().Get("Allow")
|
|
if tt.wantCode == http.StatusMethodNotAllowed && allow != jsonRoutesAllow {
|
|
t.Errorf("%s: Allow %q, want %q", name, allow, jsonRoutesAllow)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkJSONErrorMessage checks that rec holds the JSON error body with the
|
|
// given message.
|
|
func checkJSONErrorMessage(
|
|
t *testing.T, name string, rec *httptest.ResponseRecorder, want string,
|
|
) {
|
|
t.Helper()
|
|
|
|
var body struct {
|
|
Error struct {
|
|
Msg string `json:"msg"`
|
|
} `json:"error"`
|
|
}
|
|
|
|
err := json.Unmarshal(rec.Body.Bytes(), &body)
|
|
if err != nil {
|
|
t.Errorf("%s: decoding the answer %q: %v", name, rec.Body, err)
|
|
|
|
return
|
|
}
|
|
|
|
if body.Error.Msg != want {
|
|
t.Errorf("%s: error message %q, want %q", name, body.Error.Msg, want)
|
|
}
|
|
}
|