mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-01 20:32:19 +00:00
# Which Problems Are Solved While the lockout policy was correctly applied on the session API and other authentication and management endpoints , it had no effect on the user service v2 endpoints. # How the Problems Are Solved - Correctly apply lockout policy on the user service v2 endpoints. - Added tar pitting to auth factor checks (authentication and management API) to prevent brute-force attacks or denial of service because of user lockouts. - Tar pitting is not active if `IgnoreUnknownUsername` option is active to prevent leaking information whether a user exists or not. # Additional Changes None # Additional Context - requires backports * cleanup (cherry picked from commitb8db8cdf9c) (cherry picked from commitd3713dfaed)
1340 lines
36 KiB
Go
1340 lines
36 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/notification/senders"
|
|
"github.com/zitadel/zitadel/internal/notification/senders/mock"
|
|
"github.com/zitadel/zitadel/internal/repository/org"
|
|
"github.com/zitadel/zitadel/internal/repository/session"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) {
|
|
type fields struct {
|
|
userID string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
createPhoneCode encryptedCodeGeneratorWithDefaultFunc
|
|
}
|
|
type res struct {
|
|
err error
|
|
returnCode string
|
|
commands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
res res
|
|
}{
|
|
{
|
|
name: "userID missing, precondition error",
|
|
fields: fields{
|
|
userID: "",
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKL3g", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "otp not ready, precondition error",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "generate code",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createPhoneCode: mockEncryptedCodeGeneratorWithDefault("1234567", 5*time.Minute),
|
|
},
|
|
res: res{
|
|
returnCode: "1234567",
|
|
commands: []eventstore.Command{
|
|
session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
true,
|
|
"",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
// config will not be actively used for the test (is only for default),
|
|
// but not providing it would result in a nil pointer
|
|
defaultSecretGenerators: &SecretGenerators{
|
|
OTPSMS: emptyConfig,
|
|
},
|
|
}
|
|
var dst string
|
|
cmd := c.CreateOTPSMSChallengeReturnCode(&dst)
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
createPhoneCode: tt.fields.createPhoneCode,
|
|
now: time.Now,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Empty(t, gotCmds)
|
|
assert.Equal(t, tt.res.returnCode, dst)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_CreateOTPSMSChallenge(t *testing.T) {
|
|
type fields struct {
|
|
userID string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
createPhoneCode encryptedCodeGeneratorWithDefaultFunc
|
|
}
|
|
type res struct {
|
|
err error
|
|
commands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
res res
|
|
}{
|
|
{
|
|
name: "userID missing, precondition error",
|
|
fields: fields{
|
|
userID: "",
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKL3g", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "otp not ready, precondition error",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "generate code",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createPhoneCode: mockEncryptedCodeGeneratorWithDefault("1234567", 5*time.Minute),
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
false,
|
|
"",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "generate code externally",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createPhoneCode: mockEncryptedCodeGeneratorWithDefaultExternal("generatorID"),
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
nil,
|
|
0,
|
|
false,
|
|
"generatorID",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
// config will not be actively used for the test (is only for default),
|
|
// but not providing it would result in a nil pointer
|
|
defaultSecretGenerators: &SecretGenerators{
|
|
OTPSMS: emptyConfig,
|
|
},
|
|
}
|
|
|
|
cmd := c.CreateOTPSMSChallenge()
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
createPhoneCode: tt.fields.createPhoneCode,
|
|
now: time.Now,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Empty(t, gotCmds)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_OTPSMSSent(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
sessionID string
|
|
resourceOwner string
|
|
generatorInfo *senders.CodeGeneratorInfo
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "not challenged, precondition error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
sessionID: "sessionID",
|
|
resourceOwner: "instanceID",
|
|
generatorInfo: &senders.CodeGeneratorInfo{},
|
|
},
|
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-G3t31", "Errors.User.Code.NotFound"),
|
|
},
|
|
{
|
|
name: "challenged and sent",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
false,
|
|
"",
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
session.NewOTPSMSSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, &senders.CodeGeneratorInfo{}),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
sessionID: "sessionID",
|
|
resourceOwner: "instanceID",
|
|
generatorInfo: &senders.CodeGeneratorInfo{},
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "challenged and sent (externally)",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
session.NewOTPSMSChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
nil,
|
|
0,
|
|
false,
|
|
"",
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
session.NewOTPSMSSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, &senders.CodeGeneratorInfo{ID: "generatorID", VerificationID: "verificationID"}),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
sessionID: "sessionID",
|
|
resourceOwner: "instanceID",
|
|
generatorInfo: &senders.CodeGeneratorInfo{
|
|
ID: "generatorID",
|
|
VerificationID: "verificationID",
|
|
},
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
err := c.OTPSMSSent(tt.args.ctx, tt.args.sessionID, tt.args.resourceOwner, tt.args.generatorInfo)
|
|
assert.ErrorIs(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_CreateOTPEmailChallengeURLTemplate(t *testing.T) {
|
|
type fields struct {
|
|
userID string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
createCode encryptedCodeWithDefaultFunc
|
|
}
|
|
type args struct {
|
|
urlTmpl string
|
|
}
|
|
type res struct {
|
|
templateError error
|
|
err error
|
|
commands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "invalid template, precondition error",
|
|
args: args{
|
|
urlTmpl: "https://example.com/mfa/email?userID={{.UserID}}&code={{.InvalidField}}",
|
|
},
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
templateError: zerrors.ThrowInvalidArgument(nil, "DOMAIN-ieYa7", "Errors.User.InvalidURLTemplate"),
|
|
},
|
|
},
|
|
{
|
|
name: "userID missing, precondition error",
|
|
args: args{
|
|
urlTmpl: "https://example.com/mfa/email?userID={{.UserID}}&code={{.Code}}&lang={{.PreferredLanguage}}",
|
|
},
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "otp not ready, precondition error",
|
|
args: args{
|
|
urlTmpl: "https://example.com/mfa/email?userID={{.UserID}}&code={{.Code}}&lang={{.PreferredLanguage}}",
|
|
},
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "generate code",
|
|
args: args{
|
|
urlTmpl: "https://example.com/mfa/email?userID={{.UserID}}&code={{.Code}}&lang={{.PreferredLanguage}}",
|
|
},
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createCode: mockEncryptedCodeWithDefault("1234567", 5*time.Minute),
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
session.NewOTPEmailChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
false,
|
|
"https://example.com/mfa/email?userID={{.UserID}}&code={{.Code}}&lang={{.PreferredLanguage}}",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
// config will not be actively used for the test (is only for default),
|
|
// but not providing it would result in a nil pointer
|
|
defaultSecretGenerators: &SecretGenerators{
|
|
OTPEmail: emptyConfig,
|
|
},
|
|
}
|
|
|
|
cmd, err := c.CreateOTPEmailChallengeURLTemplate(tt.args.urlTmpl)
|
|
assert.ErrorIs(t, err, tt.res.templateError)
|
|
if tt.res.templateError != nil {
|
|
return
|
|
}
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
createCode: tt.fields.createCode,
|
|
now: time.Now,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Empty(t, gotCmds)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_CreateOTPEmailChallengeReturnCode(t *testing.T) {
|
|
type fields struct {
|
|
userID string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
createCode encryptedCodeWithDefaultFunc
|
|
}
|
|
type res struct {
|
|
err error
|
|
returnCode string
|
|
commands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
res res
|
|
}{
|
|
{
|
|
name: "userID missing, precondition error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "otp not ready, precondition error",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "generate code",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createCode: mockEncryptedCodeWithDefault("1234567", 5*time.Minute),
|
|
},
|
|
res: res{
|
|
returnCode: "1234567",
|
|
commands: []eventstore.Command{
|
|
session.NewOTPEmailChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
true,
|
|
"",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
// config will not be actively used for the test (is only for default),
|
|
// but not providing it would result in a nil pointer
|
|
defaultSecretGenerators: &SecretGenerators{
|
|
OTPEmail: emptyConfig,
|
|
},
|
|
}
|
|
var dst string
|
|
cmd := c.CreateOTPEmailChallengeReturnCode(&dst)
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
createCode: tt.fields.createCode,
|
|
now: time.Now,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Empty(t, gotCmds)
|
|
assert.Equal(t, tt.res.returnCode, dst)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_CreateOTPEmailChallenge(t *testing.T) {
|
|
type fields struct {
|
|
userID string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
createCode encryptedCodeWithDefaultFunc
|
|
}
|
|
type res struct {
|
|
err error
|
|
commands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
res res
|
|
}{
|
|
{
|
|
name: "userID missing, precondition error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "otp not ready, precondition error",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "generate code",
|
|
fields: fields{
|
|
userID: "userID",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org").Aggregate),
|
|
),
|
|
),
|
|
),
|
|
createCode: mockEncryptedCodeWithDefault("1234567", 5*time.Minute),
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
session.NewOTPEmailChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
false,
|
|
"",
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
// config will not be actively used for the test (is only for default),
|
|
// but not providing it would result in a nil pointer
|
|
defaultSecretGenerators: &SecretGenerators{
|
|
OTPEmail: emptyConfig,
|
|
},
|
|
}
|
|
|
|
cmd := c.CreateOTPEmailChallenge()
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
createCode: tt.fields.createCode,
|
|
now: time.Now,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Empty(t, gotCmds)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_OTPEmailSent(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
sessionID string
|
|
resourceOwner string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "not challenged, precondition error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
sessionID: "sessionID",
|
|
resourceOwner: "instanceID",
|
|
},
|
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SLr02", "Errors.User.Code.NotFound"),
|
|
},
|
|
{
|
|
name: "challenged and sent",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
session.NewOTPEmailChallengedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("1234567"),
|
|
},
|
|
5*time.Minute,
|
|
false,
|
|
"",
|
|
),
|
|
),
|
|
),
|
|
expectPush(
|
|
session.NewOTPEmailSentEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
sessionID: "sessionID",
|
|
resourceOwner: "instanceID",
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
err := c.OTPEmailSent(tt.args.ctx, tt.args.sessionID, tt.args.resourceOwner)
|
|
assert.ErrorIs(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckOTPSMS(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
userID string
|
|
otpCodeChallenge *OTPCode
|
|
otpAlg crypto.EncryptionAlgorithm
|
|
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
|
|
}
|
|
type args struct {
|
|
code string
|
|
}
|
|
type res struct {
|
|
err error
|
|
commands []eventstore.Command
|
|
errorCommands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "missing userID",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
userID: "",
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing code",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
userID: "userID",
|
|
},
|
|
args: args{},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
|
|
},
|
|
},
|
|
{
|
|
name: "not set up",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
userID: "userID",
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing challenge",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: nil,
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
|
|
},
|
|
},
|
|
{
|
|
name: "invalid code",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
|
|
0, 0, false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow.Add(-10 * time.Minute),
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
|
|
errorCommands: []eventstore.Command{
|
|
user.NewHumanOTPSMSCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid code, locked",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
|
|
0, 1, false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow.Add(-10 * time.Minute),
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
|
|
errorCommands: []eventstore.Command{
|
|
user.NewHumanOTPSMSCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "check ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow,
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
user.NewHumanOTPSMSCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
session.NewOTPSMSCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
testNow,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "check ok (external)",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: nil,
|
|
Expiry: 0,
|
|
GeneratorID: "generatorID",
|
|
VerificationID: "verificationID",
|
|
CreationDate: testNow,
|
|
},
|
|
getCodeVerifier: func(ctx context.Context, id string) (senders.CodeGenerator, error) {
|
|
sender := mock.NewMockCodeGenerator(gomock.NewController(t))
|
|
sender.EXPECT().VerifyCode("verificationID", "code").Return(nil)
|
|
return sender, nil
|
|
},
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
user.NewHumanOTPSMSCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
session.NewOTPSMSCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
testNow,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "check ok, locked in the meantime",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(
|
|
user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow,
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cmd := CheckOTPSMS(tt.args.code)
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
OTPSMSCodeChallenge: tt.fields.otpCodeChallenge,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
otpAlg: tt.fields.otpAlg,
|
|
getCodeVerifier: tt.fields.getCodeVerifier,
|
|
now: func() time.Time {
|
|
return testNow
|
|
},
|
|
tarpit: func(failedAttempts uint64) {
|
|
|
|
},
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.errorCommands, gotCmds)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckOTPEmail(t *testing.T) {
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
userID string
|
|
otpCodeChallenge *OTPCode
|
|
otpAlg crypto.EncryptionAlgorithm
|
|
tarpit Tarpit
|
|
}
|
|
type args struct {
|
|
code string
|
|
}
|
|
type res struct {
|
|
err error
|
|
commands []eventstore.Command
|
|
errorCommands []eventstore.Command
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
name: "missing userID",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
userID: "",
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing code",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
userID: "userID",
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{},
|
|
res: res{
|
|
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
|
|
},
|
|
},
|
|
{
|
|
name: "not set up",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
userID: "userID",
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
|
|
},
|
|
},
|
|
{
|
|
name: "missing challenge",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: nil,
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
|
|
},
|
|
},
|
|
{
|
|
name: "invalid code",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
|
|
0, 0, false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow.Add(-10 * time.Minute),
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
tarpit: expectTarpit(1),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
|
|
errorCommands: []eventstore.Command{
|
|
user.NewHumanOTPEmailCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid code, locked",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
|
|
0, 1, false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow.Add(-10 * time.Minute),
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
tarpit: expectTarpit(1),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
|
|
errorCommands: []eventstore.Command{
|
|
user.NewHumanOTPEmailCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "check ok",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(), // recheck
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow,
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
commands: []eventstore.Command{
|
|
user.NewHumanOTPEmailCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
|
|
session.NewOTPEmailCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
testNow,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "check ok, locked in the meantime",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
|
|
),
|
|
expectFilter(
|
|
user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
|
|
),
|
|
),
|
|
userID: "userID",
|
|
otpCodeChallenge: &OTPCode{
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: 5 * time.Minute,
|
|
CreationDate: testNow,
|
|
},
|
|
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
tarpit: expectTarpit(0),
|
|
},
|
|
args: args{
|
|
code: "code",
|
|
},
|
|
res: res{
|
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cmd := CheckOTPEmail(tt.args.code)
|
|
|
|
sessionModel := &SessionWriteModel{
|
|
UserID: tt.fields.userID,
|
|
UserCheckedAt: testNow,
|
|
State: domain.SessionStateActive,
|
|
OTPEmailCodeChallenge: tt.fields.otpCodeChallenge,
|
|
aggregate: &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
|
}
|
|
cmds := &SessionCommands{
|
|
sessionCommands: []SessionCommand{cmd},
|
|
sessionWriteModel: sessionModel,
|
|
eventstore: tt.fields.eventstore(t),
|
|
otpAlg: tt.fields.otpAlg,
|
|
now: func() time.Time {
|
|
return testNow
|
|
},
|
|
tarpit: tt.fields.tarpit.tarpit,
|
|
}
|
|
|
|
gotCmds, err := cmd(context.Background(), cmds)
|
|
assert.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.errorCommands, gotCmds)
|
|
assert.Equal(t, tt.res.commands, cmds.eventCommands)
|
|
tt.fields.tarpit.metExpectedCalls(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func expectTarpit(requiredFailedAttempts uint64) Tarpit {
|
|
return &mockTarpit{
|
|
requiredFailedAttempts: requiredFailedAttempts,
|
|
}
|
|
}
|
|
|
|
type mockTarpit struct {
|
|
requiredFailedAttempts uint64
|
|
failedAttempts uint64
|
|
}
|
|
|
|
func (m *mockTarpit) tarpit(failedAttempts uint64) {
|
|
m.failedAttempts = failedAttempts
|
|
}
|
|
|
|
func (m *mockTarpit) metExpectedCalls(t *testing.T) bool {
|
|
t.Helper()
|
|
return assert.Equalf(t, m.requiredFailedAttempts, m.failedAttempts, "tarpit was called with %d failed attempts, but %d were expected", m.failedAttempts, m.requiredFailedAttempts)
|
|
}
|
|
|
|
type Tarpit interface {
|
|
tarpit(failedAttempts uint64)
|
|
metExpectedCalls(t *testing.T) bool
|
|
}
|