import gql from 'graphql-tag'; import { execute as executeWithoutAuth, signIn, signUp, startSession, } from 'test/graphql'; import * as randomatic from 'randomatic'; import { TransactionalEmailSender } from '../../services'; jest.mock('randomatic'); const email = 'test_user@example.com'; const username = 'test_user'; const password = 'K$D4@i$HbkNNDmm!'; // must use a secure password afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers(); }); const resetPasswordMutation = gql` mutation ($input: RequestResetPasswordInput!) { requestResetPassword(input: $input) { success } } `; describe('Mutation:requestResetPassword', () => { test('can request new password', async () => { const { user } = await startSession({ username, password, email }); const emailSendMock = jest.spyOn( TransactionalEmailSender.prototype, 'send' ); const resetPasswordKey = 'snjsq4khap7snmd9kysa8f9qrjaps7qq'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return resetPasswordKey; }); const { data, errors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); expect(randomNumberMock.mock.calls.length).toBeGreaterThan(0); // sent a reset password link email expect(emailSendMock).toHaveBeenCalledTimes(1); expect(emailSendMock.mock.calls[0][0]).toMatchObject({ to: (await user?.getPasswordAuth())?.email, subject: expect.stringMatching(/reset/gi), text: expect.stringContaining(resetPasswordKey), }); expect(errors).toBeFalsy(); expect(data?.requestResetPassword?.success).toBe(true); }); it('cannot request a new password when signed in', async () => { const { execute } = await startSession({ username, password, email }); const emailSendMock = jest.spyOn( TransactionalEmailSender.prototype, 'send' ); const { data, errors } = await execute({ query: resetPasswordMutation, variables: { input: { username, }, }, }); // sent a reset password link email expect(emailSendMock).toHaveBeenCalledTimes(0); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"Cannot request reset password if already signed in"` ); expect(data).toBeFalsy(); }); it('cannot request a new password for user that does not exist, but do not fail', async () => { const emailSendMock = jest.spyOn( TransactionalEmailSender.prototype, 'send' ); const { data, errors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username: 'otheruser', }, }, }); // sent a reset password link email expect(emailSendMock).toHaveBeenCalledTimes(0); expect(errors).toBeFalsy(); expect(data?.requestResetPassword?.success).toBe(true); }); }); describe('Mutation:changePasswordWithResetPasswordKey', () => { const changePasswordMutation = gql` mutation ($input: ChangePasswordWithResetPasswordKeyInput!) { changePasswordWithResetPasswordKey(input: $input) { success } } `; test('can change password to new password with reset key', async () => { const newPassword = 'AllW3HeaRi#RaDi0GooG))'; const { user } = await startSession({ username, password, email }); const resetPasswordKey = 'snjsq4khap7snmd9kysa8f9qrjaps7qq'; jest.spyOn(randomatic, 'default').mockImplementation(() => { return resetPasswordKey; }); const { data: resetPasswordData, errors: resetPasswordErrors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); expect(resetPasswordErrors).toBeFalsy(); expect(resetPasswordData?.requestResetPassword?.success).toBe(true); const { data, errors } = await executeWithoutAuth({ query: changePasswordMutation, variables: { input: { username, newPassword, resetPasswordKey, }, }, }); expect(errors).toBeFalsy(); expect(data?.changePasswordWithResetPasswordKey?.success).toBe(true); // can sign in with new pass const { data: signInNewPassData, errors: signInNewPassErrors } = await signIn({ username, password: newPassword, }); expect(signInNewPassErrors).toBeFalsy(); expect(signInNewPassData?.signIn?.user?.id).toBe(user.id); }); it('will not change password if using the wrong key', async () => { const newPassword = 'AllW3HeaRi#RaDi0GooG))'; await signUp({ username, password, email }); const resetPasswordKey = 'snjsq4khap7snmd9kysa8f9qrjaps7qq'; jest.spyOn(randomatic, 'default').mockImplementation(() => { return resetPasswordKey; }); const { data: resetPasswordData, errors: resetPasswordErrors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); expect(resetPasswordErrors).toBeFalsy(); expect(resetPasswordData?.requestResetPassword?.success).toBe(true); const { data, errors } = await executeWithoutAuth({ query: changePasswordMutation, variables: { input: { username, newPassword, resetPasswordKey: 'wrong key', }, }, }); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"Reset password key did not match known reset password requests"` ); expect(data).toBeFalsy(); // cannot sign in with new pass const { data: signInNewPassData, errors: signInNewPassErrors } = await signIn({ username, password: newPassword, }); expect(signInNewPassErrors).toHaveLength(1); expect(signInNewPassErrors?.[0]?.message).toMatchInlineSnapshot( `"Invalid Username/Password"` ); expect(signInNewPassData).toBeFalsy(); }); it('can request multiple keys and use original', async () => { const newPassword = 'AllW3HeaRi#RaDi0GooG))'; const { user } = await startSession({ username, password, email }); const resetPasswordKey = 'snjsq4khap7snmd9kysa8f9qrjaps7qq'; jest .spyOn(randomatic, 'default') .mockImplementationOnce(() => { return resetPasswordKey; }) .mockImplementationOnce(() => { return 'second key'; }); // first key requested const { data: resetPasswordData, errors: resetPasswordErrors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); // request another key await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); expect(resetPasswordErrors).toBeFalsy(); expect(resetPasswordData?.requestResetPassword?.success).toBe(true); const { data, errors } = await executeWithoutAuth({ query: changePasswordMutation, variables: { input: { username, newPassword, resetPasswordKey, }, }, }); expect(errors).toBeFalsy(); expect(data?.changePasswordWithResetPasswordKey?.success).toBe(true); // can sign in with new pass const { data: signInNewPassData, errors: signInNewPassErrors } = await signIn({ username, password: newPassword, }); expect(signInNewPassErrors).toBeFalsy(); expect(signInNewPassData?.signIn?.user?.id).toBe(user.id); }); it('cannot change password if signed in', async () => { const newPassword = 'AllW3HeaRi#RaDi0GooG))'; const { user, execute } = await startSession({ username, password, email }); const resetPasswordKey = 'snjsq4khap7snmd9kysa8f9qrjaps7qq'; jest.spyOn(randomatic, 'default').mockImplementation(() => { return resetPasswordKey; }); const { data: resetPasswordData, errors: resetPasswordErrors } = await executeWithoutAuth({ query: resetPasswordMutation, variables: { input: { username, }, }, }); expect(resetPasswordErrors).toBeFalsy(); expect(resetPasswordData?.requestResetPassword?.success).toBe(true); const { data, errors } = await execute({ query: changePasswordMutation, variables: { input: { username, newPassword, resetPasswordKey, }, }, }); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"Cannot change password with reset password key if signed in."` ); expect(data).toBeFalsy(); // can still sign in with old pass const { data: signInOldPassData, errors: signInOldPassErrors } = await signIn({ username, password, }); expect(signInOldPassErrors).toBeFalsy(); expect(signInOldPassData?.signIn?.user?.id).toBe(user.id); }); });