package auth import ( "net/http" "net/http/httptest" "testing" "github.com/alexedwards/scs/v2" ) // newTestConfig builds a minimal Config whose Middleware only touches // SessionManager, so no Redis/OIDC wiring is needed. scs.New() defaults to // an in-memory store when Store is left nil. func newTestConfig() *Config { sm := scs.New() return &Config{SessionManager: sm} } // TestMiddleware_ExpiredSession_HTMXRequest_RespondsWithHXRedirect covers // finding #15: an HTMX request against an expired/unauthenticated session // used to get a 302 to the (cross-origin) Keycloak authorize URL, which the // XHR can't follow — error-handler.js showed a lying "Network error" toast. // The middleware must instead respond 401 with HX-Redirect: /login so htmx // performs a same-origin client-side redirect. func TestMiddleware_ExpiredSession_HTMXRequest_RespondsWithHXRedirect(t *testing.T) { cfg := newTestConfig() mw := cfg.Middleware() nextCalled := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true }) req := httptest.NewRequest(http.MethodGet, "/partials/operator/organizations/abc/enrollment", nil) req.Header.Set("HX-Request", "true") rec := httptest.NewRecorder() cfg.SessionManager.LoadAndSave(mw(next)).ServeHTTP(rec, req) if nextCalled { t.Fatal("expected middleware to short-circuit an unauthenticated HTMX request, but next handler ran") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } if got := rec.Header().Get("HX-Redirect"); got != "/login" { t.Fatalf("expected HX-Redirect header %q, got %q", "/login", got) } } // TestMiddleware_ExpiredSession_NonHTMXRequest_RedirectsToLogin verifies a // plain browser navigation still gets the original 302-to-/login behavior // (unchanged by the finding #15 fix). func TestMiddleware_ExpiredSession_NonHTMXRequest_RedirectsToLogin(t *testing.T) { cfg := newTestConfig() mw := cfg.Middleware() next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("expected middleware to short-circuit an unauthenticated request, but next handler ran") }) req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) rec := httptest.NewRecorder() cfg.SessionManager.LoadAndSave(mw(next)).ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("expected 302, got %d", rec.Code) } if got := rec.Header().Get("Location"); got != "/login" { t.Fatalf("expected Location header %q, got %q", "/login", got) } if got := rec.Header().Get("HX-Redirect"); got != "" { t.Fatalf("did not expect HX-Redirect on a non-HTMX request, got %q", got) } } // TestMiddleware_PublicPath_SkipsAuth guards against a regression where the // HX-Request branch swallows the public-path bypass. func TestMiddleware_PublicPath_SkipsAuth(t *testing.T) { cfg := newTestConfig() mw := cfg.Middleware() nextCalled := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true w.WriteHeader(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/login", nil) req.Header.Set("HX-Request", "true") rec := httptest.NewRecorder() cfg.SessionManager.LoadAndSave(mw(next)).ServeHTTP(rec, req) if !nextCalled { t.Fatal("expected /login to bypass auth and reach the next handler") } if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } }