package auth import ( "bytes" "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/require" ) type stubValidator struct{ login string } func (s stubValidator) ValidateToken(context.Context, string) (string, bool) { return s.login, s.login != "" } // gitea-mcp#60: PassthroughMiddleware logged nothing at all, success or failure. // // While verifying #59 end to end there was no way to confirm from the server // logs whether calls were using the new PAT pass-through (caller's own Gitea // PAT, own identity) or falling through to the shared JWT/static path. The only // way to reason about it was inference. The chassis names its two paths as of // v0.7.0; this is the third. // // This matters more than the other two: pass-through is the path where the // request acts as a SPECIFIC Gitea user. An audit trail that cannot say which // identity performed a write is not an audit trail. func TestPassthroughMiddleware_LogsTheAcceptedPath(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) }) reached := false h := PassthroughMiddleware(stubValidator{login: "alice"}, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true }), http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Fatal("must not fall back") })) req := httptest.NewRequest(http.MethodGet, "/mcp", nil) req.Header.Set("Authorization", "Bearer a-real-gitea-pat") h.ServeHTTP(httptest.NewRecorder(), req) require.True(t, reached) var line map[string]any require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(buf.String())), &line)) require.Equal(t, "mcp auth accepted", line["msg"]) require.Equal(t, "passthrough", line["auth_path"]) require.Equal(t, "alice", line["caller"]) require.NotEmpty(t, line["token_fp"], "correlatable without being a credential") for k, v := range line { require.NotContains(t, []any{v}, "a-real-gitea-pat", "the PAT must never appear in any field, including %q", k) } } // A token that is not a valid PAT must fall through silently to the chassis, // which owns the rejection log. Logging here as well would double-count every // static-token request — every one of them reaches this middleware first. func TestPassthroughMiddleware_FallsThroughWithoutLogging(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) }) fellBack := false h := PassthroughMiddleware(stubValidator{}, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Fatal("must not accept") }), http.HandlerFunc(func(http.ResponseWriter, *http.Request) { fellBack = true })) req := httptest.NewRequest(http.MethodGet, "/mcp", nil) req.Header.Set("Authorization", "Bearer not-a-pat") h.ServeHTTP(httptest.NewRecorder(), req) require.True(t, fellBack) require.Empty(t, strings.TrimSpace(buf.String()), "the chassis owns the verdict; logging here too would double-count every static-token call") }