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{"/", 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. 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) } }