package backchannellogout import ( "slices" "strings" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go-micro.dev/v4/store" "github.com/qsfera/server/services/proxy/pkg/staticroutes/internal/backchannellogout/mocks" ) func mustNewKey(t *testing.T, subject, session string) string { key, err := NewKey(subject, session) require.NoError(t, err) return key } func mustNewSuSe(t *testing.T, subject, session string) SuSe { suse, err := NewSuSe(mustNewKey(t, subject, session)) require.NoError(t, err) return suse } func TestNewKey(t *testing.T) { tests := []struct { name string subject string session string wantKey string wantErr error }{ { name: "key variation: 'subject.session'", subject: "subject", session: "session", wantKey: "c3ViamVjdA==.c2Vzc2lvbg==", }, { name: "key variation: 'subject.'", subject: "subject", wantKey: "c3ViamVjdA==.", }, { name: "key variation: '.session'", session: "session", wantKey: ".c2Vzc2lvbg==", }, { name: "key variation: '.'", wantErr: ErrInvalidKey, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { key, err := NewKey(tt.subject, tt.session) require.ErrorIs(t, err, tt.wantErr) require.Equal(t, tt.wantKey, key) }) } } func TestNewSuSe(t *testing.T) { tests := []struct { name string key string wantSubject string wantSession string wantMode LogoutMode wantErr error }{ { name: "key variation: '.session'", key: mustNewKey(t, "", "session"), wantSession: "session", wantMode: LogoutModeSession, }, { name: "key variation: 'session'", key: mustNewKey(t, "", "session"), wantSession: "session", wantMode: LogoutModeSession, }, { name: "key variation: 'subject.'", key: mustNewKey(t, "subject", ""), wantSubject: "subject", wantMode: LogoutModeSubject, }, { name: "key variation: 'subject.session'", key: mustNewKey(t, "subject", "session"), wantSubject: "subject", wantSession: "session", wantMode: LogoutModeSession, }, { name: "key variation: 'dot'", key: ".", wantErr: ErrInvalidSubjectOrSession, }, { name: "key variation: 'empty'", key: "", wantErr: ErrInvalidSubjectOrSession, }, { name: "key variation: string('subject.session')", key: "subject.session", wantErr: ErrInvalidSubjectOrSession, wantMode: LogoutModeSession, }, { name: "key variation: string('subject.')", key: "subject.", wantErr: ErrInvalidSubjectOrSession, wantMode: LogoutModeSubject, }, { name: "key variation: string('.session')", key: ".session", wantErr: ErrInvalidSubjectOrSession, wantMode: LogoutModeSession, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { suSe, err := NewSuSe(tt.key) require.ErrorIs(t, err, tt.wantErr) mode := suSe.Mode() require.Equal(t, tt.wantMode, mode) subject, _ := suSe.Subject() require.Equal(t, tt.wantSubject, subject) session, _ := suSe.Session() require.Equal(t, tt.wantSession, session) }) } } func TestGetLogoutRecords(t *testing.T) { sessionStore := store.NewMemoryStore() recordClaimA := &store.Record{Key: "claim-a", Value: []byte("claim-a-data")} recordClaimB := &store.Record{Key: "claim-b", Value: []byte("claim-b-data")} recordClaimC := &store.Record{Key: "claim-c", Value: []byte("claim-c-data")} recordClaimD := &store.Record{Key: "claim-d", Value: []byte("claim-d-data")} recordSessionA := &store.Record{Key: mustNewKey(t, "", "session-a"), Value: []byte(recordClaimA.Key)} recordSessionB := &store.Record{Key: mustNewKey(t, "", "session-b"), Value: []byte(recordClaimB.Key)} recordSubjectASessionC := &store.Record{Key: mustNewKey(t, "subject-a", "session-c"), Value: []byte(recordSessionA.Key)} recordSubjectASessionD := &store.Record{Key: mustNewKey(t, "subject-a", "session-d"), Value: []byte(recordSessionA.Key)} for _, r := range []*store.Record{ recordClaimA, recordClaimB, recordClaimC, recordClaimD, recordSessionA, recordSessionB, recordSubjectASessionC, recordSubjectASessionD, } { require.NoError(t, sessionStore.Write(r)) } tests := []struct { name string suSe SuSe store func(t *testing.T) store.Store wantRecords []*store.Record wantErrs []error }{ { name: "fails if multiple session records are found", suSe: mustNewSuSe(t, "", "session-a"), store: func(t *testing.T) store.Store { s := mocks.NewStore(t) s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{ recordSessionA, recordSessionB, }, nil) return s }, wantRecords: []*store.Record{}, wantErrs: []error{ErrSuspiciousCacheResult}}, { name: "fails if the record key is not ok", suSe: mustNewSuSe(t, "", "session-a"), store: func(t *testing.T) store.Store { s := mocks.NewStore(t) s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{ {Key: "invalid.record.key"}, }, nil) return s }, wantRecords: []*store.Record{}, wantErrs: []error{ErrInvalidSubjectOrSession, ErrSuspiciousCacheResult}, }, { name: "fails if the session does not match the retrieved record", suSe: mustNewSuSe(t, "", "session-a"), store: func(t *testing.T) store.Store { s := mocks.NewStore(t) s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{ recordSessionB, }, nil) return s }, wantRecords: []*store.Record{}, wantErrs: []error{ErrSuspiciousCacheResult}}, { name: "fails if the subject does not match the retrieved record", suSe: mustNewSuSe(t, "subject-a", ""), store: func(t *testing.T) store.Store { s := mocks.NewStore(t) s.EXPECT().Read(mock.Anything, mock.Anything).Return([]*store.Record{ recordSessionB, }, nil) return s }, wantRecords: []*store.Record{}, wantErrs: []error{ErrSuspiciousCacheResult}}, // key variation tests { name: "key variation: 'session-a'", suSe: mustNewSuSe(t, "", "session-a"), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{recordSessionA}, }, { name: "key variation: 'session-b'", suSe: mustNewSuSe(t, "", "session-b"), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{recordSessionB}, }, { name: "key variation: 'session-c'", suSe: mustNewSuSe(t, "", "session-c"), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{recordSubjectASessionC}, }, { name: "key variation: 'ession-c'", suSe: mustNewSuSe(t, "", "ession-c"), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{}, wantErrs: []error{store.ErrNotFound}, }, { name: "key variation: 'subject-a'", suSe: mustNewSuSe(t, "subject-a", ""), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{recordSubjectASessionC, recordSubjectASessionD}, }, { name: "key variation: 'subject-'", suSe: mustNewSuSe(t, "subject-", ""), store: func(*testing.T) store.Store { return sessionStore }, wantRecords: []*store.Record{}, wantErrs: []error{store.ErrNotFound}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { records, err := GetLogoutRecords(tt.suSe, tt.store(t)) for _, wantErr := range tt.wantErrs { require.ErrorIs(t, err, wantErr) } require.Len(t, records, len(tt.wantRecords)) sortRecords := func(r []*store.Record) []*store.Record { slices.SortFunc(r, func(a, b *store.Record) int { return strings.Compare(a.Key, b.Key) }) return r } records = sortRecords(records) for i, wantRecords := range sortRecords(tt.wantRecords) { require.True(t, len(records) >= i+1) require.Equal(t, wantRecords.Key, records[i].Key) require.Equal(t, wantRecords.Value, records[i].Value) } }) } }