Files
summercms/modules/bouncer/dispatch_test.go
Jakub Zych 3bbbe6c11e test(19.7-05): phase gate with the go and marker stages, pier test helpers, TestToolsList and TestDispatchMarker
- scripts/check-phase19.7.sh: fail-closed multi-repo gate (self-test, go,
  marker; plugins, consumers, docker, admin and docs are stubs until the
  later tasks fill them)
- modules/pier/helpers_test.go: the plan 02 fixtures moved out of the smokes
  plus fakeGuard, fakeVerifier, fakeLinkResolver, fakePersonalResolver,
  routePlugin, newApp, twoInstances, sdkClient and fireSubjectChanged
- TestToolsList (pier), TestDispatchMarker (bouncer, cabana with a Postgres
  part through the screens harness, which now keeps its app)
2026-10-23 13:31:41 +02:00

125 lines
4.5 KiB
Go

package bouncer
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
// headerGuard authenticates a request whose Authorization header is exactly
// "Bearer good" as principal 7 and writes its own 401 otherwise, so a test
// sees whether the guard or the dispatch marker decided a request.
type headerGuard struct{ calls *int }
func (g headerGuard) AuthenticateCredential(r *http.Request) (*Principal, any, error) {
*g.calls++
if r.Header.Get("Authorization") == "Bearer good" {
return &Principal{ID: 7}, "guard-credential", nil
}
return nil, nil, errors.New("no token")
}
func (headerGuard) WriteUnauthorized(w http.ResponseWriter, _ error) {
w.WriteHeader(http.StatusUnauthorized)
}
// TestDispatchMarker (phase 19.7 D-17): Registry.Middleware honours a
// dispatch marker that lists its guard name (the guard does not run, the
// marker's principal and credential reach the handler), ignores a marker for
// other guards and a marker without a principal (the guard decides), and
// never reads a header, cookie or query string for it: a request with forged
// dispatch headers is authenticated by the guard alone.
func TestDispatchMarker(t *testing.T) {
calls := 0
reg := NewRegistry()
if err := reg.Register("acme", "api_token", headerGuard{calls: &calls}); err != nil {
t.Fatal(err)
}
mw, err := reg.Middleware("api_token")
if err != nil {
t.Fatal(err)
}
type seen struct {
id uint
cred any
ok bool
}
var got seen
h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p, ok := User(r.Context())
cred, _ := Credential(r.Context())
got = seen{ok: ok, cred: cred}
if ok {
got.id = p.ID
}
w.WriteHeader(http.StatusNoContent)
}))
serve := func(r *http.Request) int {
got = seen{}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, r)
return rec.Code
}
withMarker := func(r *http.Request, d *Dispatch) *http.Request {
return r.WithContext(WithDispatch(r.Context(), d))
}
t.Run("marker for the guard is honoured without running it", func(t *testing.T) {
calls = 0
r := withMarker(httptest.NewRequest(http.MethodGet, "/x", nil), &Dispatch{Guards: []string{"backend", "api_token"}, Principal: &Principal{ID: 42}, Credential: "marker-credential"})
if code := serve(r); code != http.StatusNoContent || got.id != 42 || got.cred != "marker-credential" || calls != 0 {
t.Fatalf("code %d, seen %+v, guard calls %d", code, got, calls)
}
})
t.Run("marker for other guards is ignored", func(t *testing.T) {
calls = 0
r := withMarker(httptest.NewRequest(http.MethodGet, "/x", nil), &Dispatch{Guards: []string{"backend"}, Principal: &Principal{ID: 42}})
if code := serve(r); code != http.StatusUnauthorized || calls != 1 {
t.Fatalf("code %d, guard calls %d", code, calls)
}
r = withMarker(httptest.NewRequest(http.MethodGet, "/x", nil), &Dispatch{Guards: []string{"backend"}, Principal: &Principal{ID: 42}})
r.Header.Set("Authorization", "Bearer good")
if code := serve(r); code != http.StatusNoContent || got.id != 7 || got.cred != "guard-credential" {
t.Fatalf("guard after an unlisted marker: code %d, seen %+v", code, got)
}
})
t.Run("marker without a principal is ignored", func(t *testing.T) {
calls = 0
r := withMarker(httptest.NewRequest(http.MethodGet, "/x", nil), &Dispatch{Guards: []string{"api_token"}})
if code := serve(r); code != http.StatusUnauthorized || calls != 1 {
t.Fatalf("code %d, guard calls %d", code, calls)
}
var nilMarker *Dispatch
r = withMarker(httptest.NewRequest(http.MethodGet, "/x", nil), nilMarker)
if code := serve(r); code != http.StatusUnauthorized {
t.Fatalf("nil marker: code %d", code)
}
})
t.Run("forged headers and cookies are the guard's", func(t *testing.T) {
calls = 0
r := httptest.NewRequest(http.MethodGet, "/x?dispatch=42&summer_dispatch=api_token", nil)
r.Header.Set("X-Summer-Dispatch", "42")
r.Header.Set("X-Dispatch-Guards", "api_token")
r.Header.Set("Authorization", "Dispatch 42")
r.AddCookie(&http.Cookie{Name: "summer_dispatch", Value: "42"})
if code := serve(r); code != http.StatusUnauthorized || calls != 1 || got.ok {
t.Fatalf("forged request: code %d, seen %+v, guard calls %d", code, got, calls)
}
r.Header.Set("Authorization", "Bearer good")
if code := serve(r); code != http.StatusNoContent || got.id != 7 {
t.Fatalf("forged headers with a real token: code %d, seen %+v", code, got)
}
})
t.Run("plain context", func(t *testing.T) {
if _, ok := Dispatched(context.Background(), "api_token"); ok {
t.Fatal("a plain context carries a marker")
}
})
}