diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index a25b21c1d9..78b6b14fcf 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "net/http" "testing" "time" @@ -33,6 +34,7 @@ const ( logoURL = "logo.png" policyID = "policy1" userID = "user1" + loginName = "loginName1" orgID = "org1" eventOrigin = "https://triggered.here" assetsPath = "/assets/v1" @@ -44,41 +46,208 @@ const ( lastEmail = "last@email.com" ) -func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { - type fields struct { - queries *mock.MockQueries - commands *mock.MockCommands - es *eventstore.Eventstore - userDataCrypto crypto.EncryptionAlgorithm - SMTPPasswordCrypto crypto.EncryptionAlgorithm - SMSTokenCrypto crypto.EncryptionAlgorithm - } - type args struct { - event eventstore.Event - } - type want struct { - message messages.Email - err assert.ErrorAssertionFunc - } - queries := mock.NewMockQueries(gomock.NewController(t)) +func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { + expectMailSubject := "Initialize User" tests := []struct { name string - test func() (fields, args, want) + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) }{{ name: "asset url with event trigger url", - test: func() (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) w.message = messages.Email{ Recipients: []string{lastEmail}, - Subject: "Verify email", + Subject: expectMailSubject, Content: expectContent, } - codeAlg, code := cryptoValue(t, "testcode") - smtpAlg, _ := cryptoValue(t, "smtppw") + codeAlg, code := cryptoValue(t, ctrl, "testcode") + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") + queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(eventstore.TestConfig( + es_repo_mock.NewRepo(t).ExpectFilterEvents(), + )), + userDataCrypto: codeAlg, + SMTPPasswordCrypto: smtpAlg, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + TriggeredAtOrigin: eventOrigin, + }, + }, 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) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) + w.message = messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") + queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(eventstore.TestConfig( + es_repo_mock.NewRepo(t).ExpectFilterEvents(), + )), + userDataCrypto: codeAlg, + SMTPPasswordCrypto: smtpAlg, + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + }, + }, w + }, + }, { + name: "button url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.URL}}" + testCode := "testcode" + expectContent := fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname=%s&code=%s&orgID=%s&passwordset=%t", eventOrigin, userID, loginName, testCode, orgID, false) + w.message = messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") + queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(eventstore.TestConfig( + es_repo_mock.NewRepo(t).ExpectFilterEvents(), + )), + userDataCrypto: codeAlg, + SMTPPasswordCrypto: smtpAlg, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, { + name: "button url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.URL}}" + testCode := "testcode" + expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname=%s&code=%s&orgID=%s&passwordset=%t", externalProtocol, instancePrimaryDomain, externalPort, userID, loginName, testCode, orgID, false) + w.message = messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") + queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(eventstore.TestConfig( + es_repo_mock.NewRepo(t).ExpectFilterEvents(), + )), + userDataCrypto: codeAlg, + SMTPPasswordCrypto: smtpAlg, + SMSTokenCrypto: nil, + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + }), + Code: code, + Expiry: time.Hour, + }, + }, w + }, + }} + fs, err := statik_fs.NewWithNamespace("notification") + assert.NoError(t, err) + 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) + _, err = newUserNotifier(ctrl, fs, f, a, w).reduceInitCodeAdded(a.event) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { + expectMailSubject := "Verify email" + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + }{{ + name: "asset url with event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = messages.Email{ + Recipients: []string{lastEmail}, + Subject: expectMailSubject, + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) expectTemplateQueries(queries, givenTemplate) - commands := mock.NewMockCommands(gomock.NewController(t)) commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) return fields{ queries: queries, @@ -106,16 +275,16 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, }, { name: "asset url without event trigger url", - test: func() (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.LogoURL}}" expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) w.message = messages.Email{ Recipients: []string{lastEmail}, - Subject: "Verify email", + Subject: expectMailSubject, Content: expectContent, } - codeAlg, code := cryptoValue(t, "testcode") - smtpAlg, _ := cryptoValue(t, "smtppw") + codeAlg, code := cryptoValue(t, ctrl, "testcode") + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -124,7 +293,6 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }}, }, nil) expectTemplateQueries(queries, givenTemplate) - commands := mock.NewMockCommands(gomock.NewController(t)) commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) return fields{ queries: queries, @@ -150,20 +318,19 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, }, { name: "button url with event trigger url", - test: func() (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code=%s&orgID=%s", eventOrigin, userID, testCode, orgID) w.message = messages.Email{ Recipients: []string{lastEmail}, - Subject: "Verify email", + Subject: expectMailSubject, Content: expectContent, } - codeAlg, code := cryptoValue(t, testCode) - smtpAlg, _ := cryptoValue(t, "smtppw") + codeAlg, code := cryptoValue(t, ctrl, testCode) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) expectTemplateQueries(queries, givenTemplate) - commands := mock.NewMockCommands(gomock.NewController(t)) commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) return fields{ queries: queries, @@ -191,17 +358,17 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, }, { name: "button url without event trigger url", - test: func() (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.URL}}" testCode := "testcode" expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code=%s&orgID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, testCode, orgID) w.message = messages.Email{ Recipients: []string{lastEmail}, - Subject: "Verify email", + Subject: expectMailSubject, Content: expectContent, } - codeAlg, code := cryptoValue(t, testCode) - smtpAlg, _ := cryptoValue(t, "smtppw") + codeAlg, code := cryptoValue(t, ctrl, testCode) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ @@ -210,7 +377,6 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }}, }, nil) expectTemplateQueries(queries, givenTemplate) - commands := mock.NewMockCommands(gomock.NewController(t)) commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) return fields{ queries: queries, @@ -237,21 +403,20 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, }, { name: "button url with url template and event trigger url", - test: func() (f fields, a args, w want) { + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { givenTemplate := "{{.URL}}" urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" testCode := "testcode" expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) w.message = messages.Email{ Recipients: []string{lastEmail}, - Subject: "Verify email", + Subject: expectMailSubject, Content: expectContent, } - codeAlg, code := cryptoValue(t, testCode) - smtpAlg, _ := cryptoValue(t, "smtppw") + codeAlg, code := cryptoValue(t, ctrl, testCode) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) expectTemplateQueries(queries, givenTemplate) - commands := mock.NewMockCommands(gomock.NewController(t)) commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) return fields{ queries: queries, @@ -278,33 +443,15 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, w }, }} + fs, err := statik_fs.NewWithNamespace("notification") + assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) - f, a, w := tt.test() - channel := channel_mock.NewMockNotificationChannel(gomock.NewController(t)) - if w.err == nil { - w.message.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(&w.message).Return(nil) - } - u := &userNotifier{ - commands: f.commands, - queries: NewNotificationQueries( - f.queries, - f.es, - externalDomain, - externalPort, - externalSecure, - "", - f.userDataCrypto, - f.SMTPPasswordCrypto, - f.SMSTokenCrypto, - fs, - ), - channels: &channels{Chain: *senders.ChainChannels(channel)}, - } - _, err = u.reduceEmailCodeAdded(a.event) + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + commands := mock.NewMockCommands(ctrl) + f, a, w := tt.test(ctrl, queries, commands) + _, err = newUserNotifier(ctrl, fs, f, a, w).reduceEmailCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -314,6 +461,46 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { } } +type fields struct { + queries *mock.MockQueries + commands *mock.MockCommands + es *eventstore.Eventstore + userDataCrypto crypto.EncryptionAlgorithm + SMTPPasswordCrypto crypto.EncryptionAlgorithm + SMSTokenCrypto crypto.EncryptionAlgorithm +} +type args struct { + event eventstore.Event +} +type want struct { + message messages.Email + err assert.ErrorAssertionFunc +} + +func newUserNotifier(ctrl *gomock.Controller, fs http.FileSystem, f fields, a args, w want) *userNotifier { + channel := channel_mock.NewMockNotificationChannel(ctrl) + if w.err == nil { + w.message.TriggeringEvent = a.event + channel.EXPECT().HandleMessage(&w.message).Return(nil) + } + return &userNotifier{ + commands: f.commands, + queries: NewNotificationQueries( + f.queries, + f.es, + externalDomain, + externalPort, + externalSecure, + "", + f.userDataCrypto, + f.SMTPPasswordCrypto, + f.SMSTokenCrypto, + fs, + ), + channels: &channels{Chain: *senders.ChainChannels(channel)}, + } +} + var _ types.ChannelChains = (*channels)(nil) type channels struct { @@ -340,13 +527,18 @@ func expectTemplateQueries(queries *mock.MockQueries, template string) { }, }, nil) queries.EXPECT().MailTemplateByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.MailTemplate{Template: []byte(template)}, nil) - queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ID: userID, ResourceOwner: orgID, LastEmail: lastEmail}, nil) + queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + PreferredLoginName: loginName, + }, 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, value string) (*crypto.MockEncryptionAlgorithm, *crypto.CryptoValue) { - encAlg := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) +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"}) diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index 075fd55cac..21702eadcb 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -244,6 +244,7 @@ type HumanInitialCodeAddedEvent struct { eventstore.BaseEvent `json:"-"` Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` + TriggeredAtOrigin string `json:"base_url,omitempty"` } func (e *HumanInitialCodeAddedEvent) Data() interface{} { @@ -254,6 +255,10 @@ func (e *HumanInitialCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniq return nil } +func (e *HumanInitialCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewHumanInitialCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate,