mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-13 03:24:26 +00:00
f653589609
# Which Problems Are Solved The recently added possibility to generate and verify codes through Twilio verification service did failed on checking OTP SMS code through the session API. Additionally, password codes generated by the V2 API and sent through phone would always use the internal generator and verification mechanism rather than the configured. # How the Problems Are Solved - Correctly set the verifier for OTP SMS for the session API - Always use the internal verifier for OTP Email (for now) - Select the generator / verifier based on the configuration for password codes with notification type SMS for V2 APIs # Additional Changes None # Additional Context - relates to #8678 - reported by customer --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
1301 lines
35 KiB
Go
1301 lines
35 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
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|
|
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.NewHumanOTPEmailAddedEvent(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.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)),
|
|
},
|
|
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)),
|
|
},
|
|
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)),
|
|
},
|
|
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)),
|
|
},
|
|
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
|
|
},
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|