196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type authorizationFixture struct {
|
|
RequiredClientID string `json:"requiredClientId"`
|
|
SessionSecret string `json:"sessionSecret"`
|
|
NowUnix int64 `json:"nowUnix"`
|
|
Cases []struct {
|
|
Name string `json:"name"`
|
|
Session Session `json:"session"`
|
|
Snapshot *AuthorizationSnapshot `json:"snapshot"`
|
|
Expected struct {
|
|
Outcome string `json:"outcome"`
|
|
Reason RejectionReason `json:"reason"`
|
|
LoaderCalls int `json:"loaderCalls"`
|
|
Session Session `json:"session"`
|
|
} `json:"expected"`
|
|
} `json:"cases"`
|
|
}
|
|
|
|
type recordingAuthorizationLoader struct {
|
|
snapshot AuthorizationSnapshot
|
|
found bool
|
|
err error
|
|
ids []string
|
|
}
|
|
|
|
func (loader *recordingAuthorizationLoader) FindAuthorizationSnapshot(_ context.Context, id string) (AuthorizationSnapshot, bool, error) {
|
|
loader.ids = append(loader.ids, id)
|
|
return loader.snapshot, loader.found, loader.err
|
|
}
|
|
|
|
func TestResolverDrivesPlatformAuthorizationContract(t *testing.T) {
|
|
fixture := loadAuthorizationFixture(t)
|
|
if len(fixture.Cases) != 14 {
|
|
t.Fatalf("authorization fixture cases = %d, want 14", len(fixture.Cases))
|
|
}
|
|
|
|
for _, testCase := range fixture.Cases {
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
cookie, err := json.Marshal(testCase.Session)
|
|
if err != nil {
|
|
t.Fatalf("marshal session fixture: %v", err)
|
|
}
|
|
signed, err := Sign(cookie, fixture.SessionSecret)
|
|
if err != nil {
|
|
t.Fatalf("sign session fixture: %v", err)
|
|
}
|
|
|
|
loader := &recordingAuthorizationLoader{}
|
|
if testCase.Snapshot != nil {
|
|
loader.snapshot = *testCase.Snapshot
|
|
loader.found = true
|
|
}
|
|
resolver := NewResolver(loader, fixture.SessionSecret, fixture.RequiredClientID, func() time.Time {
|
|
return time.Unix(fixture.NowUnix, 0)
|
|
})
|
|
|
|
got, resolveErr := resolver.Resolve(context.Background(), signed)
|
|
if len(loader.ids) != testCase.Expected.LoaderCalls {
|
|
t.Fatalf("loader calls = %d, want %d", len(loader.ids), testCase.Expected.LoaderCalls)
|
|
}
|
|
if len(loader.ids) == 1 && loader.ids[0] != testCase.Session.User.ID {
|
|
t.Fatalf("loader ID = %q, want %q", loader.ids[0], testCase.Session.User.ID)
|
|
}
|
|
|
|
switch testCase.Expected.Outcome {
|
|
case "authenticated":
|
|
if resolveErr != nil {
|
|
t.Fatalf("Resolve() error = %v", resolveErr)
|
|
}
|
|
if !reflect.DeepEqual(got, testCase.Expected.Session) {
|
|
t.Errorf("resolved session mismatch\n got: %#v\nwant: %#v", got, testCase.Expected.Session)
|
|
}
|
|
case "unauthenticated":
|
|
var rejection *UnauthenticatedError
|
|
if !errors.As(resolveErr, &rejection) {
|
|
t.Fatalf("Resolve() error = %v, want typed unauthenticated rejection", resolveErr)
|
|
}
|
|
if !errors.Is(resolveErr, ErrUnauthenticated) {
|
|
t.Errorf("Resolve() must collapse to ErrUnauthenticated")
|
|
}
|
|
if rejection.Reason != testCase.Expected.Reason {
|
|
t.Errorf("rejection reason = %q, want %q", rejection.Reason, testCase.Expected.Reason)
|
|
}
|
|
default:
|
|
t.Fatalf("unsupported fixture outcome %q", testCase.Expected.Outcome)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolverPropagatesLoaderError(t *testing.T) {
|
|
fixture := loadAuthorizationFixture(t)
|
|
databaseErr := errors.New("database unavailable")
|
|
loader := &recordingAuthorizationLoader{err: databaseErr}
|
|
resolver := NewResolver(loader, fixture.SessionSecret, fixture.RequiredClientID, func() time.Time {
|
|
return time.Unix(fixture.NowUnix, 0)
|
|
})
|
|
signed := signFixtureSession(t, fixture.Cases[0].Session, fixture.SessionSecret)
|
|
|
|
_, err := resolver.Resolve(context.Background(), signed)
|
|
if !errors.Is(err, databaseErr) {
|
|
t.Fatalf("Resolve() error = %v, want database error", err)
|
|
}
|
|
if errors.Is(err, ErrUnauthenticated) {
|
|
t.Fatal("database error must not collapse to unauthenticated")
|
|
}
|
|
if !reflect.DeepEqual(loader.ids, []string{fixture.Cases[0].Session.User.ID}) {
|
|
t.Fatalf("loader IDs = %#v", loader.ids)
|
|
}
|
|
}
|
|
|
|
func TestResolverRejectsInvalidCookieWithoutLoadingAuthorization(t *testing.T) {
|
|
fixture := loadAuthorizationFixture(t)
|
|
loader := &recordingAuthorizationLoader{}
|
|
resolver := NewResolver(loader, fixture.SessionSecret, fixture.RequiredClientID, func() time.Time {
|
|
return time.Unix(fixture.NowUnix, 0)
|
|
})
|
|
|
|
_, err := resolver.Resolve(context.Background(), "not-a-signed-session")
|
|
var rejection *UnauthenticatedError
|
|
if !errors.As(err, &rejection) || rejection.Reason != RejectionInvalidSession {
|
|
t.Fatalf("Resolve() error = %v, want invalid-session rejection", err)
|
|
}
|
|
if len(loader.ids) != 0 {
|
|
t.Fatalf("loader calls = %d, want 0", len(loader.ids))
|
|
}
|
|
}
|
|
|
|
func TestNewResolverDefaultsRequiredPlatformClient(t *testing.T) {
|
|
fixture := loadAuthorizationFixture(t)
|
|
loader := &recordingAuthorizationLoader{}
|
|
resolver := NewResolver(loader, fixture.SessionSecret, "", func() time.Time {
|
|
return time.Unix(fixture.NowUnix, 0)
|
|
})
|
|
signed := signFixtureSession(t, fixture.Cases[5].Session, fixture.SessionSecret)
|
|
|
|
_, err := resolver.Resolve(context.Background(), signed)
|
|
var rejection *UnauthenticatedError
|
|
if !errors.As(err, &rejection) || rejection.Reason != RejectionClientMismatch {
|
|
t.Fatalf("Resolve() error = %v, want client mismatch with default platform client", err)
|
|
}
|
|
if len(loader.ids) != 0 {
|
|
t.Fatalf("loader calls = %d, want 0", len(loader.ids))
|
|
}
|
|
}
|
|
|
|
func TestResolverFailsClosedWhenNotConfigured(t *testing.T) {
|
|
for _, resolver := range []*Resolver{
|
|
nil,
|
|
NewResolver(nil, "secret", "platform", nil),
|
|
NewResolver(&recordingAuthorizationLoader{}, "", "platform", nil),
|
|
} {
|
|
_, err := resolver.Resolve(context.Background(), "cookie")
|
|
if err == nil || errors.Is(err, ErrUnauthenticated) {
|
|
t.Fatalf("Resolve() error = %v, want configuration failure", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadAuthorizationFixture(t *testing.T) authorizationFixture {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("../../../contracts/auth/platform-session-authorization-v1.json")
|
|
if err != nil {
|
|
t.Fatalf("read authorization fixture: %v", err)
|
|
}
|
|
var fixture authorizationFixture
|
|
if err := json.Unmarshal(raw, &fixture); err != nil {
|
|
t.Fatalf("decode authorization fixture: %v", err)
|
|
}
|
|
return fixture
|
|
}
|
|
|
|
func signFixtureSession(t *testing.T, session Session, secret string) string {
|
|
t.Helper()
|
|
raw, err := json.Marshal(session)
|
|
if err != nil {
|
|
t.Fatalf("marshal fixture session: %v", err)
|
|
}
|
|
signed, err := Sign(raw, secret)
|
|
if err != nil {
|
|
t.Fatalf("sign fixture session: %v", err)
|
|
}
|
|
return signed
|
|
}
|