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
This commit is contained in:
Marco A.
2026-03-17 10:50:47 +00:00
committed by GitHub
parent 3fdac54878
commit aa4432dc99
16 changed files with 1422 additions and 157 deletions
@@ -0,0 +1,17 @@
package convert
import (
"github.com/zitadel/zitadel/backend/v3/domain"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func CheckIDPIntentGRPCToDomain(checkIDPIntent *session_grpc.CheckIDPIntent) *domain.CheckIDPIntentType {
if checkIDPIntent == nil {
return nil
}
return &domain.CheckIDPIntentType{
ID: checkIDPIntent.GetIdpIntentId(),
Token: checkIDPIntent.GetIdpIntentToken(),
}
}
@@ -0,0 +1,55 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/backend/v3/domain"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func TestCheckIDPIntentGRPCToDomain(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
input *session_grpc.CheckIDPIntent
expected *domain.CheckIDPIntentType
}{
{
testName: "nil input",
input: nil,
expected: nil,
},
{
testName: "valid input",
input: &session_grpc.CheckIDPIntent{
IdpIntentId: "intent-123",
IdpIntentToken: "token-abc",
},
expected: &domain.CheckIDPIntentType{
ID: "intent-123",
Token: "token-abc",
},
},
{
testName: "empty strings",
input: &session_grpc.CheckIDPIntent{
IdpIntentId: "",
IdpIntentToken: "",
},
expected: &domain.CheckIDPIntentType{
ID: "",
Token: "",
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
result := CheckIDPIntentGRPCToDomain(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
+11 -4
View File
@@ -8,10 +8,13 @@ import (
)
var (
pool database.Pool
legacyEventstore eventstore.LegacyEventstore
sysConfig systemdefaults.SystemDefaults
passwordHasher *crypto.Hasher
pool database.Pool
// tracer tracing.Tracer = tracing.GlobalTracer()
// runtimeLogger *slog.Logger = logging.New(logging.StreamRuntime, nil)
legacyEventstore eventstore.LegacyEventstore
sysConfig systemdefaults.SystemDefaults
passwordHasher *crypto.Hasher
idpEncryptionAlgo crypto.EncryptionAlgorithm
)
func SetPool(p database.Pool) {
@@ -29,3 +32,7 @@ func SetSystemConfig(cfg systemdefaults.SystemDefaults) {
func SetPasswordHasher(hasher *crypto.Hasher) {
passwordHasher = hasher
}
func SetIDPEncryptionAlgorithm(idpEncryptionAlg crypto.EncryptionAlgorithm) {
idpEncryptionAlgo = idpEncryptionAlg
}
+23
View File
@@ -1,7 +1,11 @@
package domain
import (
"errors"
"fmt"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/zerrors"
)
type wrongIDPTypeError struct {
@@ -89,6 +93,25 @@ func (e *PasswordVerificationError) Error() string {
return fmt.Sprintf("Message=failed password attempts (%d)", e.failedAttempts)
}
// handleGetError wraps DB errors coming from Get calls into [zerrors] errors.
//
// - errorID should be in the format <package short name>-<random id>
// - objectType should be the string representation of a DB object (e.g. 'user', 'session', 'idp'...)
//
// The function wraps [database.NoRowFoundError] to [zerrors.NotFound] error
// and any other error to [zerrors.InternalError]
func handleGetError(inputErr error, errorID, objectType string) error {
if inputErr == nil {
return nil
}
if errors.Is(inputErr, &database.NoRowFoundError{}) {
return zerrors.CreateZitadelError(zerrors.KindNotFound, inputErr, errorID, fmt.Sprintf("%s not found", objectType), 1)
}
return zerrors.CreateZitadelError(zerrors.KindInternal, inputErr, errorID, fmt.Sprintf("failed fetching %s", objectType), 1)
}
func (err *PasswordVerificationError) Is(target error) bool {
_, ok := target.(*PasswordVerificationError)
return ok
+7
View File
@@ -63,6 +63,12 @@ func WithPermissionChecker(checker PermissionChecker) InvokeOpt {
}
}
func WithIDPIntentRepo(repo IDPIntentRepository) InvokeOpt {
return func(opts *InvokeOpts) {
opts.idpIntentRepo = repo
}
}
// WithQueryExecutor sets the database client to be used by the command.
// If not set, the default pool will be used.
// This is mainly used for testing.
@@ -99,6 +105,7 @@ type InvokeOpts struct {
instanceDomainRepo InstanceDomainRepository
sessionRepo SessionRepository
userRepo UserRepository
idpIntentRepo IDPIntentRepository
// Settings repos
lockoutSettingRepo LockoutSettingsRepository
+43
View File
@@ -0,0 +1,43 @@
package domainmock
import (
context "context"
gomock "go.uber.org/mock/gomock"
domain "github.com/zitadel/zitadel/backend/v3/domain"
database "github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
)
func NewIDPIntentRepo(ctrl *gomock.Controller) *IDPIntentRepo {
return &IDPIntentRepo{
mock: NewMockIDPIntentRepository(ctrl),
IDPIntentRepository: repository.IDPIntentRepository(),
}
}
type IDPIntentRepo struct {
domain.IDPIntentRepository
mock *MockIDPIntentRepository
}
func (u *IDPIntentRepo) EXPECT() *MockIDPIntentRepositoryMockRecorder {
return u.mock.EXPECT()
}
func (i *IDPIntentRepo) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.IDPIntent, error) {
return i.mock.Get(ctx, client, opts...)
}
func (i *IDPIntentRepo) Create(ctx context.Context, client database.QueryExecutor, intent *domain.IDPIntent) error {
return i.mock.Create(ctx, client, intent)
}
func (i *IDPIntentRepo) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
return i.mock.Update(ctx, client, condition, changes...)
}
func (i *IDPIntentRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
return i.mock.Delete(ctx, client, condition)
}
@@ -0,0 +1,161 @@
package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"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"
)
type CheckIDPIntentType struct {
ID string
Token string
}
type IDPIntentCheckCommand struct {
CheckIntent *CheckIDPIntentType
SessionID string
InstanceID string
EncAlgo crypto.EncryptionAlgorithm
FetchedUser User
IsCheckComplete bool
}
// NewIDPIntentCheckCommand returns an IDPIntentCheckCommand initialized with the input values.
//
// If encryptionAlgo is nil, the default [crypto.EncryptionAlgorithm] will be used
func NewIDPIntentCheckCommand(request *CheckIDPIntentType, sessionID, instanceID string, encryptionAlgo crypto.EncryptionAlgorithm) *IDPIntentCheckCommand {
idpCheckCommand := &IDPIntentCheckCommand{
CheckIntent: request,
SessionID: sessionID,
InstanceID: instanceID,
EncAlgo: idpEncryptionAlgo,
}
if encryptionAlgo != nil {
idpCheckCommand.EncAlgo = encryptionAlgo
}
return idpCheckCommand
}
// Events implements [Commander].
func (i *IDPIntentCheckCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
if i.CheckIntent == nil || !i.IsCheckComplete {
return nil, nil
}
return []eventstore.Command{
session.NewIntentCheckedEvent(ctx, &session.NewAggregate(i.SessionID, i.InstanceID).Aggregate, time.Now()),
idpintent.NewConsumedEvent(ctx, &idpintent.NewAggregate(i.CheckIntent.ID, "").Aggregate),
}, nil
}
// Execute implements [Commander].
func (i *IDPIntentCheckCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
if i.CheckIntent == nil {
return nil
}
intentRepo := opts.idpIntentRepo
removedRows, err := intentRepo.Delete(ctx, opts.DB(), intentRepo.PrimaryKeyCondition(i.InstanceID, i.CheckIntent.ID))
if err != nil {
return zerrors.ThrowInternal(err, "DOM-j1s5Eu", "failed deleting IDP intent")
}
if removedRows != 1 {
return zerrors.ThrowInternal(NewRowsReturnedMismatchError(1, removedRows), "DOM-3CBpdB", "unexpected number of rows deleted")
}
i.IsCheckComplete = true
return nil
}
// String implements [Commander].
func (i *IDPIntentCheckCommand) String() string {
return "IDPIntentCheckCommand"
}
// Validate implements [Commander].
func (i *IDPIntentCheckCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
if i.CheckIntent == nil {
return nil
}
if i.SessionID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-5Y8pb4", "Errors.Missing.SessionID")
}
if i.InstanceID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-Q4YFIq", "Errors.Missing.InstanceID")
}
sessionRepo := opts.sessionRepo
idpIntentRepo := opts.idpIntentRepo
userRepo := opts.userRepo
session, err := sessionRepo.Get(ctx, opts.DB(), database.WithCondition(sessionRepo.PrimaryKeyCondition(i.InstanceID, i.SessionID)))
if err = handleGetError(err, "DOM-EhIgey", "session"); err != nil {
return err
}
if session.UserID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-IJcVkV", "Errors.User.UserIDMissing")
}
if err := crypto.CheckToken(i.EncAlgo, i.CheckIntent.Token, i.CheckIntent.ID); err != nil {
return err
}
intent, err := idpIntentRepo.Get(ctx, opts.DB(), database.WithCondition(idpIntentRepo.PrimaryKeyCondition(i.InstanceID, i.CheckIntent.ID)))
if err = handleGetError(err, "DOM-5XkWJV", "intent"); err != nil {
return err
}
if intent.State != IDPIntentStateSucceeded {
return zerrors.ThrowPreconditionFailed(nil, "DOM-0UKHku", "Errors.Intent.NotSucceeded")
}
if intent.ExpiresAt == nil || intent.ExpiresAt.Before(time.Now()) {
return zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired")
}
user, err := userRepo.Get(ctx, opts.DB(), database.WithCondition(userRepo.PrimaryKeyCondition(i.InstanceID, session.UserID)))
if err = handleGetError(err, "DOM-Vnx2G9", "user"); err != nil {
return err
}
if user.Human == nil {
return zerrors.ThrowInternal(nil, "DOM-FkX5lZ", "user not human")
}
if intentUsr := intent.UserID; intentUsr != "" {
if intentUsr != session.UserID {
return zerrors.ThrowPreconditionFailed(nil, "DOM-FLdnLH", "Errors.Intent.OtherUser")
}
i.FetchedUser = *user
return nil
}
var matchingLink *IdentityProviderLink
for _, idpLink := range user.Human.IdentityProviderLinks {
if idpLink.ProviderID == intent.IDPID {
matchingLink = idpLink
break
}
}
if matchingLink == nil {
return zerrors.ThrowPreconditionFailed(nil, "DOM-XuNkt7", "Errors.Intent.OtherUser")
}
i.FetchedUser = *user
return nil
}
var _ Commander = (*IDPIntentCheckCommand)(nil)
@@ -0,0 +1,821 @@
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)
})
}
}
@@ -570,6 +570,11 @@ func TestDeleteIDPIntent(t *testing.T) {
),
expectedDeletedCount: 2,
},
{
testName: "when condition matches no record should not delete anything",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, "not found"),
expectedDeletedCount: 0,
},
}
for _, tc := range tt {