Files
minstrel/internal/server/hygiene_test.go
T
bvandeusenandClaude Opus 5.5 d191a28217 docs: server comments say what the code does, not how it came to be (#6277)
Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.

Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
  it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
  not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
  today rather than a milestone that would change it.

Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:15:05 -04:00

129 lines
4.6 KiB
Go

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)
}
})
}
}
func TestSecurityHeaders(t *testing.T) {
serve := func(hops int, proto string) http.Header {
h := securityHeaders(func() int { return hops })(okHandler())
req := httptest.NewRequest(http.MethodGet, "/api/me", nil)
if proto != "" {
req.Header.Set("X-Forwarded-Proto", proto)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
return w.Header()
}
base := serve(0, "")
for key, want := range map[string]string{
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-Frame-Options": "DENY",
} {
if got := base.Get(key); got != want {
t.Errorf("%s = %q, want %q", key, got, want)
}
}
if !strings.Contains(base.Get("Permissions-Policy"), "microphone=()") {
t.Errorf("Permissions-Policy = %q", base.Get("Permissions-Policy"))
}
// HSTS with no trusted proxy, and through one saying https or http.
if got := serve(0, "https").Get("Strict-Transport-Security"); got != "" {
t.Errorf("hops 0 + forwarded https: HSTS = %q, want none", got)
}
if got := serve(1, "https").Get("Strict-Transport-Security"); got == "" {
t.Error("hops 1 + forwarded https: want HSTS")
}
if got := serve(1, "").Get("Strict-Transport-Security"); got != "" {
t.Errorf("plain request: HSTS = %q, want none", got)
}
}