Files
summercms/modules/surf/recover_redaction_test.go
Jakub Zych 7241704e93 feat(14-01): sunscreen redacting slog handler installed by every generated main
- Wrap redacts sensitive keys at any depth and scrubs Bearer, sk- and x-api-key shapes
- InstallDefault is the first statement of the generated run; hello main regenerated
- surf test pins that recovered panics echo no credential
- sunscreen README, root modules row and the logging docs page
2026-10-03 20:01:36 +02:00

75 lines
2.4 KiB
Go

package surf
import (
"bytes"
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/sunscreen"
)
// TestRecoverHidesPanicDetails pins the SafeExceptionResponse behaviour: a
// panicking handler's message, here carrying credentials, never reaches the
// response in either group type, and a log record of the panic written
// through a sunscreen handler carries neither credential.
func TestRecoverHidesPanicDetails(t *testing.T) {
const skKey = "sk-abcdefghijklmnopqrstuvwxyz0123"
const token = "eyJhbGciOiJIUzI1NiJ9.claims.signature"
panicErr := errors.New("vendor rejected key " + skKey + " with Authorization: Bearer " + token)
prev := slog.Default()
t.Cleanup(func() { slog.SetDefault(prev) })
var logs bytes.Buffer
sunscreen.InstallDefault(&logs)
r := New(nil)
r.GroupRaw("/oauth", nil, func(g pact.Router) {
g.Post("/token", func(http.ResponseWriter, *http.Request) { panic(panicErr) })
})
r.Post("/api/v1/recognize", func(http.ResponseWriter, *http.Request) { panic(panicErr) })
h, err := r.compile()
if err != nil {
t.Fatal(err)
}
cases := []struct {
path, body, contentType string
}{
{"/api/v1/recognize", `{"error":true,"message":"Internal server error"}`, "application/json"},
{"/oauth/token", "", ""},
}
for _, c := range cases {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, c.path, nil))
if rec.Code != http.StatusInternalServerError {
t.Fatalf("%s status = %d", c.path, rec.Code)
}
body := strings.TrimSpace(rec.Body.String())
if body != c.body {
t.Fatalf("%s body = %q, want the opaque %q", c.path, body, c.body)
}
if got := rec.Header().Get("Content-Type"); got != c.contentType {
t.Fatalf("%s Content-Type = %q", c.path, got)
}
for _, secret := range []string{skKey, token, "vendor rejected"} {
if strings.Contains(rec.Body.String(), secret) {
t.Fatalf("%s response echoes %q", c.path, secret)
}
}
}
slog.Error("handler panicked", "err", panicErr, "path", "/api/v1/recognize")
out := logs.String()
if strings.Contains(out, skKey) || strings.Contains(out, token) {
t.Fatalf("log record leaks a credential:\n%s", out)
}
if !strings.Contains(out, "sk-[REDACTED]") || !strings.Contains(out, "Bearer [REDACTED]") {
t.Fatalf("log record not scrubbed as expected:\n%s", out)
}
}