import gql from 'graphql-tag'; import * as randomatic from 'randomatic'; import { signUp, execute } from 'test/graphql'; import { User } from '@/entities'; jest.mock('randomatic'); const email = 'user@example.com'; const username = 'test_user'; const password = 'K$D4@i$HbkNNDmm!'; // must use a secure password afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers(); }); const verifyMutation = gql` mutation ($input: VerifyOneTimePasswordInput!) { verifyOneTimePassword(input: $input) { success } } `; describe('Mutation:verifyOneTimePassword', () => { test('can verify properly', async () => { const testCode = '123456'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return testCode; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me?.passwordAuth?.isVerified).toBe(false); const { data, errors } = await execute( { query: verifyMutation, variables: { input: { otpAttempt: testCode, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(errors).toBeFalsy(); expect(data?.verifyOneTimePassword?.success).toBe(true); const meAfterVerify = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(meAfterVerify).toBeDefined(); expect(meAfterVerify.passwordAuth.isVerified).toBe(true); }); test('cannot verify if not signed in', async () => { const testCode = '123456'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return testCode; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); const { errors } = await execute({ query: verifyMutation, variables: { input: { otpAttempt: testCode, }, }, }); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"Cannot verify one time password if not signed in"` ); }); test('cannot verify if code has expired', async () => { const testCode = '123456'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return testCode; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); // fast forward in time 1000 days jest .spyOn(Date, 'now') .mockImplementation( () => new Date().getTime() + 1000 * 60 * 60 * 24 * 1000 ); const { errors } = await execute({ query: verifyMutation, variables: { input: { otpAttempt: testCode, }, }, }); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"Cannot verify one time password if not signed in"` ); jest.useRealTimers(); }); it('will not verify with wrong code', async () => { const testCode = '123456'; const wrongCode = '555555'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return testCode; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); const { errors } = await execute( { query: verifyMutation, variables: { input: { otpAttempt: wrongCode, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"OTP attempt did not match known TOPTs"` ); const meAfterVerify = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(meAfterVerify).toBeDefined(); expect(meAfterVerify.passwordAuth.isVerified).toBe(false); }); it('can not verify if already verified', async () => { const testCode = '123456'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementation(() => { return testCode; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); await execute( { query: verifyMutation, variables: { input: { otpAttempt: testCode, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); // second verify attempt const { errors } = await execute( { query: verifyMutation, variables: { input: { otpAttempt: testCode, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(errors).toHaveLength(1); expect(errors?.[0]?.message).toMatchInlineSnapshot( `"No need to verify if already verified."` ); const meAfterVerify = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(meAfterVerify).toBeDefined(); expect(meAfterVerify.passwordAuth.isVerified).toBe(true); }); }); describe('Mutation:requestOneTimePassword', () => { const query = gql` mutation { requestOneTimePassword { success } } `; it('can request additional codes and first code is usable', async () => { const testCode1 = '123456'; const testCode2 = '654321'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementationOnce(() => { return testCode1; }) .mockImplementationOnce(() => { return testCode2; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); const { data, errors } = await execute( { query, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(errors).toBeFalsy(); const meAfterRequestAdditional = await User.findOne( signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], } ); expect(meAfterRequestAdditional).toBeDefined(); expect(meAfterRequestAdditional.passwordAuth.isVerified).toBe(false); expect(data?.requestOneTimePassword?.success).toBe(true); expect(randomNumberMock).toHaveBeenCalledTimes(2); const { data: verifyData, errors: verifyErrors } = await execute( { query: verifyMutation, variables: { input: { otpAttempt: testCode1, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(verifyErrors).toBeFalsy(); const meAfterVerify = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(meAfterVerify).toBeDefined(); expect(meAfterVerify.passwordAuth.isVerified).toBe(true); expect(verifyData?.verifyOneTimePassword?.success).toBe(true); }); it('can request additional codes and second code is usable', async () => { const testCode1 = '123456'; const testCode2 = '654321'; const randomNumberMock = jest .spyOn(randomatic, 'default') .mockImplementationOnce(() => { return testCode1; }) .mockImplementationOnce(() => { return testCode2; }); const { data: signUpData, errors: signUpErrors } = await signUp({ username, password, email, skipVerification: true, }); expect(randomNumberMock).toHaveBeenCalledTimes(1); expect(signUpErrors).toBeFalsy(); expect(signUpData?.signUp?.accessToken).toBeDefined(); const me = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(me).toBeDefined(); expect(me.passwordAuth.isVerified).toBe(false); const { data, errors } = await execute( { query, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(errors).toBeFalsy(); const meAfterRequestAdditional = await User.findOne( signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], } ); expect(meAfterRequestAdditional).toBeDefined(); expect(meAfterRequestAdditional.passwordAuth.isVerified).toBe(false); expect(data?.requestOneTimePassword?.success).toBe(true); expect(randomNumberMock).toHaveBeenCalledTimes(2); const { data: verifyData, errors: verifyErrors } = await execute( { query: verifyMutation, variables: { input: { otpAttempt: testCode2, }, }, }, { accessToken: signUpData?.signUp?.accessToken, } ); expect(verifyErrors).toBeFalsy(); const meAfterVerify = await User.findOne(signUpData?.signUp?.user?.id, { relations: ['passwordAuth'], }); expect(meAfterVerify).toBeDefined(); expect(meAfterVerify.passwordAuth.isVerified).toBe(true); expect(verifyData?.verifyOneTimePassword?.success).toBe(true); }); });