Files
zitadel/backend/v3/domain/session_challenge_util_test.go
T
Gayathri VijayanandGitHub f99142bc4a feat: implement otp sms session challenge (#11919)
# Which Problems Are Solved

This PR adds the OTP SMS Challenge needed for CreateSession API
https://github.com/zitadel/zitadel/issues/11035

# How the Problems Are Solved

- Introduce `OTPSMSChallengeCommand` (validate/execute/events) for
session creation checks
- Introduce `GetOTPCryptoGeneratorConfigWithDefault` to fetch
per-instance secret generator settings
- Add unit tests

# Additional Changes

Refactor tests in `session_check_recovery_code_test.go` to make the
helper functions reusable in `session_challenge_otp_sms_test.go`

# Additional Context
Related to https://github.com/zitadel/zitadel/issues/11035
2026-03-26 11:33:44 +00:00

236 lines
7.8 KiB
Go

package domain_test
import (
"context"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestGetOTPCryptoGeneratorConfigWithDefault(t *testing.T) {
tests := []struct {
name string
instanceID string
secretGeneratorSettingsRepo func(ctrl *gomock.Controller) domain.SecretGeneratorSettingsRepository
defaultConfig *crypto.GeneratorConfig
otpType domain.OTPType
want *crypto.GeneratorConfig
wantErr error
}{
{
name: "no default config",
instanceID: "instance-1",
wantErr: zerrors.ThrowInternal(nil, "DOM-3AcM0U", "missing default config"),
},
{
name: "failed to get secret generator settings",
instanceID: "instance-1",
secretGeneratorSettingsRepo: func(ctrl *gomock.Controller) domain.SecretGeneratorSettingsRepository {
repo := domainmock.NewSecretGeneratorSettingsRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(
repo.UniqueCondition("instance-1", nil, domain.SettingTypeSecretGenerator, domain.SettingStateActive),
),
),
).Times(1).
Return(nil, assert.AnError)
return repo
},
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeEmail,
wantErr: zerrors.ThrowInternal(assert.AnError, "DOM-x7Yd3E", "failed fetching SecretGeneratorSettings"),
},
{
name: "inactive secret generator settings",
instanceID: "instance-1",
secretGeneratorSettingsRepo: secretGeneratorSettingsRepo(domain.SettingStatePreview, domain.OTPTypeSMS),
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeSMS,
want: &crypto.GeneratorConfig{
Length: 6,
},
},
{
name: "active secret generator settings - type sms",
instanceID: "instance-1",
secretGeneratorSettingsRepo: secretGeneratorSettingsRepo(domain.SettingStateActive, domain.OTPTypeSMS),
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeSMS,
want: &crypto.GeneratorConfig{
Expiry: 30 * time.Minute,
Length: 6,
IncludeLowerLetters: true,
IncludeUpperLetters: false,
IncludeDigits: true,
IncludeSymbols: false,
},
},
{
name: "active secret generator settings - type email",
instanceID: "instance-1",
secretGeneratorSettingsRepo: secretGeneratorSettingsRepo(domain.SettingStateActive, domain.OTPTypeEmail),
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeEmail,
want: &crypto.GeneratorConfig{
Expiry: 30 * time.Minute,
Length: 8,
IncludeLowerLetters: true,
IncludeUpperLetters: false,
IncludeDigits: true,
IncludeSymbols: false,
},
},
{
name: "active secret generator settings sms type with nil values - return default config",
instanceID: "instance-1",
secretGeneratorSettingsRepo: func(ctrl *gomock.Controller) domain.SecretGeneratorSettingsRepository {
repo := domainmock.NewSecretGeneratorSettingsRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(
repo.UniqueCondition("instance-1", nil, domain.SettingTypeSecretGenerator, domain.SettingStateActive),
),
),
).Times(1).
Return(&domain.SecretGeneratorSettings{
Settings: domain.Settings{
State: domain.SettingStateActive,
},
}, nil)
return repo
},
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeSMS,
want: &crypto.GeneratorConfig{
Length: 6,
},
},
{
name: "active secret generator settings email type with nil values - return default config",
instanceID: "instance-1",
secretGeneratorSettingsRepo: func(ctrl *gomock.Controller) domain.SecretGeneratorSettingsRepository {
repo := domainmock.NewSecretGeneratorSettingsRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(
repo.UniqueCondition("instance-1", nil, domain.SettingTypeSecretGenerator, domain.SettingStateActive),
),
),
).Times(1).
Return(&domain.SecretGeneratorSettings{
Settings: domain.Settings{
State: domain.SettingStateActive,
},
}, nil)
return repo
},
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
},
otpType: domain.OTPTypeEmail,
want: &crypto.GeneratorConfig{
Length: 6,
},
},
{
name: "active secret generator settings with nil field values - return default config",
instanceID: "instance-1",
secretGeneratorSettingsRepo: func(ctrl *gomock.Controller) domain.SecretGeneratorSettingsRepository {
repo := domainmock.NewSecretGeneratorSettingsRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(),
dbmock.QueryOptions(
database.WithCondition(
repo.UniqueCondition("instance-1", nil, domain.SettingTypeSecretGenerator, domain.SettingStateActive),
),
),
).Times(1).
Return(&domain.SecretGeneratorSettings{
Settings: domain.Settings{
State: domain.SettingStateActive,
},
SecretGeneratorSettingsAttributes: domain.SecretGeneratorSettingsAttributes{
OTPEmail: &domain.OTPEmailAttributes{
SecretGeneratorAttrsWithExpiry: domain.SecretGeneratorAttrsWithExpiry{
Expiry: gu.Ptr(30 * time.Minute),
SecretGeneratorAttrs: domain.SecretGeneratorAttrs{
Length: nil, // should use value from the default config
IncludeLowerLetters: nil, // should use value from the default config
IncludeUpperLetters: gu.Ptr(false),
IncludeDigits: gu.Ptr(true),
IncludeSymbols: gu.Ptr(false),
},
},
},
},
}, nil)
return repo
},
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
IncludeUpperLetters: true,
IncludeLowerLetters: true,
},
otpType: domain.OTPTypeEmail,
want: &crypto.GeneratorConfig{
Expiry: 30 * time.Minute,
Length: 6, // from the default config
IncludeLowerLetters: true, // from the default config
IncludeUpperLetters: false,
IncludeDigits: true,
IncludeSymbols: false,
},
},
{
name: "invalid otp type",
instanceID: "instance-1",
otpType: 3,
defaultConfig: &crypto.GeneratorConfig{
Length: 6,
IncludeUpperLetters: true,
IncludeLowerLetters: true,
},
secretGeneratorSettingsRepo: secretGeneratorSettingsRepo(domain.SettingStateActive, domain.OTPTypeEmail),
wantErr: zerrors.ThrowInternal(nil, "DOM-3AcM0U", "invalid otp type"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
if tt.secretGeneratorSettingsRepo != nil {
domain.WithSecretGeneratorSettingsRepo(tt.secretGeneratorSettingsRepo(ctrl))(opts)
}
got, err := domain.GetOTPCryptoGeneratorConfigWithDefault(context.Background(), tt.instanceID, opts, tt.defaultConfig, tt.otpType)
assert.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}