package command import ( "context" "io" "net" "testing" "time" "github.com/golang/mock/gomock" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/user" ) func TestCommandSide_AddHumanTOTP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore } type ( args struct { ctx context.Context orgID string userID string } ) type res struct { want *domain.ObjectDetails err func(error) bool } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: eventstoreExpect( t, ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "", }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, { name: "user not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter(), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ err: caos_errs.IsPreconditionFailed, }, }, { name: "org not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), expectFilter(), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ err: caos_errs.IsPreconditionFailed, }, }, { name: "org iam policy not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), expectFilter( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), expectFilter(), expectFilter(), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ err: caos_errs.IsPreconditionFailed, }, }, { name: "otp already exists, already exists error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), expectFilter( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), expectFilter( org.NewDomainPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, true, true, true, ), ), expectFilter( user.NewHumanOTPAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("a"), }, ), user.NewHumanOTPVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "agent1", ), ), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ err: caos_errs.IsErrorAlreadyExists, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } got, err := r.AddHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { assert.Equal(t, tt.res.want, got) } }) } } func TestCommands_createHumanTOTP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore } type args struct { ctx context.Context userID string resourceOwner string } tests := []struct { name string fields fields args args want bool wantErr error }{ { name: "user not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter(), ), }, args: args{ ctx: context.Background(), resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SqyJz", "Errors.User.NotFound"), }, { name: "org not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), ), expectFilter(), ), }, args: args{ ctx: context.Background(), resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-55M9f", "Errors.Org.NotFound"), }, { name: "org iam policy not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), ), expectFilter(), expectFilter(), ), }, args: args{ ctx: context.Background(), resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound"), }, { name: "otp already exists, already exists error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), ), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, true, true, true, ), ), ), expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("a"), }), ), eventFromEventPusher( user.NewHumanOTPVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "agent1")), ), ), }, args: args{ ctx: context.Background(), resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady"), }, { name: "issuer not in context", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), ), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, true, true, true, ), ), ), expectFilter(), ), }, args: args{ ctx: context.Background(), resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowInternal(nil, "TOTP-ieY3o", "Errors.Internal"), }, { name: "success", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", "lastname", "nickname", "displayname", language.German, domain.GenderUnspecified, "email@test.ch", true, ), ), ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), &user.NewAggregate("org1", "org1").Aggregate, "org", ), ), ), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, true, true, true, ), ), ), expectFilter(), ), }, args: args{ ctx: authz.WithRequestedDomain(context.Background(), "zitadel.com"), resourceOwner: "org1", userID: "user1", }, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, }, } got, err := c.createHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) require.ErrorIs(t, err, tt.wantErr) if tt.want { require.NotNil(t, got) assert.NotNil(t, got.wm) assert.NotNil(t, got.userAgg) require.NotNil(t, got.key) assert.NotEmpty(t, got.key.URL()) assert.NotEmpty(t, got.key.Secret()) assert.Len(t, got.cmds, 1) } }) } } func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { ctx := authz.NewMockContext("", "org1", "user1") cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg) require.NoError(t, err) userAgg := &user.NewAggregate("user1", "org1").Aggregate code, err := totp.GenerateCode(key.Secret(), time.Now()) require.NoError(t, err) type fields struct { eventstore *eventstore.Eventstore } type args struct { userID string code string resourceOwner string } tests := []struct { name string fields fields args args want bool wantErr error }{ { name: "missing user id", args: args{}, wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"), }, { name: "filter error", fields: fields{ eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe), ), }, args: args{ userID: "user1", resourceOwner: "org1", }, wantErr: io.ErrClosedPipe, }, { name: "otp not existing error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), ), eventFromEventPusher( user.NewHumanOTPRemovedEvent(ctx, userAgg), ), ), ), }, args: args{ resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotExisting"), }, { name: "otp already ready error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), ), eventFromEventPusher( user.NewHumanOTPVerifiedEvent(context.Background(), userAgg, "agent1", ), ), ), ), }, args: args{ resourceOwner: "org1", userID: "user1", }, wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady"), }, { name: "wrong code", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), ), ), ), }, args: args{ resourceOwner: "org1", code: "wrong", userID: "user1", }, wantErr: caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"), }, { name: "push error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), ), ), expectPushFailed(io.ErrClosedPipe, user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1", ), ), ), }, args: args{ resourceOwner: "org1", code: code, userID: "user1", }, wantErr: io.ErrClosedPipe, }, { name: "success", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), ), ), expectPush( user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1", ), ), ), }, args: args{ resourceOwner: "org1", code: code, userID: "user1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: cryptoAlg, }, }, } got, err := c.HumanCheckMFATOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner) require.ErrorIs(t, err, tt.wantErr) if tt.want { require.NotNil(t, got) assert.Equal(t, "org1", got.ResourceOwner) } }) } } func TestCommandSide_RemoveHumanTOTP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore } type ( args struct { ctx context.Context orgID string userID string } ) type res struct { want *domain.ObjectDetails err func(error) bool } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: eventstoreExpect( t, ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "", }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, { name: "otp not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter(), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ err: caos_errs.IsNotFound, }, }, { name: "otp not existing, not found error", fields: fields{ eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, nil, ), ), ), expectPush( user.NewHumanOTPRemovedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: context.Background(), orgID: "org1", userID: "user1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } got, err := r.HumanRemoveTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { assert.NoError(t, err) } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { assert.Equal(t, tt.res.want, got) } }) } } func TestCommandSide_AddHumanOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing"), }, }, { name: "wrong user, permission denied error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "other", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), }, }, { name: "otp sms already exists, already exists error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady"), }, }, { name: "phone not verified, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "phone removed, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanPhoneChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "+4179654321", ), ), eventFromEventPusher( user.NewHumanPhoneVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), eventFromEventPusher( user.NewHumanPhoneRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanPhoneChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "+4179654321", ), ), eventFromEventPusher( user.NewHumanPhoneVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } got, err := r.AddHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_AddHumanOTPSMSWithCheckSucceeded(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanPhoneChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "+4179654321", ), ), eventFromEventPusher( user.NewHumanPhoneVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add with auth request", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanPhoneChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "+4179654321", ), ), eventFromEventPusher( user.NewHumanPhoneVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), user.NewHumanOTPSMSCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } got, err := r.AddHumanOTPSMSWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing"), }, }, { name: "other user not permission, permission denied error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: ctx, userID: "other", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, }, { name: "otp sms not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting"), }, }, { name: "successful remove", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPSMSRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } got, err := r.RemoveHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_HumanSendOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") defaultGenerators := &SecretGenerators{ OTPSMS: &crypto.GeneratorConfig{ Length: 8, Expiry: time.Hour, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true, IncludeSymbols: true, }, } type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm defaultSecretGenerators *SecretGenerators } type ( args struct { ctx context.Context userID string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing"), }, }, { name: "otp sms not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate, domain.SecretGeneratorTypeOTPSMS, 8, time.Hour, true, true, true, true, )), ), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, nil, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add (without secret config)", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter(), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, nil, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add with auth request", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate, domain.SecretGeneratorTypeOTPSMS, 8, time.Hour, true, true, true, true, )), ), expectPush( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.HumanSendOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) }) } } func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing"), }, }, { name: "otp sms not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPSMSCodeSentEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } err := r.HumanOTPSMSCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) }) } } func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm } type ( args struct { ctx context.Context userID string code string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", code: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"), }, }, { name: "code missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "user1", code: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"), }, }, { name: "otp sms not added, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "otp sms code not added, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), ), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"), }, }, { name: "invalid code, error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), eventFromEventPusherWithCreationDateNow( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("other-code"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), expectPush( user.NewHumanOTPSMSCheckFailedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"), }, }, { name: "code ok", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPSMSAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), eventFromEventPusherWithCreationDateNow( user.NewHumanOTPSMSCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("code"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), expectPush( user.NewHumanOTPSMSCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, } err := r.HumanCheckOTPSMS(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) }) } } func TestCommandSide_AddHumanOTPEmail(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing"), }, }, { name: "otp email already exists, already exists error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady"), }, }, { name: "email not verified, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanEmailChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "email@test.ch", ), ), eventFromEventPusher( user.NewHumanEmailVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } got, err := r.AddHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_AddHumanOTPEmailWithCheckSucceeded(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanEmailChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "email@test.ch", ), ), eventFromEventPusher( user.NewHumanEmailVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add with auth request", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanEmailChangedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "email@test.ch", ), ), eventFromEventPusher( user.NewHumanEmailVerifiedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), user.NewHumanOTPEmailCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } got, err := r.AddHumanOTPEmailWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing"), }, }, { name: "other user not permission, permission denied error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: ctx, userID: "other", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, }, { name: "otp email not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting"), }, }, { name: "successful remove", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPEmailRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } got, err := r.RemoveHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) } } func TestCommandSide_HumanSendOTPEmail(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") defaultGenerators := &SecretGenerators{ OTPEmail: &crypto.GeneratorConfig{ Length: 8, Expiry: time.Hour, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true, IncludeSymbols: true, }, } type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm defaultSecretGenerators *SecretGenerators } type ( args struct { ctx context.Context userID string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing"), }, }, { name: "otp email not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, time.Hour, true, true, true, true, )), ), expectPush( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, nil, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add (without secret config)", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter(), expectPush( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, nil, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, { name: "successful add with auth request", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectFilter( eventFromEventPusher( instance.NewSecretGeneratorAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, time.Hour, true, true, true, true, )), ), expectPush( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("12345678"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"), defaultSecretGenerators: defaultGenerators, }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.HumanSendOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) }) } } func TestCommandSide_HumanOTPEmailCodeSent(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { ctx context.Context userID string resourceOwner string } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing"), }, }, { name: "otp email not added, not found error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "successful add", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), expectPush( user.NewHumanOTPEmailCodeSentEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), }, args: args{ ctx: ctx, userID: "user1", resourceOwner: "org1", }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } err := r.HumanOTPEmailCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) }) } } func TestCommandSide_HumanCheckOTPEmail(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm } type ( args struct { ctx context.Context userID string code string resourceOwner string authRequest *domain.AuthRequest } ) type res struct { want *domain.ObjectDetails err error } tests := []struct { name string fields fields args args res res }{ { name: "userid missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "", code: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"), }, }, { name: "code missing, invalid argument error", fields: fields{ eventstore: expectEventstore(), }, args: args{ ctx: ctx, userID: "user1", code: "", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"), }, }, { name: "otp email not added, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter(), ), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"), }, }, { name: "otp email code not added, precondition failed error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), ), ), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", }, res: res{ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"), }, }, { name: "invalid code, error", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), eventFromEventPusherWithCreationDateNow( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("other-code"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), expectPush( user.NewHumanOTPEmailCheckFailedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ err: caos_errs.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"), }, }, { name: "code ok", fields: fields{ eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPEmailAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), eventFromEventPusherWithCreationDateNow( user.NewHumanOTPEmailCodeAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("code"), }, time.Hour, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), expectPush( user.NewHumanOTPEmailCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, &user.AuthRequestInfo{ ID: "authRequestID", UserAgentID: "userAgentID", BrowserInfo: &user.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, ), ), ), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: ctx, userID: "user1", code: "code", resourceOwner: "org1", authRequest: &domain.AuthRequest{ ID: "authRequestID", AgentID: "userAgentID", BrowserInfo: &domain.BrowserInfo{ UserAgent: "user-agent", AcceptLanguage: "en", RemoteIP: net.IP{192, 0, 2, 1}, }, }, }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, } err := r.HumanCheckOTPEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) }) } }