check / check (push) Canceled after 0s
Every route was registered for GET only, so HEAD answered 405, and on the JSON routes that 405 said "Internal server error". chi's GetHead now serves HEAD with the GET handler. The /api/v1 and /ip route groups answer a path they lack with a JSON 404 "Not found", and the other methods the router knows (POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT) with a JSON 405 "Method not allowed" whose Allow header names GET and HEAD. JSONValidationMiddleware gives a 4xx sent without a JSON body the status's standard text instead of "Internal server error". Model: opus-5-5
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestRoutesAnswerHEAD sends HEAD for a page and a JSON route through the
|
|
// server's router and checks that each answers as GET does. 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{"/", statsPath} {
|
|
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. 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).
|
|
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.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)
|
|
}
|
|
}
|