package userimpl import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/notifications" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempusertest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" ) func TestVerifier_Start(t *testing.T) { ts := &tempusertest.FakeTempUserService{} us := &usertest.FakeUserService{} ns := notifications.MockNotificationService() is := &idtest.FakeService{} type calls struct { expireCalled bool createCalled bool updateCalled bool } verifier := ProvideVerifier(setting.NewCfg(), us, ts, ns, is) t.Run("should error if email already exist for other user", func(t *testing.T) { us.ExpectedUser = &user.User{ID: 1} err := verifier.Start(context.Background(), user.StartVerifyEmailCommand{ User: user.User{ID: 2}, Email: "some@email.com", Action: user.EmailUpdateAction, }) assert.ErrorIs(t, err, user.ErrEmailConflict) }) t.Run("should succeed when no user has the email", func(t *testing.T) { us.ExpectedUser = nil var c calls ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error { c.expireCalled = true return nil } ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) { c.createCalled = true return &tempuser.TempUser{ OrgID: cmd.OrgID, Email: cmd.Email, Name: cmd.Name, InvitedByUserID: cmd.InvitedByUserID, Code: cmd.Code, }, nil } ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error { c.updateCalled = true return nil } err := verifier.Start(context.Background(), user.StartVerifyEmailCommand{ User: user.User{ID: 2}, Email: "some@email.com", Action: user.EmailUpdateAction, }) assert.NoError(t, err) assert.True(t, c.expireCalled) assert.True(t, c.createCalled) assert.True(t, c.updateCalled) }) t.Run("should succeed when the user holding the email is the same user that want to verify it", func(t *testing.T) { us.ExpectedUser = &user.User{ID: 2} var c calls ts.ExpirePreviousVerificationsFN = func(ctx context.Context, cmd *tempuser.ExpirePreviousVerificationsCommand) error { c.expireCalled = true return nil } ts.CreateTempUserFN = func(ctx context.Context, cmd *tempuser.CreateTempUserCommand) (*tempuser.TempUser, error) { c.createCalled = true return &tempuser.TempUser{ OrgID: cmd.OrgID, Email: cmd.Email, Name: cmd.Name, InvitedByUserID: cmd.InvitedByUserID, Code: cmd.Code, }, nil } ts.UpdateTempUserWithEmailSentFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserWithEmailSentCommand) error { c.updateCalled = true return nil } err := verifier.Start(context.Background(), user.StartVerifyEmailCommand{ User: user.User{ID: 2}, Email: "some@email.com", Action: user.EmailUpdateAction, }) assert.ErrorIs(t, err, nil) assert.True(t, c.expireCalled) assert.True(t, c.createCalled) assert.True(t, c.updateCalled) }) } func TestVerifier_Complete(t *testing.T) { ts := &tempusertest.FakeTempUserService{} us := &usertest.FakeUserService{} ns := notifications.MockNotificationService() is := &idtest.FakeService{} type calls struct { updateCalled bool updateStatusCalled bool removeTokenCalled bool } cfg := setting.NewCfg() cfg.VerificationEmailMaxLifetime = 1 * time.Hour verifier := ProvideVerifier(cfg, us, ts, ns, is) t.Run("should return error for invalid code", func(t *testing.T) { ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return nil, tempuser.ErrTempUserNotFound } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.ErrorIs(t, err, errInvalidCode) }) t.Run("should return error when verification has wrong status", func(t *testing.T) { ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateCompleted, }, nil } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.ErrorIs(t, err, errInvalidCode) }) t.Run("should return error when verification email was never sent", func(t *testing.T) { ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateStarted, EmailSent: false, }, nil } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.ErrorIs(t, err, errInvalidCode) }) t.Run("should return error when verification code has expired", func(t *testing.T) { ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateStarted, EmailSent: true, EmailSentOn: time.Now().Add(-10 * time.Hour), }, nil } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.ErrorIs(t, err, errExpiredCode) }) t.Run("should return error user connect to code don't exists", func(t *testing.T) { ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateStarted, EmailSent: true, EmailSentOn: time.Now(), }, nil } us.ExpectedError = user.ErrUserNotFound err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.ErrorIs(t, err, user.ErrUserNotFound) }) t.Run("should update user email on valid code", func(t *testing.T) { var c calls ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateStarted, Name: string(user.EmailUpdateAction), InvitedByID: 1, Email: "updated@email.com", EmailSent: true, EmailSentOn: time.Now(), }, nil } ts.UpdateTempUserStatusFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserStatusCommand) error { c.updateStatusCalled = true return nil } is.RemoveIDTokenFn = func(ctx context.Context, identity identity.Requester) error { c.removeTokenCalled = true return nil } us.ExpectedUser = &user.User{Email: "initial@email.com"} us.ExpectedError = nil us.UpdateFn = func(ctx context.Context, cmd *user.UpdateUserCommand) error { c.updateCalled = true assert.True(t, *cmd.EmailVerified) assert.Equal(t, int64(1), cmd.UserID) assert.Equal(t, "", cmd.Login) assert.Equal(t, "updated@email.com", cmd.Email) return nil } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.NoError(t, err) assert.True(t, c.updateCalled) assert.True(t, c.updateStatusCalled) assert.True(t, c.removeTokenCalled) }) t.Run("should update user email and login if login is an email on valid code", func(t *testing.T) { var c calls ts.GetTempUserByCodeFN = func(ctx context.Context, query *tempuser.GetTempUserByCodeQuery) (*tempuser.TempUserDTO, error) { return &tempuser.TempUserDTO{ Status: tempuser.TmpUserEmailUpdateStarted, Name: string(user.EmailUpdateAction), InvitedByID: 1, Email: "updated@email.com", EmailSent: true, EmailSentOn: time.Now(), }, nil } ts.UpdateTempUserStatusFN = func(ctx context.Context, cmd *tempuser.UpdateTempUserStatusCommand) error { c.updateStatusCalled = true return nil } is.RemoveIDTokenFn = func(ctx context.Context, identity identity.Requester) error { c.removeTokenCalled = true return nil } us.ExpectedUser = &user.User{Email: "initial@email.com", Login: "other@email.com"} us.ExpectedError = nil us.UpdateFn = func(ctx context.Context, cmd *user.UpdateUserCommand) error { c.updateCalled = true assert.True(t, *cmd.EmailVerified) assert.Equal(t, int64(1), cmd.UserID) assert.Equal(t, "updated@email.com", cmd.Email) assert.Equal(t, "updated@email.com", cmd.Login) return nil } err := verifier.Complete(context.Background(), user.CompleteEmailVerifyCommand{Code: "some-code"}) assert.NoError(t, err) assert.True(t, c.updateCalled) assert.True(t, c.updateStatusCalled) assert.True(t, c.removeTokenCalled) }) }