package authn import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-ecore/login" ) // captureHandler records the caller present on the request context when reached. func captureHandler(dst **auth.AuthContext, reached *bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { *reached = true *dst = CallerFromContext(r.Context()) w.WriteHeader(http.StatusOK) }) } func runCookieMiddleware(t *testing.T, cookie *http.Cookie) (*auth.AuthContext, int) { t.Helper() var got *auth.AuthContext var reached bool h := OptionalCookieMiddleware()(captureHandler(&got, &reached)) req := httptest.NewRequest(http.MethodGet, "/", nil) if cookie != nil { req.AddCookie(cookie) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) require.True(t, reached, "middleware must always call next (never rejects)") return got, rec.Code } func TestOptionalCookieMiddleware_ValidCookie(t *testing.T) { withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{ "bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER), }}) got, code := runCookieMiddleware(t, &http.Cookie{ Name: login.CookieName, Value: forgeCookie(t, "bigbes"), }) require.Equal(t, http.StatusOK, code) require.NotNil(t, got, "expected an authenticated caller") assert.Equal(t, "bigbes", got.Username) assert.Equal(t, 1, got.UserID) assert.Equal(t, auth.AUTH_COOKIE, got.AuthMethod) } // TestOptionalCookieMiddleware_StripsCanonicalSigil pins a difference from the // decode this package used to carry: a payload holding the canonical "~name" // form resolves to the bare name rather than being looked up with the sigil // still on it — which meta would answer for nobody. func TestOptionalCookieMiddleware_StripsCanonicalSigil(t *testing.T) { stub := &stubBackend{users: map[string]auth.AuthContext{ "bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER), }} withStubBackend(t, stub) got, code := runCookieMiddleware(t, &http.Cookie{ Name: login.CookieName, Value: forgeCookie(t, "~bigbes"), }) require.Equal(t, http.StatusOK, code) require.NotNil(t, got) assert.Equal(t, "bigbes", got.Username) // The backend is asked for the bare name, not "~bigbes". Asserting on the // resolved caller alone would not show this: the stub normalises the sigil // away itself, as meta.LookupUser does, so both spellings resolve either // way and only the recorded argument says which one was sent. assert.Equal(t, []string{"bigbes"}, stub.lookedUp) } func TestOptionalCookieMiddleware_SuspendedStillResolves(t *testing.T) { withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{ "susp": sampleUser(2, "susp", auth.USER_TYPE_SUSPENDED), }}) got, _ := runCookieMiddleware(t, &http.Cookie{ Name: login.CookieName, Value: forgeCookie(t, "susp"), }) require.NotNil(t, got, "suspended users must still resolve (reads are allowed)") assert.True(t, AsCoreCaller(got).Suspended, "resolved caller must be flagged suspended") } // TestOptionalCookieMiddleware_UnusableCookieIsAnonymous is this package's own // contract rather than login's: whatever the decode answers, the middleware // resolves nobody and still calls next. It is what keeps public browsing and // public clones working when anything about the session goes wrong. // // The name cases are the second thing this package used not to do. A cookie // naming "../../etc/passwd" or an empty string decoded to that string and went // straight to the user lookup; now it never reaches the backend at all, and the // assertion below is that it did not. func TestOptionalCookieMiddleware_UnusableCookieIsAnonymous(t *testing.T) { cases := []struct { name string cookie func(t *testing.T) *http.Cookie }{ {"no cookie at all", func(*testing.T) *http.Cookie { return nil }}, {"undecryptable value", func(*testing.T) *http.Cookie { return &http.Cookie{Name: login.CookieName, Value: "not-a-valid-fernet-token"} }}, {"empty name in payload", func(t *testing.T) *http.Cookie { return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "")} }}, {"path separator in name", func(t *testing.T) *http.Cookie { return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "../../etc/passwd")} }}, {"newline in name", func(t *testing.T) *http.Cookie { return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "alice\nWARN forged")} }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { stub := &stubBackend{users: map[string]auth.AuthContext{ "bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER), }} withStubBackend(t, stub) got, code := runCookieMiddleware(t, c.cookie(t)) assert.Nil(t, got, "must degrade to anonymous") assert.Equal(t, http.StatusOK, code, "must not reject") assert.Empty(t, stub.lookedUp, "an unusable cookie must not reach the user lookup") }) } } func TestOptionalCookieMiddleware_UnknownUser(t *testing.T) { withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{}}) got, code := runCookieMiddleware(t, &http.Cookie{ Name: login.CookieName, Value: forgeCookie(t, "ghost"), }) assert.Nil(t, got, "unknown user must degrade to anonymous") assert.Equal(t, http.StatusOK, code, "must not reject") } func TestOptionalCookieMiddleware_BackendDownIsAnonymous(t *testing.T) { withStubBackend(t, &stubBackend{lookupErr: errBackendDown}) got, code := runCookieMiddleware(t, &http.Cookie{ Name: login.CookieName, Value: forgeCookie(t, "bigbes"), }) assert.Nil(t, got, "backend failure must degrade to anonymous") assert.Equal(t, http.StatusOK, code, "must not 500") }