Fix HTMX expired-session handling, CSP-blocked form behaviors, reorder recovery, billing currency display, plan/checkout guards, FedWiki quota edge cases, and operator/member empty/error states. Add entitlement uniqueness migrations, canonical migration source wiring, and regression coverage for the remediated flows. Update status docs with the audit triage and model inventory.
103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|