package server import ( "io" "net/http" "net/http/httptest" "strings" "testing" "git.fabledsword.com/bvandeusen/minstrel/internal/auth" ) func okHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body != nil { if _, err := io.ReadAll(r.Body); err != nil { http.Error(w, "too large", http.StatusRequestEntityTooLarge) return } } w.WriteHeader(http.StatusNoContent) }) } func TestLimitRequestBody_RejectsOversizedBody(t *testing.T) { h := limitRequestBody(okHandler()) big := strings.NewReader(strings.Repeat("x", maxRequestBody+1)) w := httptest.NewRecorder() h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/api/playlists", big)) if w.Code != http.StatusRequestEntityTooLarge { t.Errorf("oversized body: status = %d, want the handler's read to fail", w.Code) } small := strings.NewReader(`{"name":"ok"}`) w = httptest.NewRecorder() h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/api/playlists", small)) if w.Code != http.StatusNoContent { t.Errorf("small body: status = %d, want 204", w.Code) } } // The cases a cross-site form or no-preflight fetch could produce must be // refused when the session cookie rides along; everything a real client // sends must pass. func TestRequireJSONForCookieWrites(t *testing.T) { cookie := &http.Cookie{Name: auth.SessionCookieName, Value: "tok"} tests := []struct { name string method string path string contentType string body string withCookie bool want int }{ {"form post with cookie", http.MethodPost, "/api/me/password", "application/x-www-form-urlencoded", "a=b", true, http.StatusUnsupportedMediaType}, {"text/plain post with cookie", http.MethodPost, "/api/me/password", "text/plain", `{"a":1}`, true, http.StatusUnsupportedMediaType}, {"multipart with cookie", http.MethodPut, "/api/me/profile", "multipart/form-data; boundary=x", "--x--", true, http.StatusUnsupportedMediaType}, {"body without content type", http.MethodPost, "/api/me/profile", "", `{"a":1}`, true, http.StatusUnsupportedMediaType}, {"json with cookie", http.MethodPost, "/api/me/password", "application/json", `{}`, true, http.StatusNoContent}, {"json with charset", http.MethodPost, "/api/me/password", "application/json; charset=utf-8", `{}`, true, http.StatusNoContent}, {"bodiless delete with cookie", http.MethodDelete, "/api/me/sessions/1", "", "", true, http.StatusNoContent}, {"form post without cookie (bearer client)", http.MethodPost, "/api/me/password", "text/plain", "x", false, http.StatusNoContent}, {"subsonic post is not /api", http.MethodPost, "/rest/scrobble", "application/x-www-form-urlencoded", "id=1", true, http.StatusNoContent}, {"GET is never checked", http.MethodGet, "/api/me", "text/plain", "", true, http.StatusNoContent}, } h := requireJSONForCookieWrites(okHandler()) for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var body io.Reader if tc.body != "" { body = strings.NewReader(tc.body) } req := httptest.NewRequest(tc.method, tc.path, body) if tc.contentType != "" { req.Header.Set("Content-Type", tc.contentType) } if tc.withCookie { req.AddCookie(cookie) } w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != tc.want { t.Errorf("status = %d, want %d", w.Code, tc.want) } }) } }