package domain_test import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/text/language" "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/repository/session" "github.com/zitadel/zitadel/internal/zerrors" ) func TestUserCheckCommand_Validate(t *testing.T) { t.Parallel() getErr := errors.New("get error") permissionErr := errors.New("permission denied") notFoundErr := database.NewNoRowFoundError(nil) tt := []struct { testName string userRepo func(ctrl *gomock.Controller) domain.UserRepository permissionChecker func(ctrl *gomock.Controller) domain.PermissionRepository cmd *domain.UserCheckCommand expectedError error }{ { testName: "when CheckUser is nil should return no error", cmd: &domain.UserCheckCommand{}, expectedError: nil, }, { testName: "when session ID is not set should return error", cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, }, expectedError: domain.ErrIDMissing(), }, { testName: "when instance ID is not set should return error", cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", }, expectedError: domain.ErrInstanceIDMissing(), }, { testName: "when permission check fails should return permission denied error", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(permissionErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: zerrors.ThrowPermissionDenied(permissionErr, "DOM-4qz3mt", "Errors.PermissionDenied"), }, { testName: "when search type is UserId should fetch user by ID", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(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.IDCondition("user-123"), ), )). Times(1). Return(&domain.User{ ID: "user-123", OrganizationID: "org-1", State: domain.UserStateActive, }, nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: nil, }, { testName: "when user not active should return precondition failed error", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(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.IDCondition("user-123"), ), )). Times(1). Return(&domain.User{ ID: "user-123", OrganizationID: "org-1", State: domain.UserStateInactive, }, nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: domain.ErrUserNotActive("user-123", ""), }, { testName: "when user retrieval fails should return error", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(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.IDCondition("user-123"), ), )). Times(1). Return(nil, getErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: domain.ErrInternal(getErr, ""), }, { testName: "when user not found should return not found error", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(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.IDCondition("user-123"), ), )). Times(1). Return(nil, notFoundErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: domain.ErrUserNotFound(notFoundErr, "user-123"), }, { testName: "when search type is LoginName should attempt to fetch by login name", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(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.LoginNameCondition(database.TextOperationEqual, "user@example.com"), ), )). Times(1). Return(&domain.User{ ID: "user-123", LoginNames: []domain.LoginName{{LoginName: "user@example.com"}}, OrganizationID: "org-1", State: domain.UserStateActive, }, nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{LoginName: "user@example.com"}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: nil, }, { testName: "when search type is unknown should return invalid argument error", permissionChecker: func(ctrl *gomock.Controller) domain.PermissionRepository { repo := domainmock.NewMockPermissionChecker(ctrl) repo.EXPECT(). CheckSessionPermission(gomock.Any(), domain.SessionWritePermission, "session-1"). Times(1). Return(nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{}, SessionID: "session-1", InstanceID: "instance-1", }, expectedError: domain.ErrInvalidRequest(""), }, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { t.Parallel() // Given ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "") ctrl := gomock.NewController(t) cmd := domain.NewUserCheckCommand(tc.cmd.SessionID, tc.cmd.InstanceID, tc.cmd.InputCheckUser) opts := &domain.InvokeOpts{ Invoker: domain.NewTransactionInvoker(nil), } domain.WithQueryExecutor(new(noopdb.Pool))(opts) if tc.userRepo != nil { domain.WithUserRepo(tc.userRepo(ctrl))(opts) } if tc.permissionChecker != nil { domain.WithPermissionChecker(tc.permissionChecker(ctrl))(opts) } // Test err := cmd.Validate(ctx, opts) // Verify assert.ErrorIs(t, err, tc.expectedError) }) } } func TestUserCheckCommand_Execute(t *testing.T) { t.Parallel() sessionNotFoundErr := database.NewNoRowFoundError(nil) getErr := errors.New("get error") updateErr := errors.New("update error") tt := []struct { testName string sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository cmd *domain.UserCheckCommand expectedError error expectedPreferredLanguage *language.Tag }{ { testName: "when CheckUser is nil should return no error", cmd: &domain.UserCheckCommand{}, expectedError: nil, }, { testName: "when session retrieval fails should return error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(nil, getErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, expectedError: domain.ErrInternal(getErr, ""), }, { testName: "when session not found should return not found error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(nil, sessionNotFoundErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", Human: &domain.HumanUser{ PreferredLanguage: language.Italian, }, }, }, expectedError: domain.ErrSessionNotFound(sessionNotFoundErr, "session-1"), }, { testName: "when user change is attempted should return invalid argument error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "user-456", }, nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, expectedError: domain.ErrSessionUserChange(), }, { testName: "when session update fails should return error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "user-123", }, nil) repo.EXPECT(). Update(gomock.Any(), gomock.Any(), repo.IDCondition("session-1"), repo.SetFactor(&domain.SessionFactorUser{ UserID: "user-123", LastVerifiedAt: time.Now(), })). Times(1). Return(int64(0), updateErr) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, expectedError: domain.ErrInternal(updateErr, "failed to update the session"), }, { testName: "when session update returns no rows should return not found error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "", }, nil) repo.EXPECT(). Update(gomock.Any(), gomock.Any(), repo.IDCondition("session-1"), repo.SetFactor(&domain.SessionFactorUser{ UserID: "user-123", LastVerifiedAt: time.Now(), })). Times(1). Return(int64(0), nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, expectedError: domain.ErrSessionNotFound(nil, "session-1"), }, { testName: "when session update returns multiple rows should return internal error", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "", }, nil) repo.EXPECT(). Update(gomock.Any(), gomock.Any(), repo.IDCondition("session-1"), repo.SetFactor(&domain.SessionFactorUser{ UserID: "user-123", LastVerifiedAt: time.Now(), })). Times(1). Return(int64(2), nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, expectedError: domain.ErrMoreThanOneRowAffected("unexpected number of rows updated", 2), }, { testName: "when session has no user and user is set should execute successfully", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "", }, nil) repo.EXPECT(). Update(gomock.Any(), gomock.Any(), repo.IDCondition("session-1"), repo.SetFactor(&domain.SessionFactorUser{ UserID: "user-123", LastVerifiedAt: time.Now(), })). Times(1). Return(int64(1), nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", Human: &domain.HumanUser{ PreferredLanguage: language.Albanian, }, }, }, expectedPreferredLanguage: &language.Albanian, }, { testName: "when session user matches fetched user should execute successfully", sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository { repo := domainmock.NewSessionRepo(ctrl) repo.EXPECT(). Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions( database.WithCondition( repo.IDCondition("session-1"), ), )). Times(1). Return(&domain.Session{ ID: "session-1", UserID: "user-123", }, nil) repo.EXPECT(). Update(gomock.Any(), gomock.Any(), repo.IDCondition("session-1"), repo.SetFactor(&domain.SessionFactorUser{ UserID: "user-123", LastVerifiedAt: time.Now(), })). Times(1). Return(int64(1), nil) return repo }, cmd: &domain.UserCheckCommand{ InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ ID: "user-123", OrganizationID: "org-1", }, }, }, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { t.Parallel() // Given ctx := authz.NewMockContext("instance-1", "", "") ctrl := gomock.NewController(t) opts := &domain.InvokeOpts{ Invoker: domain.NewTransactionInvoker(nil), } domain.WithQueryExecutor(new(noopdb.Pool))(opts) if tc.sessionRepo != nil { domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts) } // Test err := tc.cmd.Execute(ctx, opts) // Verify assert.ErrorIs(t, err, tc.expectedError) if tc.expectedError == nil && tc.cmd.InputCheckUser != nil { assert.NotZero(t, tc.cmd.UserCheckedAt) assert.Equal(t, tc.expectedPreferredLanguage, tc.cmd.PreferredUserLanguage) } }) } } func TestUserCheckCommand_Events(t *testing.T) { t.Parallel() tt := []struct { testName string cmd *domain.UserCheckCommand expectedEventLen int expectedError error }{ { testName: "when CheckUser is nil should return nil", cmd: &domain.UserCheckCommand{}, expectedEventLen: 0, expectedError: nil, }, { testName: "when CheckUser is set should return user checked event", cmd: &domain.UserCheckCommand{ SessionID: "session-1", InstanceID: "instance-1", FetchedUser: domain.User{ID: "user-123", OrganizationID: "org-1"}, UserCheckedAt: time.Now(), InputCheckUser: &domain.CheckUserType{UserID: "user-123"}, }, expectedEventLen: 1, expectedError: nil, }, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { t.Parallel() // Given ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "") opts := &domain.InvokeOpts{ Invoker: domain.NewTransactionInvoker(nil), } // Test events, err := tc.cmd.Events(ctx, opts) // Verify assert.Equal(t, tc.expectedError, err) require.Len(t, events, tc.expectedEventLen) if tc.expectedEventLen != 0 { usrCheckedEvent, ok := events[0].(*session.UserCheckedEvent) require.True(t, ok) assert.Equal(t, tc.cmd.FetchedUser.ID, usrCheckedEvent.UserID) assert.Equal(t, tc.cmd.FetchedUser.OrganizationID, usrCheckedEvent.UserResourceOwner) assert.NotZero(t, usrCheckedEvent.CheckedAt) } }) } } func TestNewUserCheckCommand(t *testing.T) { t.Parallel() tt := []struct { testName string sessionID string instanceID string checkUser *domain.CheckUserType expected *domain.UserCheckCommand }{ { testName: "should create command with provided sessionID, instanceID and checkUserType", sessionID: "session-1", instanceID: "instance-1", checkUser: &domain.CheckUserType{}, expected: &domain.UserCheckCommand{ SessionID: "session-1", InstanceID: "instance-1", InputCheckUser: &domain.CheckUserType{}, }, }, { testName: "should create command with empty sessionID", sessionID: "", instanceID: "instance-1", checkUser: &domain.CheckUserType{}, expected: &domain.UserCheckCommand{ SessionID: "", InstanceID: "instance-1", InputCheckUser: &domain.CheckUserType{}, }, }, { testName: "should create command with empty instanceID", sessionID: "session-1", instanceID: "", checkUser: &domain.CheckUserType{}, expected: &domain.UserCheckCommand{ SessionID: "session-1", InstanceID: "", InputCheckUser: &domain.CheckUserType{}, }, }, { testName: "should create command with nil checkUserType", sessionID: "session-1", instanceID: "instance-1", expected: &domain.UserCheckCommand{ SessionID: "session-1", InstanceID: "instance-1", }, }, { testName: "should create command with all empty", sessionID: "", instanceID: "", expected: &domain.UserCheckCommand{ SessionID: "", InstanceID: "", }, }, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { t.Parallel() // Test cmd := domain.NewUserCheckCommand(tc.sessionID, tc.instanceID, tc.checkUser) // Verify assert.Equal(t, tc.expected.SessionID, cmd.SessionID) assert.Equal(t, tc.expected.InstanceID, cmd.InstanceID) assert.Equal(t, tc.expected.InputCheckUser, cmd.InputCheckUser) assert.Zero(t, cmd.UserCheckedAt) assert.Nil(t, cmd.PreferredUserLanguage) assert.Zero(t, cmd.FetchedUser) }) } }