mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-14 20:08:02 +00:00
8537805ea5
# Which Problems Are Solved The current handling of notification follows the same pattern as all other projections: Created events are handled sequentially (based on "position") by a handler. During the process, a lot of information is aggregated (user, texts, templates, ...). This leads to back pressure on the projection since the handling of events might take longer than the time before a new event (to be handled) is created. # How the Problems Are Solved - The current user notification handler creates separate notification events based on the user / session events. - These events contain all the present and required information including the userID. - These notification events get processed by notification workers, which gather the necessary information (recipient address, texts, templates) to send out these notifications. - If a notification fails, a retry event is created based on the current notification request including the current state of the user (this prevents race conditions, where a user is changed in the meantime and the notification already gets the new state). - The retry event will be handled after a backoff delay. This delay increases with every attempt. - If the configured amount of attempts is reached or the message expired (based on config), a cancel event is created, letting the workers know, the notification must no longer be handled. - In case of successful send, a sent event is created for the notification aggregate and the existing "sent" events for the user / session object is stored. - The following is added to the defaults.yaml to allow configuration of the notification workers: ```yaml Notifications: # The amount of workers processing the notification request events. # If set to 0, no notification request events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. Workers: 1 # ZITADEL_NOTIFIACATIONS_WORKERS # The amount of events a single worker will process in a run. BulkLimit: 10 # ZITADEL_NOTIFIACATIONS_BULKLIMIT # Time interval between scheduled notifications for request events RequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_REQUEUEEVERY # The amount of workers processing the notification retry events. # If set to 0, no notification retry events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. RetryWorkers: 1 # ZITADEL_NOTIFIACATIONS_RETRYWORKERS # Time interval between scheduled notifications for retry events RetryRequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_RETRYREQUEUEEVERY # Only instances are projected, for which at least a projection-relevant event exists within the timeframe # from HandleActiveInstances duration in the past until the projection's current time # If set to 0 (default), every instance is always considered active HandleActiveInstances: 0s # ZITADEL_NOTIFIACATIONS_HANDLEACTIVEINSTANCES # The maximum duration a transaction remains open # before it spots left folding additional events # and updates the table. TransactionDuration: 1m # ZITADEL_NOTIFIACATIONS_TRANSACTIONDURATION # Automatically cancel the notification after the amount of failed attempts MaxAttempts: 3 # ZITADEL_NOTIFIACATIONS_MAXATTEMPTS # Automatically cancel the notification if it cannot be handled within a specific time MaxTtl: 5m # ZITADEL_NOTIFIACATIONS_MAXTTL # Failed attempts are retried after a confogired delay (with exponential backoff). # Set a minimum and maximum delay and a factor for the backoff MinRetryDelay: 1s # ZITADEL_NOTIFIACATIONS_MINRETRYDELAY MaxRetryDelay: 20s # ZITADEL_NOTIFIACATIONS_MAXRETRYDELAY # Any factor below 1 will be set to 1 RetryDelayFactor: 1.5 # ZITADEL_NOTIFIACATIONS_RETRYDELAYFACTOR ``` # Additional Changes None # Additional Context - closes #8931
1854 lines
69 KiB
Go
1854 lines
69 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/mock/gomock"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
|
es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
|
"github.com/zitadel/zitadel/internal/notification/channels/email"
|
|
"github.com/zitadel/zitadel/internal/notification/channels/set"
|
|
"github.com/zitadel/zitadel/internal/notification/channels/sms"
|
|
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
|
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
|
|
"github.com/zitadel/zitadel/internal/notification/messages"
|
|
"github.com/zitadel/zitadel/internal/notification/senders"
|
|
"github.com/zitadel/zitadel/internal/notification/types"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
"github.com/zitadel/zitadel/internal/repository/session"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
)
|
|
|
|
const (
|
|
orgID = "org1"
|
|
policyID = "policy1"
|
|
userID = "user1"
|
|
codeID = "event1"
|
|
logoURL = "logo.png"
|
|
instanceID = "instanceID"
|
|
sessionID = "sessionID"
|
|
eventOrigin = "https://triggered.here"
|
|
eventOriginDomain = "triggered.here"
|
|
assetsPath = "/assets/v1"
|
|
preferredLoginName = "loginName1"
|
|
lastEmail = "last@email.com"
|
|
verifiedEmail = "verified@email.com"
|
|
lastPhone = "+41797654321"
|
|
verifiedPhone = "+41791234567"
|
|
instancePrimaryDomain = "primary.domain"
|
|
externalDomain = "external.domain"
|
|
externalPort = 3000
|
|
externalSecure = false
|
|
externalProtocol = "http"
|
|
defaultOTPEmailTemplate = "/otp/verify?loginName={{.LoginName}}&code={{.Code}}"
|
|
authRequestID = "authRequestID"
|
|
smsProviderID = "smsProviderID"
|
|
emailProviderID = "emailProviderID"
|
|
verificationID = "verificationID"
|
|
)
|
|
|
|
func Test_userNotifier_reduceInitCodeAdded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s",
|
|
eventOrigin, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInitialCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InitCodeMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInitialCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInitialCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s",
|
|
externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInitialCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InitCodeMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInitialCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInitialCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInitCodeAdded(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s",
|
|
eventOrigin, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanEmailCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.VerifyEmailMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanEmailCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanEmailCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s",
|
|
externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanEmailCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.VerifyEmailMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanEmailCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanEmailCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanEmailCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanEmailCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: true,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceEmailCodeAdded(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s",
|
|
eventOrigin, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanPasswordCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordResetMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "asset url without event trigger url",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s",
|
|
externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanPasswordCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordResetMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "external code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s",
|
|
eventOrigin, userID, orgID, authRequestID),
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: user.HumanPasswordCodeAddedType,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
MessageType: domain.PasswordResetMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{AuthRequestID: authRequestID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordCodeAddedType,
|
|
}),
|
|
Code: nil,
|
|
Expiry: 0,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
GeneratorID: smsProviderID,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordCodeAddedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: 1 * time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: true,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
GeneratorID: smsProviderID,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordCodeAdded(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reduceDomainClaimed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/login?orgID=%s",
|
|
eventOrigin, orgID),
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: user.UserDomainClaimedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.DomainClaimedMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{TempUsername: "newUsername"},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: true,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.DomainClaimedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.UserDomainClaimedType,
|
|
}),
|
|
TriggeredAtOrigin: eventOrigin,
|
|
UserName: "newUsername",
|
|
},
|
|
}, w
|
|
},
|
|
}, {
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login?orgID=%s",
|
|
externalProtocol, instancePrimaryDomain, externalPort, orgID),
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: user.UserDomainClaimedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.DomainClaimedMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{TempUsername: "newUsername"},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: true,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.DomainClaimedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.UserDomainClaimedType,
|
|
}),
|
|
UserName: "newUsername",
|
|
},
|
|
}, w
|
|
},
|
|
}}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceDomainClaimed(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", eventOrigin, userID, orgID, codeID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanPasswordlessInitCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordlessRegistrationMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{CodeID: codeID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordlessInitCodeRequestedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordlessInitCodeAddedType,
|
|
}),
|
|
ID: codeID,
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanPasswordlessInitCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordlessRegistrationMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{CodeID: codeID},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordlessInitCodeRequestedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordlessInitCodeAddedType,
|
|
}),
|
|
ID: codeID,
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordlessInitCodeRequestedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordlessInitCodeAddedType,
|
|
}),
|
|
ID: codeID,
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: true,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordlessCodeRequested(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reducePasswordChanged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{
|
|
PasswordChange: true,
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/console?login_hint={{.PreferredLoginName}}", eventOrigin),
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: user.HumanPasswordChangedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordChangeMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: nil,
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordChangedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordChangedType,
|
|
}),
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{
|
|
PasswordChange: true,
|
|
}, nil)
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/console?login_hint={{.PreferredLoginName}}",
|
|
externalProtocol, instancePrimaryDomain, externalPort),
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: user.HumanPasswordChangedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.PasswordChangeMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: nil,
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordChangedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordChangedType,
|
|
}),
|
|
},
|
|
}, w
|
|
},
|
|
}, {
|
|
name: "no notification",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{
|
|
PasswordChange: false,
|
|
}, nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanPasswordChangedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanPasswordChangedType,
|
|
}),
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordChanged(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "url with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/otp/verify?loginName={{.LoginName}}&code={{.Code}}", eventOrigin),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: session.OTPEmailChallengedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.VerifyEmailOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: eventOriginDomain,
|
|
Expiry: 1 * time.Hour,
|
|
Origin: eventOrigin,
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPEmailChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPEmailChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTmpl: "",
|
|
ReturnCode: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/otp/verify?loginName={{.LoginName}}&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: session.OTPEmailChallengedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.VerifyEmailOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: instancePrimaryDomain,
|
|
Expiry: 1 * time.Hour,
|
|
Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPEmailChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPEmailChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
ReturnCode: false,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPEmailChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPEmailChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTmpl: "",
|
|
ReturnCode: true,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "url template",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: "/verify-otp?sessionID={{.SessionID}}",
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: session.OTPEmailChallengedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.VerifyEmailOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: eventOriginDomain,
|
|
Expiry: 1 * time.Hour,
|
|
Origin: eventOrigin,
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPEmailChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPEmailChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTmpl: "/verify-otp?sessionID={{.SessionID}}",
|
|
ReturnCode: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
testCode := "testcode"
|
|
_, code := cryptoValue(t, ctrl, testCode)
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: "",
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: session.OTPSMSChallengedType,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
MessageType: domain.VerifySMSOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: eventOriginDomain,
|
|
Expiry: 1 * time.Hour,
|
|
Origin: eventOrigin,
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPSMSChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPSMSChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: 1 * time.Hour,
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
testCode := "testcode"
|
|
_, code := cryptoValue(t, ctrl, testCode)
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: "",
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: session.OTPSMSChallengedType,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
MessageType: domain.VerifySMSOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: instancePrimaryDomain,
|
|
Expiry: 1 * time.Hour,
|
|
Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPSMSChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPSMSChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: 1 * time.Hour,
|
|
CodeReturned: false,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "external code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{
|
|
ID: sessionID,
|
|
ResourceOwner: instanceID,
|
|
UserFactor: query.SessionUserFactor{
|
|
UserID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: "",
|
|
Code: nil,
|
|
CodeExpiry: 0,
|
|
EventType: session.OTPSMSChallengedType,
|
|
NotificationType: domain.NotificationTypeSms,
|
|
MessageType: domain.VerifySMSOTPMessageType,
|
|
UnverifiedNotificationChannel: false,
|
|
Args: &domain.NotificationArguments{
|
|
Domain: eventOriginDomain,
|
|
Expiry: 0 * time.Hour,
|
|
Origin: eventOrigin,
|
|
SessionID: sessionID,
|
|
},
|
|
AggregateID: sessionID,
|
|
AggregateResourceOwner: instanceID,
|
|
IsOTP: true,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPSMSChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPSMSChallengedType,
|
|
}),
|
|
Code: nil,
|
|
Expiry: 0,
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &session.OTPSMSChallengedEvent{
|
|
BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: sessionID,
|
|
ResourceOwner: sql.NullString{String: instanceID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: session.OTPSMSChallengedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: 1 * time.Hour,
|
|
CodeReturned: true,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPSMSChallenged(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want)
|
|
}{
|
|
{
|
|
name: "with event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInviteCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InviteUserMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{
|
|
ApplicationName: "ZITADEL",
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInviteCodeAddedEvent{
|
|
BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInviteCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "without event trigger",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testCode")
|
|
queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{
|
|
Domains: []*query.InstanceDomain{{
|
|
Domain: instancePrimaryDomain,
|
|
IsPrimary: true,
|
|
}},
|
|
}, nil)
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort),
|
|
URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInviteCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InviteUserMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{
|
|
ApplicationName: "ZITADEL",
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInviteCodeAddedEvent{
|
|
BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInviteCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "return code",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
w.noOperation = true
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInviteCodeAddedEvent{
|
|
BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInviteCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: true,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "url template",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: "/passwordless-init?userID={{.UserID}}",
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInviteCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InviteUserMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{
|
|
ApplicationName: "ZITADEL",
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInviteCodeAddedEvent{
|
|
BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInviteCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "/passwordless-init?userID={{.UserID}}",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
{
|
|
name: "application name",
|
|
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) {
|
|
_, code := cryptoValue(t, ctrl, "testcode")
|
|
commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{
|
|
UserID: userID,
|
|
UserResourceOwner: orgID,
|
|
TriggerOrigin: eventOrigin,
|
|
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
|
Code: code,
|
|
CodeExpiry: time.Hour,
|
|
EventType: user.HumanInviteCodeAddedType,
|
|
NotificationType: domain.NotificationTypeEmail,
|
|
MessageType: domain.InviteUserMessageType,
|
|
UnverifiedNotificationChannel: true,
|
|
Args: &domain.NotificationArguments{
|
|
ApplicationName: "APP",
|
|
AuthRequestID: authRequestID,
|
|
},
|
|
AggregateID: "",
|
|
AggregateResourceOwner: "",
|
|
IsOTP: false,
|
|
RequiresPreviousDomain: false,
|
|
}).Return(nil)
|
|
return fields{
|
|
queries: queries,
|
|
commands: commands,
|
|
es: eventstore.NewEventstore(&eventstore.Config{
|
|
Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier,
|
|
}),
|
|
}, args{
|
|
event: &user.HumanInviteCodeAddedEvent{
|
|
BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{
|
|
InstanceID: instanceID,
|
|
AggregateID: userID,
|
|
ResourceOwner: sql.NullString{String: orgID},
|
|
CreationDate: time.Now().UTC(),
|
|
Typ: user.HumanInviteCodeAddedType,
|
|
}),
|
|
Code: code,
|
|
Expiry: time.Hour,
|
|
URLTemplate: "",
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: eventOrigin,
|
|
AuthRequestID: authRequestID,
|
|
ApplicationName: "APP",
|
|
},
|
|
}, w
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
queries := mock.NewMockQueries(ctrl)
|
|
commands := mock.NewMockCommands(ctrl)
|
|
f, a, w := tt.test(ctrl, queries, commands)
|
|
stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInviteCodeAdded(a.event)
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
if w.noOperation {
|
|
assert.Nil(t, stmt.Execute)
|
|
return
|
|
}
|
|
err = stmt.Execute(nil, "")
|
|
if w.err != nil {
|
|
w.err(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fields struct {
|
|
queries *mock.MockQueries
|
|
commands *mock.MockCommands
|
|
es *eventstore.Eventstore
|
|
userDataCrypto crypto.EncryptionAlgorithm
|
|
SMSTokenCrypto crypto.EncryptionAlgorithm
|
|
}
|
|
type fieldsWorker struct {
|
|
queries *mock.MockQueries
|
|
commands *mock.MockCommands
|
|
es *eventstore.Eventstore
|
|
userDataCrypto crypto.EncryptionAlgorithm
|
|
SMSTokenCrypto crypto.EncryptionAlgorithm
|
|
now nowFunc
|
|
backOff func(current time.Duration) time.Duration
|
|
maxAttempts uint8
|
|
}
|
|
type args struct {
|
|
event eventstore.Event
|
|
}
|
|
type argsWorker struct {
|
|
event eventstore.Event
|
|
}
|
|
type want struct {
|
|
noOperation bool
|
|
err assert.ErrorAssertionFunc
|
|
}
|
|
type wantWorker struct {
|
|
message *messages.Email
|
|
messageSMS *messages.SMS
|
|
sendError error
|
|
err assert.ErrorAssertionFunc
|
|
}
|
|
|
|
func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier {
|
|
queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil)
|
|
smtpAlg, _ := cryptoValue(t, ctrl, "smtppw")
|
|
return &userNotifier{
|
|
commands: f.commands,
|
|
queries: NewNotificationQueries(
|
|
f.queries,
|
|
f.es,
|
|
externalDomain,
|
|
externalPort,
|
|
externalSecure,
|
|
"",
|
|
f.userDataCrypto,
|
|
smtpAlg,
|
|
f.SMSTokenCrypto,
|
|
),
|
|
otpEmailTmpl: defaultOTPEmailTemplate,
|
|
}
|
|
}
|
|
|
|
var _ types.ChannelChains = (*notificationChannels)(nil)
|
|
|
|
type notificationChannels struct {
|
|
senders.Chain
|
|
EmailConfig *email.Config
|
|
SMSConfig *sms.Config
|
|
}
|
|
|
|
func (c *notificationChannels) Email(context.Context) (*senders.Chain, *email.Config, error) {
|
|
return &c.Chain, c.EmailConfig, nil
|
|
}
|
|
|
|
func (c *notificationChannels) SMS(context.Context) (*senders.Chain, *sms.Config, error) {
|
|
return &c.Chain, c.SMSConfig, nil
|
|
}
|
|
|
|
func (c *notificationChannels) Webhook(context.Context, webhook.Config) (*senders.Chain, error) {
|
|
return &c.Chain, nil
|
|
}
|
|
|
|
func (c *notificationChannels) SecurityTokenEvent(context.Context, set.Config) (*senders.Chain, error) {
|
|
return &c.Chain, nil
|
|
}
|
|
|
|
func expectTemplateQueries(queries *mock.MockQueries, template string) {
|
|
queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{
|
|
AllowedLanguages: []language.Tag{language.English},
|
|
}, nil)
|
|
queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{
|
|
ID: policyID,
|
|
Light: query.Theme{
|
|
LogoURL: logoURL,
|
|
},
|
|
}, nil)
|
|
queries.EXPECT().MailTemplateByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.MailTemplate{Template: []byte(template)}, nil)
|
|
queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English)
|
|
queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil)
|
|
}
|
|
|
|
func expectTemplateWithNotifyUserQueries(queries *mock.MockQueries, template string) {
|
|
queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{
|
|
ID: userID,
|
|
ResourceOwner: orgID,
|
|
LastEmail: lastEmail,
|
|
VerifiedEmail: verifiedEmail,
|
|
PreferredLoginName: preferredLoginName,
|
|
LastPhone: lastPhone,
|
|
VerifiedPhone: verifiedPhone,
|
|
}, nil)
|
|
expectTemplateQueries(queries, template)
|
|
}
|
|
|
|
func expectTemplateWithNotifyUserQueriesSMS(queries *mock.MockQueries) {
|
|
queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{
|
|
ID: userID,
|
|
ResourceOwner: orgID,
|
|
LastEmail: lastEmail,
|
|
VerifiedEmail: verifiedEmail,
|
|
PreferredLoginName: preferredLoginName,
|
|
LastPhone: lastPhone,
|
|
VerifiedPhone: verifiedPhone,
|
|
}, nil)
|
|
queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{
|
|
AllowedLanguages: []language.Tag{language.English},
|
|
}, nil)
|
|
queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{
|
|
ID: policyID,
|
|
Light: query.Theme{
|
|
LogoURL: logoURL,
|
|
},
|
|
}, nil)
|
|
queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English)
|
|
queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil)
|
|
}
|
|
|
|
func cryptoValue(t *testing.T, ctrl *gomock.Controller, value string) (*crypto.MockEncryptionAlgorithm, *crypto.CryptoValue) {
|
|
encAlg := crypto.NewMockEncryptionAlgorithm(ctrl)
|
|
encAlg.EXPECT().Algorithm().AnyTimes().Return("enc")
|
|
encAlg.EXPECT().EncryptionKeyID().AnyTimes().Return("id")
|
|
encAlg.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"})
|
|
encAlg.EXPECT().DecryptString(gomock.Any(), gomock.Any()).AnyTimes().Return(value, nil)
|
|
encAlg.EXPECT().Encrypt(gomock.Any()).AnyTimes().Return(make([]byte, 0), nil)
|
|
code, err := crypto.Encrypt(nil, encAlg)
|
|
assert.NoError(t, err)
|
|
return encAlg, code
|
|
}
|