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>
173 lines
5.4 KiB
Go
173 lines
5.4 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// captureLogger is a slog.Handler that records each Record emitted, so
|
|
// requestLog tests can assert level + attrs without parsing JSON.
|
|
type capturedRecord struct {
|
|
Level slog.Level
|
|
Msg string
|
|
Attrs map[string]any
|
|
}
|
|
|
|
type captureHandler struct {
|
|
records *[]capturedRecord
|
|
}
|
|
|
|
func (h *captureHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
|
|
|
|
func (h *captureHandler) Handle(_ context.Context, r slog.Record) error {
|
|
rec := capturedRecord{Level: r.Level, Msg: r.Message, Attrs: map[string]any{}}
|
|
r.Attrs(func(a slog.Attr) bool { rec.Attrs[a.Key] = a.Value.Any(); return true })
|
|
*h.records = append(*h.records, rec)
|
|
return nil
|
|
}
|
|
|
|
func (h *captureHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
|
|
func (h *captureHandler) WithGroup(_ string) slog.Handler { return h }
|
|
|
|
func newCaptureLogger() (*slog.Logger, *[]capturedRecord) {
|
|
records := &[]capturedRecord{}
|
|
return slog.New(&captureHandler{records: records}), records
|
|
}
|
|
|
|
func TestRequestLog_StatusToSeverity(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
status int
|
|
wantLevel slog.Level
|
|
}{
|
|
{"2xx is Info", http.StatusOK, slog.LevelInfo},
|
|
{"4xx is Warn", http.StatusNotFound, slog.LevelWarn},
|
|
{"5xx is Error", http.StatusInternalServerError, slog.LevelError},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
logger, records := newCaptureLogger()
|
|
h := requestLog(logger, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(tc.status)
|
|
}))
|
|
req := httptest.NewRequest(http.MethodGet, "/something", nil)
|
|
h.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
if len(*records) != 1 {
|
|
t.Fatalf("len(records) = %d, want 1", len(*records))
|
|
}
|
|
rec := (*records)[0]
|
|
if rec.Level != tc.wantLevel {
|
|
t.Errorf("level = %v, want %v", rec.Level, tc.wantLevel)
|
|
}
|
|
if rec.Attrs["status"] != int64(tc.status) {
|
|
t.Errorf("status = %v, want %d", rec.Attrs["status"], tc.status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLog_SkipsHealthz(t *testing.T) {
|
|
logger, records := newCaptureLogger()
|
|
h := requestLog(logger, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
h.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
if len(*records) != 0 {
|
|
t.Errorf("expected /healthz to be skipped, got %d records", len(*records))
|
|
}
|
|
}
|
|
|
|
// Sanity that other paths produce a useful structured payload.
|
|
func TestRequestLog_AttributesPresent(t *testing.T) {
|
|
// Use a real slog text handler buffer so we also exercise the
|
|
// formatter (catches WithAttrs/WithGroup integration regressions).
|
|
var buf bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
h := requestLog(logger, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
req := httptest.NewRequest(http.MethodPost, "/api/something", strings.NewReader(""))
|
|
h.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
var got map[string]any
|
|
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode log line: %v\nraw: %s", err, buf.String())
|
|
}
|
|
for _, key := range []string{"method", "path", "status", "duration_ms", "remote"} {
|
|
if _, ok := got[key]; !ok {
|
|
t.Errorf("expected key %q in log entry, got %v", key, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Behind a proxy, the access log's `remote` must be the client rather than the proxy, and must agree with what the
|
|
// Active-sessions surface records for the same request. Logs and UI
|
|
// disagreeing about who connected is worse than either being wrong alone.
|
|
func TestRequestLog_RemoteHonoursTrustedProxyDepth(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
hops func() int
|
|
remoteAddr string
|
|
forwarded string
|
|
want string
|
|
}{
|
|
{
|
|
name: "nil accessor falls back to the socket peer",
|
|
hops: nil,
|
|
remoteAddr: "203.0.113.200:40000",
|
|
forwarded: "198.51.100.7",
|
|
want: "203.0.113.200",
|
|
},
|
|
{
|
|
name: "depth 0 ignores a forwarded header",
|
|
hops: func() int { return 0 },
|
|
remoteAddr: "203.0.113.200:40000",
|
|
forwarded: "198.51.100.7",
|
|
want: "203.0.113.200",
|
|
},
|
|
{
|
|
// A proxy on a PUBLIC address: a "trust XFF only from private
|
|
// peers" heuristic would log the proxy forever.
|
|
name: "depth 1 through a public-addressed proxy logs the client",
|
|
hops: func() int { return 1 },
|
|
remoteAddr: "203.0.113.200:40000",
|
|
forwarded: "198.51.100.7",
|
|
want: "198.51.100.7",
|
|
},
|
|
{
|
|
name: "depth 2 reaches through a cdn to the client",
|
|
hops: func() int { return 2 },
|
|
remoteAddr: "172.18.0.1:40000",
|
|
forwarded: "198.51.100.7, 203.0.113.50",
|
|
want: "198.51.100.7",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
logger, records := newCaptureLogger()
|
|
h := requestLog(logger, tc.hops)(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
|
|
req := httptest.NewRequest(http.MethodGet, "/api/something", nil)
|
|
req.RemoteAddr = tc.remoteAddr
|
|
req.Header.Set("X-Forwarded-For", tc.forwarded)
|
|
h.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
if len(*records) != 1 {
|
|
t.Fatalf("len(records) = %d, want 1", len(*records))
|
|
}
|
|
if got := (*records)[0].Attrs["remote"]; got != tc.want {
|
|
t.Errorf("remote = %v, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|