mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat(session): User Check API with relation tables (#11777)
# Which Problems Are Solved As part of #11035 , this PR implements the user check logic for session validation # How the Problems Are Solved - Add user and session repositories as invoke options, as well as their mocks to prepare the ground for testing - Implement the logic for the UserCheck command, following what has been done on eventstore side - Add a session permission chek interface to be used during the `Validate()` part of the Commander # Additional Context This is a cherry-picked PR + minor changes, coming from https://github.com/zitadel/zitadel/pull/11164 - Relates to #11035 --------- Co-authored-by: Silvan <[email protected]>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
func CheckUserGRPCToQueryOpt(checkUser *session_grpc.CheckUser) (*domain.CheckUserType, error) {
|
||||
var toReturn *domain.CheckUserType
|
||||
if checkUser == nil {
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
switch searchType := checkUser.GetSearch().(type) {
|
||||
case *session_grpc.CheckUser_UserId:
|
||||
toReturn = &domain.CheckUserType{UserID: searchType.UserId}
|
||||
case *session_grpc.CheckUser_LoginName:
|
||||
toReturn = &domain.CheckUserType{LoginName: searchType.LoginName}
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "CONV-7B2m0b", "user search %T not implemented", searchType)
|
||||
}
|
||||
|
||||
return toReturn, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
func TestCheckUserGRPCToQueryOpt(t *testing.T) {
|
||||
t.Parallel()
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputCheckUser *session_grpc.CheckUser
|
||||
expectedError error
|
||||
expectedDomainCheckUser *domain.CheckUserType
|
||||
}{
|
||||
{
|
||||
testName: "when input checkUser is nil should return nil, nil",
|
||||
},
|
||||
{
|
||||
testName: "when input checkUser query is not matching type should return invalid argument error",
|
||||
inputCheckUser: &session_grpc.CheckUser{},
|
||||
expectedError: zerrors.ThrowInvalidArgumentf(nil, "CONV-7B2m0b", "user search %T not implemented", nil),
|
||||
},
|
||||
{
|
||||
testName: "when input checkUser query is UserID should set user id on domain CheckUserType",
|
||||
inputCheckUser: &session_grpc.CheckUser{
|
||||
Search: &session_grpc.CheckUser_UserId{UserId: "user-123"},
|
||||
},
|
||||
expectedDomainCheckUser: &domain.CheckUserType{UserID: "user-123"},
|
||||
},
|
||||
{
|
||||
testName: "when input checkUser query is LoginName should set user id on domain CheckUserType",
|
||||
inputCheckUser: &session_grpc.CheckUser{
|
||||
Search: &session_grpc.CheckUser_LoginName{LoginName: "[email protected]"},
|
||||
},
|
||||
expectedDomainCheckUser: &domain.CheckUserType{LoginName: "[email protected]"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test
|
||||
res, err := CheckUserGRPCToQueryOpt(tc.inputCheckUser)
|
||||
|
||||
// Verify
|
||||
assert.ErrorIs(t, err, tc.expectedError)
|
||||
assert.Equal(t, tc.expectedDomainCheckUser, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,24 @@ func WithInstanceDomainRepo(repo InstanceDomainRepository) InvokeOpt {
|
||||
}
|
||||
}
|
||||
|
||||
func WithSessionRepo(repo SessionRepository) InvokeOpt {
|
||||
return func(opts *InvokeOpts) {
|
||||
opts.sessionRepo = repo
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserRepo(repo UserRepository) InvokeOpt {
|
||||
return func(opts *InvokeOpts) {
|
||||
opts.userRepo = repo
|
||||
}
|
||||
}
|
||||
|
||||
func WithPermissionChecker(checker PermissionChecker) InvokeOpt {
|
||||
return func(opts *InvokeOpts) {
|
||||
opts.Permissions = checker
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -73,6 +91,8 @@ type InvokeOpts struct {
|
||||
projectRepo ProjectRepository
|
||||
instanceRepo InstanceRepository
|
||||
instanceDomainRepo InstanceDomainRepository
|
||||
sessionRepo SessionRepository
|
||||
userRepo UserRepository
|
||||
}
|
||||
|
||||
func (o *InvokeOpts) DB() database.QueryExecutor {
|
||||
|
||||
@@ -191,3 +191,41 @@ func (c *MockPermissionCheckerCheckProjectPermissionCall) DoAndReturn(f func(con
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// CheckSessionPermission mocks base method.
|
||||
func (m *MockPermissionChecker) CheckSessionPermission(ctx context.Context, permission, sessionID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CheckSessionPermission", ctx, permission, sessionID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CheckSessionPermission indicates an expected call of CheckSessionPermission.
|
||||
func (mr *MockPermissionCheckerMockRecorder) CheckSessionPermission(ctx, permission, sessionID any) *MockPermissionCheckerCheckSessionPermissionCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckSessionPermission", reflect.TypeOf((*MockPermissionChecker)(nil).CheckSessionPermission), ctx, permission, sessionID)
|
||||
return &MockPermissionCheckerCheckSessionPermissionCall{Call: call}
|
||||
}
|
||||
|
||||
// MockPermissionCheckerCheckSessionPermissionCall wrap *gomock.Call
|
||||
type MockPermissionCheckerCheckSessionPermissionCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockPermissionCheckerCheckSessionPermissionCall) Return(arg0 error) *MockPermissionCheckerCheckSessionPermissionCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockPermissionCheckerCheckSessionPermissionCall) Do(f func(context.Context, string, string) error) *MockPermissionCheckerCheckSessionPermissionCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockPermissionCheckerCheckSessionPermissionCall) DoAndReturn(f func(context.Context, string, string) error) *MockPermissionCheckerCheckSessionPermissionCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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 NewSessionRepo(ctrl *gomock.Controller) *SessionRepo {
|
||||
return &SessionRepo{
|
||||
mock: NewMockSessionRepository(ctrl),
|
||||
SessionRepository: repository.SessionRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type SessionRepo struct {
|
||||
domain.SessionRepository
|
||||
mock *MockSessionRepository
|
||||
}
|
||||
|
||||
func (s *SessionRepo) EXPECT() *MockSessionRepositoryMockRecorder {
|
||||
return s.mock.EXPECT()
|
||||
}
|
||||
|
||||
func (s *SessionRepo) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Session, error) {
|
||||
return s.mock.Get(ctx, client, opts...)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Session, error) {
|
||||
return s.mock.List(ctx, client, opts...)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) Create(ctx context.Context, client database.QueryExecutor, user *domain.Session) error {
|
||||
return s.mock.Create(ctx, client, user)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
return s.mock.Update(ctx, client, condition, changes...)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
return s.mock.Delete(ctx, client, condition)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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 NewUserRepo(ctrl *gomock.Controller) *UserRepo {
|
||||
return &UserRepo{
|
||||
mock: NewMockUserRepository(ctrl),
|
||||
UserRepository: repository.UserRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type UserRepo struct {
|
||||
domain.UserRepository
|
||||
mock *MockUserRepository
|
||||
}
|
||||
|
||||
func (u *UserRepo) EXPECT() *MockUserRepositoryMockRecorder {
|
||||
return u.mock.EXPECT()
|
||||
}
|
||||
|
||||
func (u *UserRepo) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.User, error) {
|
||||
return u.mock.Get(ctx, client, opts...)
|
||||
}
|
||||
|
||||
func (u *UserRepo) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.User, error) {
|
||||
return u.mock.List(ctx, client, opts...)
|
||||
}
|
||||
|
||||
func (u *UserRepo) Create(ctx context.Context, client database.QueryExecutor, user *domain.User) error {
|
||||
return u.mock.Create(ctx, client, user)
|
||||
}
|
||||
|
||||
func (u *UserRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
return u.mock.Delete(ctx, client, condition)
|
||||
}
|
||||
|
||||
func (u *UserRepo) Human() domain.HumanUserRepository {
|
||||
return u.mock.Human()
|
||||
}
|
||||
|
||||
func (u *UserRepo) Machine() domain.MachineUserRepository {
|
||||
return u.mock.Machine()
|
||||
}
|
||||
@@ -9,6 +9,8 @@ const (
|
||||
DomainWritePermission = "domain.write"
|
||||
OrganizationReadPermission = "organization.read"
|
||||
OrganizationWritePermission = "organization.write"
|
||||
SessionReadPermission = "session.read"
|
||||
SessionWritePermission = "session.write"
|
||||
)
|
||||
|
||||
// PermissionRepository is the interface that manages and checks Zitadel permissions.
|
||||
@@ -36,10 +38,19 @@ type PermissionChecker interface {
|
||||
// Check if the authenticated user has the given permission on the given project grant.
|
||||
// A permission may be inherited from the instance or granted organization level.
|
||||
CheckProjectGrantPermission(ctx context.Context, permission, projectGrantID string) error
|
||||
|
||||
// Check if the user in context has the given permission on the given session.
|
||||
// A permission may be inherited from the instance or granted organization level.
|
||||
CheckSessionPermission(ctx context.Context, permission, sessionID string) error
|
||||
}
|
||||
|
||||
type noopPermissionChecker struct{}
|
||||
|
||||
// CheckSessionPermission implements [PermissionChecker].
|
||||
func (n *noopPermissionChecker) CheckSessionPermission(ctx context.Context, permission string, sessionID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ PermissionChecker = (*noopPermissionChecker)(nil)
|
||||
|
||||
func (*noopPermissionChecker) CheckInstancePermission(context.Context, string) error {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type CheckUserType struct {
|
||||
UserID string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
type UserCheckCommand struct {
|
||||
InputCheckUser *CheckUserType
|
||||
SessionID string
|
||||
InstanceID string
|
||||
|
||||
FetchedUser User
|
||||
|
||||
// Out
|
||||
PreferredUserLanguage *language.Tag
|
||||
UserCheckedAt time.Time
|
||||
}
|
||||
|
||||
// NewUserCheckCommand returns a check Commander validating the input user.
|
||||
//
|
||||
// It assumes that a [Session] already exists: this check should be part of the
|
||||
// batch call to create/set a session.
|
||||
//
|
||||
// The check will update the existing session or return an error if the session
|
||||
// is not found or validation fails.
|
||||
func NewUserCheckCommand(sessionID, instanceID string, checkUser *CheckUserType) *UserCheckCommand {
|
||||
return &UserCheckCommand{
|
||||
SessionID: sessionID,
|
||||
InstanceID: instanceID,
|
||||
InputCheckUser: checkUser,
|
||||
}
|
||||
}
|
||||
|
||||
// RequiresTransaction implements [Transactional].
|
||||
func (u *UserCheckCommand) RequiresTransaction() {}
|
||||
|
||||
// Events implements [Commander].
|
||||
func (u *UserCheckCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
|
||||
if u.InputCheckUser == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []eventstore.Command{
|
||||
session.NewUserCheckedEvent(
|
||||
ctx,
|
||||
&session.NewAggregate(u.SessionID, u.InstanceID).Aggregate,
|
||||
u.FetchedUser.ID,
|
||||
u.FetchedUser.OrganizationID,
|
||||
u.UserCheckedAt,
|
||||
u.PreferredUserLanguage,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute implements [Commander].
|
||||
func (u *UserCheckCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
|
||||
if u.InputCheckUser == nil {
|
||||
return err
|
||||
}
|
||||
sessionRepo := opts.sessionRepo
|
||||
|
||||
if human := u.FetchedUser.Human; human != nil {
|
||||
if !human.PreferredLanguage.IsRoot() {
|
||||
u.PreferredUserLanguage = &human.PreferredLanguage
|
||||
}
|
||||
}
|
||||
|
||||
session, err := sessionRepo.Get(ctx, opts.DB(), database.WithCondition(sessionRepo.IDCondition(u.SessionID)))
|
||||
if err != nil {
|
||||
if errors.Is(err, &database.NoRowFoundError{}) {
|
||||
return zerrors.ThrowNotFound(err, "DOM-rbdCv3", "session not found")
|
||||
}
|
||||
return zerrors.ThrowInternal(err, "DOM-To1rLz", "failed fetching session")
|
||||
}
|
||||
|
||||
if session.UserID != "" && u.FetchedUser.ID != "" && session.UserID != u.FetchedUser.ID {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOM-78g1TV", "user change not possible")
|
||||
}
|
||||
userFactor := &SessionFactorUser{
|
||||
UserID: u.FetchedUser.ID,
|
||||
LastVerifiedAt: time.Now(),
|
||||
}
|
||||
|
||||
updateCount, err := sessionRepo.Update(ctx, opts.DB(), sessionRepo.IDCondition(session.ID), sessionRepo.SetFactor(userFactor))
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "DOM-netNam", "failed updating session")
|
||||
}
|
||||
|
||||
if updateCount == 0 {
|
||||
err = zerrors.ThrowNotFound(nil, "DOM-FszyWS", "session not found")
|
||||
return err
|
||||
}
|
||||
if updateCount > 1 {
|
||||
err = zerrors.ThrowInternal(NewMultipleObjectsUpdatedError(1, updateCount), "DOM-SsIwDt", "unexpected number of rows updated")
|
||||
return err
|
||||
}
|
||||
|
||||
u.UserCheckedAt = userFactor.LastVerifiedAt
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// String implements [Commander].
|
||||
func (u *UserCheckCommand) String() string {
|
||||
return "UserCheckCommand"
|
||||
}
|
||||
|
||||
// Validate implements [Commander].
|
||||
func (u *UserCheckCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
|
||||
if u.InputCheckUser == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if u.SessionID == "" {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "DOM-00o0ys", "Errors.Missing.SessionID")
|
||||
}
|
||||
if u.InstanceID == "" {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "DOM-Oe1dtz", "Errors.Missing.InstanceID")
|
||||
}
|
||||
|
||||
if authZErr := opts.Permissions.CheckSessionPermission(ctx, SessionWritePermission, u.SessionID); authZErr != nil {
|
||||
return zerrors.ThrowPermissionDenied(authZErr, "DOM-4qz3mt", "Errors.PermissionDenied")
|
||||
}
|
||||
|
||||
var usrQueryOpt database.QueryOption
|
||||
userRepo := opts.userRepo
|
||||
|
||||
if loginName := u.InputCheckUser.LoginName; loginName != "" {
|
||||
usrQueryOpt = database.WithCondition(userRepo.LoginNameCondition(database.TextOperationEqual, loginName))
|
||||
} else if userID := u.InputCheckUser.UserID; userID != "" {
|
||||
usrQueryOpt = database.WithCondition(userRepo.IDCondition(userID))
|
||||
} else {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOM-7B2m0b", "no valid query option")
|
||||
}
|
||||
|
||||
usr, err := userRepo.Get(ctx, opts.DB(), usrQueryOpt)
|
||||
if err != nil {
|
||||
if errors.Is(err, &database.NoRowFoundError{}) {
|
||||
return zerrors.ThrowNotFound(err, "DOM-lcZeXI", "user not found")
|
||||
}
|
||||
return zerrors.ThrowInternal(err, "DOM-Y846I0", "failed fetching user")
|
||||
}
|
||||
|
||||
if usr.State != UserStateActive {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "DOM-vgDIu9", "Errors.User.NotActive")
|
||||
}
|
||||
|
||||
u.FetchedUser = *usr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Commander = (*UserCheckCommand)(nil)
|
||||
var _ Transactional = (*UserCheckCommand)(nil)
|
||||
@@ -0,0 +1,735 @@
|
||||
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: zerrors.ThrowPreconditionFailed(nil, "DOM-00o0ys", "Errors.Missing.SessionID"),
|
||||
},
|
||||
{
|
||||
testName: "when instance ID is not set should return error",
|
||||
cmd: &domain.UserCheckCommand{
|
||||
InputCheckUser: &domain.CheckUserType{UserID: "user-123"},
|
||||
SessionID: "session-1",
|
||||
},
|
||||
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-Oe1dtz", "Errors.Missing.InstanceID"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowPreconditionFailed(nil, "DOM-vgDIu9", "Errors.User.NotActive"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowInternal(getErr, "DOM-Y846I0", "failed fetching user"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowNotFound(notFoundErr, "DOM-lcZeXI", "user not found"),
|
||||
},
|
||||
{
|
||||
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, "[email protected]"),
|
||||
),
|
||||
)).
|
||||
Times(1).
|
||||
Return(&domain.User{
|
||||
ID: "user-123",
|
||||
LoginNames: []domain.LoginName{{LoginName: "[email protected]"}},
|
||||
OrganizationID: "org-1",
|
||||
State: domain.UserStateActive,
|
||||
}, nil)
|
||||
return repo
|
||||
},
|
||||
cmd: &domain.UserCheckCommand{
|
||||
InputCheckUser: &domain.CheckUserType{LoginName: "[email protected]"},
|
||||
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: zerrors.ThrowInvalidArgumentf(nil, "DOM-7B2m0b", "no valid query option"),
|
||||
},
|
||||
}
|
||||
|
||||
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: zerrors.ThrowInternal(getErr, "DOM-To1rLz", "failed fetching session"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowNotFound(sessionNotFoundErr, "DOM-rbdCv3", "session not found"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowInvalidArgument(nil, "DOM-78g1TV", "user change not possible"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowInternal(updateErr, "DOM-netNam", "failed updating 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: zerrors.ThrowNotFound(nil, "DOM-FszyWS", "session not found"),
|
||||
},
|
||||
{
|
||||
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: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-SsIwDt", "unexpected number of rows updated"),
|
||||
},
|
||||
{
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
@@ -126,15 +127,15 @@ func (c Changes) Write(builder *StatementBuilder) error {
|
||||
|
||||
// Matches implements [gomock.Matcher].
|
||||
func (c Changes) Matches(x any) bool {
|
||||
toMatch, ok := x.(*Changes)
|
||||
toMatch, ok := x.(Changes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if len(c) != len(*toMatch) {
|
||||
if len(c) != len(toMatch) {
|
||||
return false
|
||||
}
|
||||
for i := range c {
|
||||
if !c[i].Matches((*toMatch)[i]) {
|
||||
if !c[i].Matches(toMatch[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -384,7 +385,26 @@ func (c *cteChange) Matches(x any) bool {
|
||||
if expectedCTEBuilder.String() != actualCTEBuilder.String() {
|
||||
return false
|
||||
}
|
||||
return slices.Equal(expectedCTEBuilder.Args(), actualCTEBuilder.Args())
|
||||
|
||||
if len(expectedCTEBuilder.Args()) != len(actualCTEBuilder.Args()) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, e := range expectedCTEBuilder.Args() {
|
||||
/* This comparison is an oversimplification because it compares only the types. If the type
|
||||
* is a struct, we would need to type assert that and come up with a custom comparator.
|
||||
* In some cases, like for [domain.SessionFactorUser] it can become quite tricky because
|
||||
* it includes a [time.Time] value, for which you will need to do comparisons in range rather
|
||||
* than equal matches. Of course, this is just an example, but we could have much more complex
|
||||
* structs that makes it very hard to forecast what kind of code we need to write here
|
||||
*/
|
||||
expectedType, actualType := fmt.Sprintf("%T", e), fmt.Sprintf("%T", actualCTEBuilder.Args()[i])
|
||||
if expectedType != actualType {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetName implements [CTEChange].
|
||||
|
||||
Reference in New Issue
Block a user