textmachine/platform/internal/auth/principal_test.go

79 lines
3 KiB
Go

package auth
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// principal_test.go: that "authenticated, but nobody can revoke them" is not a state this package
// can hand out.
//
// PD-379 was exactly that state, and the first shape of its fix kept it reachable: the identity and
// the means to re-ask travelled as two separate context values, so a route mounted outside the guard
// — or a future one that minted a principal some other way — produced a caller with an id and no way
// to check it, and a handler could only fail closed by remembering to. They are ONE value now, and
// what that buys is checked here rather than described.
// A principal that did not come from Require answers "not live". The direction is the whole point:
// the only way this can be wrong must be the way that refuses.
//
// Mutation caught: returning nil from StillLive when the store is absent.
func TestAPrincipalNobodyAuthenticatedIsNotLive(t *testing.T) {
for name, p := range map[string]Principal{
"the zero value": {},
"an id someone set": {UserID: "u1"},
"an id and a presence": {UserID: "u1", Via: ViaBearer},
} {
t.Run(name, func(t *testing.T) {
if err := p.StillLive(context.Background()); !errors.Is(err, ErrNoSession) {
t.Errorf("StillLive = %v, want %v: a caller whose session nothing can look up must "+
"not be able to hold a long-lived response open", err, ErrNoSession)
}
})
}
}
// The probe a real principal carries asks about the SAME credential the guard admitted, and asks the
// store rather than remembering the guard's answer.
//
// Mutation caught: binding an empty digest; caching Lookup's verdict instead of re-asking.
func TestTheProbeAsksTheStoreAboutTheCredentialThatWasPresented(t *testing.T) {
now := time.Now()
token := NewToken()
store := &fakeStore{session: Session{
UserID: "u1", IdleExpiresAt: now.Add(time.Hour), AbsoluteExpiresAt: now.Add(time.Hour),
}}
a, _ := newAuth(store, now)
var got Principal
h := a.Require(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
got, _ = FromContext(r.Context())
}))
r := httptest.NewRequest("GET", "/v0/books/bk_1/events", nil)
r.Header.Set("Authorization", "Bearer "+token)
h.ServeHTTP(httptest.NewRecorder(), r)
if got.UserID != "u1" {
t.Fatalf("the guard produced %+v", got)
}
// Live now…
if err := got.StillLive(context.Background()); err != nil {
t.Fatalf("a freshly admitted caller is not live: %v", err)
}
if want := Digest(token); string(store.digest) != string(want) {
t.Errorf("the probe asked about a different credential than the one presented")
}
if store.asked == 0 {
t.Error("the probe answered without asking the store: a verdict remembered from the door is " +
"the defect, not the fix")
}
// …and gone a moment later, WITHOUT the request being made again. That is the whole mechanism.
store.live = ErrNoSession
if err := got.StillLive(context.Background()); !errors.Is(err, ErrNoSession) {
t.Errorf("StillLive = %v after the session was revoked, want %v", err, ErrNoSession)
}
}