90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/config"
|
|
)
|
|
|
|
func TestAccessLogPath_DropsQueryAndFragment(t *testing.T) {
|
|
got := accessLogPath("/api/v1/chat?token=secret&q=1#frag")
|
|
if got != "/api/v1/chat" {
|
|
t.Fatalf("path=%q", got)
|
|
}
|
|
msg := accessLogSafeMessage("GET", "/x?password=p", 200, 3, "req_1", "")
|
|
blob := strings.ToLower(strings.Join([]string{
|
|
msg["method"].(string),
|
|
msg["path"].(string),
|
|
msg["request_id"].(string),
|
|
}, " "))
|
|
for _, leak := range []string{"password", "token", "secret", "bearer", "authorization"} {
|
|
if strings.Contains(blob, leak) {
|
|
t.Fatalf("leak %q in %#v", leak, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccessLogMiddleware_EmitsSafeFieldsOnly(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
prev := slog.Default()
|
|
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
|
t.Cleanup(func() { slog.SetDefault(prev) })
|
|
|
|
srv := &Server{cfg: &config.Config{HTTPAccessLogEnabled: true}}
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Authorization", "Bearer should-not-log")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"password":"nope","prompt":"secret text"}`))
|
|
})
|
|
handler := srv.accessLogMiddleware(inner)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/login?access_token=leak", strings.NewReader(`{"password":"x"}`))
|
|
req.Header.Set("Authorization", "Bearer super-secret")
|
|
req.Header.Set("Cookie", "session=abc")
|
|
req.Header.Set("X-Request-ID", "req_test_access")
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("status=%d", rec.Code)
|
|
}
|
|
line := buf.String()
|
|
if !strings.Contains(line, `"msg":"http_access"`) && !strings.Contains(line, `"http_access"`) {
|
|
t.Fatalf("missing http_access log: %s", line)
|
|
}
|
|
if !strings.Contains(line, `"path":"/api/v1/login"`) {
|
|
t.Fatalf("expected redacted path, got %s", line)
|
|
}
|
|
if !strings.Contains(line, `"status":201`) {
|
|
t.Fatalf("expected status, got %s", line)
|
|
}
|
|
if !strings.Contains(line, "req_test_access") {
|
|
t.Fatalf("expected request_id, got %s", line)
|
|
}
|
|
low := strings.ToLower(line)
|
|
for _, leak := range []string{"super-secret", "access_token=leak", "password", "bearer super", "session=abc", "secret text"} {
|
|
if strings.Contains(low, leak) {
|
|
t.Fatalf("log leaked %q: %s", leak, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccessLogMiddleware_DisabledIsNoOp(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
prev := slog.Default()
|
|
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
|
t.Cleanup(func() { slog.SetDefault(prev) })
|
|
|
|
srv := &Server{cfg: &config.Config{HTTPAccessLogEnabled: false}}
|
|
handler := srv.accessLogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/health", nil))
|
|
if strings.Contains(buf.String(), "http_access") {
|
|
t.Fatalf("disabled middleware must not log: %s", buf.String())
|
|
}
|
|
}
|