Files
zitadel/backend/v3/domain/session_check_idp_intent_test.go
T
Marco A.andGitHub aa4432dc99 feat(session): IDP Intent Check API with relation tables (#11820)
# Which Problems Are Solved

As part of #11035 , this PR implements the IDP intent check logic for
session validation

# How the Problems Are Solved

- Inject encryption algorithm for IDP intent tokens at DB initialization
  - Add the IDP intent repository as an invoke option
  - Rework crypto mocks using gomock
  - Implement IDP intent check logic
- minor - DB: expand IDP Intent deletion testing table with a test case
where IDP intent is not found

# Noteworthy

I did not implement `domain.Transactional` given the doubts that
@adlerhurst expressed in https://github.com/zitadel/zitadel/pull/11804 .
So, there is no transaction AT ALL in here because the `Execute()`
method contains one single DB call (Delete) and `Validate()` just does
Get calls.

# Additional Context

This is a cherry-picked PR + minor changes, coming from
https://github.com/zitadel/zitadel/pull/11164
- Relates to #11035
- Depends on https://github.com/zitadel/zitadel/pull/11804
2026-03-17 10:50:47 +00:00

822 lines
32 KiB
Go

package domain_test
import (
"context"
"encoding/base64"
"errors"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestIDPIntentCheckCommand_Events(t *testing.T) {
t.Parallel()
tt := []struct {
name string
command *domain.IDPIntentCheckCommand
expectedEvents []eventstore.Command
}{
{
name: "nil CheckIntent returns nil events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: nil,
IsCheckComplete: true,
},
},
{
name: "isCheckComplete false returns nil events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{},
},
},
{
name: "valid command returns events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123"},
SessionID: "session-456",
InstanceID: "instance-789",
IsCheckComplete: true,
},
expectedEvents: []eventstore.Command{
session.NewIntentCheckedEvent(t.Context(), &session.NewAggregate("session-456", "instance-789").Aggregate, time.Now()),
idpintent.NewConsumedEvent(t.Context(), &idpintent.NewAggregate("intent-123", "").Aggregate),
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Test
events, err := tc.command.Events(context.Background(), &domain.InvokeOpts{})
// Verify
assert.NoError(t, err)
require.Len(t, events, len(tc.expectedEvents))
for i, expectedType := range tc.expectedEvents {
assert.IsType(t, expectedType, events[i])
switch expectedAssertedType := expectedType.(type) {
case *session.IntentCheckedEvent:
actualAssertedType, ok := events[i].(*session.IntentCheckedEvent)
require.True(t, ok)
assert.InDelta(t, expectedAssertedType.CheckedAt.UnixMilli(), actualAssertedType.CheckedAt.UnixMilli(), 10)
case *idpintent.ConsumedEvent:
_, ok := events[i].(*idpintent.ConsumedEvent)
require.True(t, ok)
}
}
})
}
}
func TestIDPIntentCheckCommand_Validate(t *testing.T) {
t.Parallel()
getErr := errors.New("get error")
notFoundErr := database.NewNoRowFoundError(nil)
decryptionError := errors.New("decryption error")
invalidTokenEncoding := base64.RawURLEncoding.EncodeToString([]byte("invalid-token"))
validTokenEncoding := base64.RawURLEncoding.EncodeToString([]byte("token-123"))
tt := []struct {
testName string
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
idpIntentRepo func(ctrl *gomock.Controller) domain.IDPIntentRepository
userRepo func(ctrl *gomock.Controller) domain.UserRepository
encryptionAlgorithm func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm
cmd *domain.IDPIntentCheckCommand
expectedError error
expectedUser domain.User
}{
{
testName: "when CheckIntent is nil should return no error",
cmd: &domain.IDPIntentCheckCommand{},
expectedError: nil,
},
{
testName: "when session ID is not set should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "",
InstanceID: "instance-1",
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-5Y8pb4", "Errors.Missing.SessionID"),
},
{
testName: "when instance ID is not set should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "",
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-Q4YFIq", "Errors.Missing.InstanceID"),
},
{
testName: "when retrieving session fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-EhIgey", "failed fetching session"),
},
{
testName: "when session not found should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-EhIgey", "session not found"),
},
{
testName: "when session userID is empty should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: ""}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-IJcVkV", "Errors.User.UserIDMissing"),
},
{
testName: "when token verification fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: invalidTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("", decryptionError)
return cryptoAlg
},
expectedError: zerrors.ThrowPermissionDenied(decryptionError, "CRYPTO-Sf4gt", "Errors.Intent.InvalidToken"),
},
{
testName: "when retrieving intent fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-5XkWJV", "failed fetching intent"),
},
{
testName: "when intent not found should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-5XkWJV", "intent not found"),
},
{
testName: "when intent state is not succeeded should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateFailed, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour))}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-0UKHku", "Errors.Intent.NotSucceeded"),
},
{
testName: "when intent is expired should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(-time.Hour))}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired"),
},
{
testName: "when intent expiration is nil should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: nil}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired"),
},
{
testName: "when retrieving user fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-Vnx2G9", "failed fetching user"),
},
{
testName: "when user is not found should return not found error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-Vnx2G9", "user not found"),
},
{
testName: "when user is not human should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{ID: "user-1", State: domain.UserStateActive, Human: nil}, nil)
return repo
},
expectedError: zerrors.ThrowInternal(nil, "DOM-FkX5lZ", "user not human"),
},
{
testName: "when intent has user and it matches session user should return no error and set fetched user",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-123"}}},
}, nil)
return repo
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "user-1"}, nil)
return repo
},
expectedError: nil,
expectedUser: domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
},
},
{
testName: "when intent has user but it does not match session user should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-123"}}},
}, nil)
return repo
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "user-2"}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-FLdnLH", "Errors.Intent.OtherUser"),
},
{
testName: "when user has no matching IDP link should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "", IDPID: "idp-123"}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-456"}}},
}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-XuNkt7", "Errors.Intent.OtherUser"),
},
{
testName: "when all validations pass should return no error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.EncryptionAlgorithm {
cryptoAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
EncryptionKeyID().
Times(1)
cryptoAlg.EXPECT().
DecryptString(gomock.Any(), gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "", IDPID: "idp-123"}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
}, nil)
return repo
},
expectedError: nil,
expectedUser: domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.sessionRepo != nil {
domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts)
}
if tc.userRepo != nil {
domain.WithUserRepo(tc.userRepo(ctrl))(opts)
}
if tc.idpIntentRepo != nil {
domain.WithIDPIntentRepo(tc.idpIntentRepo(ctrl))(opts)
}
if tc.encryptionAlgorithm != nil {
tc.cmd.EncAlgo = tc.encryptionAlgorithm(ctrl)
}
// Test
err := tc.cmd.Validate(ctx, opts)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
assert.Equal(t, tc.expectedUser, tc.cmd.FetchedUser)
})
}
}
func TestIDPIntentCheckCommand_Execute(t *testing.T) {
t.Parallel()
deleteErr := errors.New("delete error")
tt := []struct {
testName string
idpIntentRepo func(ctrl *gomock.Controller) domain.IDPIntentRepository
cmd *domain.IDPIntentCheckCommand
expectedError error
expectComplete bool
}{
{
testName: "when CheckIntent is nil should return no error and not mark complete",
cmd: &domain.IDPIntentCheckCommand{},
expectedError: nil,
expectComplete: false,
},
{
testName: "when intent deletion fails should return error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(0), deleteErr)
return repo
},
expectedError: zerrors.ThrowInternal(deleteErr, "DOM-j1s5Eu", "failed deleting IDP intent"),
expectComplete: false,
},
{
testName: "when intent deletion returns unexpected row count should return error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(0), nil)
return repo
},
expectedError: zerrors.ThrowInternal(domain.NewRowsReturnedMismatchError(1, 0), "DOM-3CBpdB", "unexpected number of rows deleted"),
expectComplete: false,
},
{
testName: "when intent is successfully deleted should mark complete",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(1), nil)
return repo
},
expectedError: nil,
expectComplete: true,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// t.Parallel()
// Given
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.idpIntentRepo != nil {
domain.WithIDPIntentRepo(tc.idpIntentRepo(ctrl))(opts)
}
// Test
err := tc.cmd.Execute(ctx, opts)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
assert.Equal(t, tc.expectComplete, tc.cmd.IsCheckComplete)
})
}
}