package clients import ( "context" "testing" "github.com/stretchr/testify/assert" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "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" "github.com/grafana/grafana/pkg/util" ) func TestPasswordless_StartPasswordless(t *testing.T) { type testCase struct { desc string email string findUser bool findTempUser bool blockLogin bool expectedErr error } tests := []testCase{ { desc: "should succeed if user is found", email: "user@domain.com", findUser: true, blockLogin: false, }, { desc: "should succeed if temp user is found", email: "user@domain.com", findUser: false, findTempUser: true, blockLogin: false, }, { desc: "should fail if user or temp user is not found", email: "user@domain.com", findUser: false, findTempUser: false, blockLogin: false, expectedErr: errPasswordlessClientInvalidEmail.Errorf("no user or invite found with email user@domain.com"), }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { hashed, _ := util.EncodePassword("password", "salt") userService := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 1, Email: "user@domain.com", Login: "user", Password: user.Password(hashed), Salt: "salt"}, } las := &loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin} tus := &tempusertest.FakeTempUserService{} tus.GetTempUsersQueryFN = func(ctx context.Context, query *tempuser.GetTempUsersQuery) ([]*tempuser.TempUserDTO, error) { return []*tempuser.TempUserDTO{{ ID: 1, Email: "user@domain.com", Status: tempuser.TmpUserInvitePending, EmailSent: true, }}, nil } ns := notifications.MockNotificationService() cache := remotecache.NewFakeCacheStorage() if !tt.findUser { userService.ExpectedUser = nil userService.ExpectedError = user.ErrUserNotFound } if !tt.findTempUser { tus.GetTempUsersQueryFN = func(ctx context.Context, query *tempuser.GetTempUsersQuery) ([]*tempuser.TempUserDTO, error) { return nil, tempuser.ErrTempUserNotFound } } c := ProvidePasswordless(setting.NewCfg(), las, userService, tus, ns, cache) _, err := c.startPasswordless(context.Background(), tt.email) assert.ErrorIs(t, err, tt.expectedErr) }) } } func TestPasswordless_AuthenticatePasswordless(t *testing.T) { type testCase struct { desc string email string findUser bool blockLogin bool expectedErr error expectedIdentity *authn.Identity } tests := []testCase{ { desc: "should successfully authenticate user with correct passwordless magic link", email: "user@domain.com", findUser: true, blockLogin: false, expectedIdentity: &authn.Identity{ ID: "1", Type: claims.TypeUser, OrgID: 1, AuthenticatedBy: login.PasswordlessAuthModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, }, }, { desc: "should fail if login is blocked", email: "user@domain.com", findUser: true, blockLogin: true, expectedErr: errPasswordlessClientTooManyLoginAttempts.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked"), }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { hashed, _ := util.EncodePassword("password", "salt") userService := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 1, Email: "user@domain.com", Login: "user", Password: user.Password(hashed), Salt: "salt"}, } las := &loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin} tus := &tempusertest.FakeTempUserService{} ns := notifications.MockNotificationService() cache := remotecache.NewFakeCacheStorage() if !tt.findUser { userService.ExpectedUser = nil userService.ExpectedError = user.ErrUserNotFound } c := ProvidePasswordless(setting.NewCfg(), las, userService, tus, ns, cache) code, err := c.startPasswordless(context.Background(), tt.email) if err != nil { t.Fatalf("failed to start passwordless: %v", err) } form := &PasswordlessForm{ Code: code, ConfirmationCode: ns.Email.Data["ConfirmationCode"].(string), Name: "user", Username: "username", } identity, err := c.authenticatePasswordless(context.Background(), &authn.Request{OrgID: 1}, *form) assert.ErrorIs(t, err, tt.expectedErr) assert.EqualValues(t, tt.expectedIdentity, identity) }) } }