diff --git a/api-js/src/user/mutations/__tests__/reset-password.test.js b/api-js/src/user/mutations/__tests__/reset-password.test.js index 87c965fc7c..84605bd905 100644 --- a/api-js/src/user/mutations/__tests__/reset-password.test.js +++ b/api-js/src/user/mutations/__tests__/reset-password.test.js @@ -129,7 +129,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -155,7 +163,9 @@ describe('reset users password', () => { const expectedResponse = { data: { resetPassword: { - status: 'Password was successfully reset.', + result: { + status: 'Password was successfully reset.', + }, }, }, } @@ -248,7 +258,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -299,7 +317,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -322,11 +348,18 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError('Unable to reset password. Please try again.'), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'Unable to reset password. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `When resetting password user attempted to verify account, but userKey is not located in the token parameters.`, ]) @@ -349,7 +382,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -372,11 +413,18 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError('Unable to reset password. Please try again.'), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'Unable to reset password. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `When resetting password user attempted to verify account, but userKey is not located in the token parameters.`, ]) @@ -399,7 +447,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -422,11 +478,18 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError('Unable to reset password. Please try again.'), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'Unable to reset password. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `A user attempted to reset the password for 1, however there is no associated account.`, ]) @@ -459,7 +522,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -482,11 +553,18 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError('Unable to reset password. Please try again.'), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'Unable to reset password. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset password, however the current password does not match the current hashed password in the db.`, ]) @@ -516,7 +594,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -539,11 +625,18 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError('New passwords do not match. Please try again.'), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'New passwords do not match. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset their password, however the submitted passwords do not match.`, ]) @@ -573,7 +666,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -596,13 +697,19 @@ describe('reset users password', () => { }, ) - const error = [ - new GraphQLError( - 'Password is not strong enough. Please try again.', - ), - ] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: + 'Password is not strong enough. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset their password, however the submitted password is not long enough.`, ]) @@ -636,7 +743,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -700,7 +815,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -774,7 +897,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -800,7 +931,9 @@ describe('reset users password', () => { const expectedResponse = { data: { resetPassword: { - status: 'todo', + result: { + status: 'todo', + }, }, }, } @@ -888,7 +1021,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -911,9 +1052,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `When resetting password user attempted to verify account, but userKey is not located in the token parameters.`, ]) @@ -936,7 +1086,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -959,9 +1117,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `When resetting password user attempted to verify account, but userKey is not located in the token parameters.`, ]) @@ -984,7 +1151,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -1007,9 +1182,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `A user attempted to reset the password for 1, however there is no associated account.`, ]) @@ -1042,7 +1226,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -1065,9 +1257,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset password, however the current password does not match the current hashed password in the db.`, ]) @@ -1097,7 +1298,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -1120,9 +1329,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset their password, however the submitted passwords do not match.`, ]) @@ -1152,7 +1370,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -1175,9 +1401,18 @@ describe('reset users password', () => { }, ) - const error = [new GraphQLError('todo')] + const error = { + data: { + resetPassword: { + result: { + code: 400, + description: 'todo', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: ${user._key} attempted to reset their password, however the submitted password is not long enough.`, ]) @@ -1211,7 +1446,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, @@ -1273,7 +1516,15 @@ describe('reset users password', () => { resetToken: "${resetToken}" } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `, diff --git a/api-js/src/user/mutations/reset-password.js b/api-js/src/user/mutations/reset-password.js index cc4e547807..0f4704a997 100644 --- a/api-js/src/user/mutations/reset-password.js +++ b/api-js/src/user/mutations/reset-password.js @@ -2,6 +2,8 @@ import { GraphQLNonNull, GraphQLString } from 'graphql' import { mutationWithClientMutationId } from 'graphql-relay' import { t } from '@lingui/macro' +import { resetPasswordUnion } from '../unions' + export const resetPassword = new mutationWithClientMutationId({ name: 'ResetPassword', description: @@ -22,13 +24,11 @@ export const resetPassword = new mutationWithClientMutationId({ }, }), outputFields: () => ({ - status: { - type: GraphQLString, + result: { + type: resetPasswordUnion, description: - 'Informs the user if the password reset was successful, and to redirect to sign in page.', - resolve: async (payload) => { - return payload.status - }, + '`ResetPasswordUnion` returning either a `ResetPasswordResult`, or `ResetPasswordError` object.', + resolve: (payload) => payload, }, }), mutateAndGetPayload: async ( @@ -57,7 +57,11 @@ export const resetPassword = new mutationWithClientMutationId({ console.warn( `When resetting password user attempted to verify account, but userKey is not located in the token parameters.`, ) - throw new Error(i18n._(t`Unable to reset password. Please try again.`)) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to reset password. Please try again.`), + } } // Check if user exists @@ -67,7 +71,11 @@ export const resetPassword = new mutationWithClientMutationId({ console.warn( `A user attempted to reset the password for ${tokenParameters.userKey}, however there is no associated account.`, ) - throw new Error(i18n._(t`Unable to reset password. Please try again.`)) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to reset password. Please try again.`), + } } // Check if password in token matches token in db @@ -75,7 +83,11 @@ export const resetPassword = new mutationWithClientMutationId({ console.warn( `User: ${user._key} attempted to reset password, however the current password does not match the current hashed password in the db.`, ) - throw new Error(i18n._(t`Unable to reset password. Please try again.`)) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to reset password. Please try again.`), + } } // Check to see if newly submitted passwords match @@ -83,7 +95,11 @@ export const resetPassword = new mutationWithClientMutationId({ console.warn( `User: ${user._key} attempted to reset their password, however the submitted passwords do not match.`, ) - throw new Error(i18n._(t`New passwords do not match. Please try again.`)) + return { + _type: 'error', + code: 400, + description: i18n._(t`New passwords do not match. Please try again.`), + } } // Check to see if password meets GoC requirements @@ -91,9 +107,13 @@ export const resetPassword = new mutationWithClientMutationId({ console.warn( `User: ${user._key} attempted to reset their password, however the submitted password is not long enough.`, ) - throw new Error( - i18n._(t`Password is not strong enough. Please try again.`), - ) + return { + _type: 'error', + code: 400, + description: i18n._( + t`Password is not strong enough. Please try again.`, + ), + } } // Update users password in db @@ -114,6 +134,7 @@ export const resetPassword = new mutationWithClientMutationId({ console.info(`User: ${user._key} successfully reset their password.`) return { + _type: 'regular', status: i18n._(t`Password was successfully reset.`), } }, diff --git a/api-js/src/user/objects/__tests__/reset-password-error.test.js b/api-js/src/user/objects/__tests__/reset-password-error.test.js new file mode 100644 index 0000000000..4e779680cf --- /dev/null +++ b/api-js/src/user/objects/__tests__/reset-password-error.test.js @@ -0,0 +1,39 @@ +import { GraphQLInt, GraphQLString } from 'graphql' + +import { resetPasswordErrorType } from '../reset-password-error' + +describe('given the resetPasswordErrorType object', () => { + describe('testing the field definitions', () => { + it('has an code field', () => { + const demoType = resetPasswordErrorType.getFields() + + expect(demoType).toHaveProperty('code') + expect(demoType.code.type).toMatchObject(GraphQLInt) + }) + it('has a description field', () => { + const demoType = resetPasswordErrorType.getFields() + + expect(demoType).toHaveProperty('description') + expect(demoType.description.type).toMatchObject(GraphQLString) + }) + }) + + describe('testing the field resolvers', () => { + describe('testing the code resolver', () => { + it('returns the resolved field', () => { + const demoType = resetPasswordErrorType.getFields() + + expect(demoType.code.resolve({ code: 400 })).toEqual(400) + }) + }) + describe('testing the description field', () => { + it('returns the resolved value', () => { + const demoType = resetPasswordErrorType.getFields() + + expect( + demoType.description.resolve({ description: 'description' }), + ).toEqual('description') + }) + }) + }) +}) diff --git a/api-js/src/user/objects/__tests__/reset-password-result.test.js b/api-js/src/user/objects/__tests__/reset-password-result.test.js new file mode 100644 index 0000000000..351d768a0f --- /dev/null +++ b/api-js/src/user/objects/__tests__/reset-password-result.test.js @@ -0,0 +1,24 @@ +import { GraphQLString } from 'graphql' + +import { resetPasswordResultType } from '../reset-password-result' + +describe('given the resetPasswordErrorType object', () => { + describe('testing the field definitions', () => { + it('has an status field', () => { + const demoType = resetPasswordResultType.getFields() + + expect(demoType).toHaveProperty('status') + expect(demoType.status.type).toMatchObject(GraphQLString) + }) + }) + + describe('testing the field resolvers', () => { + describe('testing the status resolver', () => { + it('returns the resolved field', () => { + const demoType = resetPasswordResultType.getFields() + + expect(demoType.status.resolve({ status: 'status' })).toEqual('status') + }) + }) + }) +}) diff --git a/api-js/src/user/objects/index.js b/api-js/src/user/objects/index.js index fb1f3b3688..298ffd09ac 100644 --- a/api-js/src/user/objects/index.js +++ b/api-js/src/user/objects/index.js @@ -1,4 +1,6 @@ export * from './auth-result' +export * from './reset-password-error' +export * from './reset-password-result' export * from './sign-in-error' export * from './tfa-sign-in-result' export * from './user-personal' diff --git a/api-js/src/user/objects/reset-password-error.js b/api-js/src/user/objects/reset-password-error.js new file mode 100644 index 0000000000..d0353e04ba --- /dev/null +++ b/api-js/src/user/objects/reset-password-error.js @@ -0,0 +1,19 @@ +import { GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql' + +export const resetPasswordErrorType = new GraphQLObjectType({ + name: 'ResetPasswordError', + description: + 'This object is used to inform the user if any errors occurred while resetting their password.', + fields: () => ({ + code: { + type: GraphQLInt, + description: 'Error code to inform user what the issue is related to.', + resolve: ({ code }) => code, + }, + description: { + type: GraphQLString, + description: 'Description of the issue that was encountered.', + resolve: ({ description }) => description, + }, + }), +}) diff --git a/api-js/src/user/objects/reset-password-result.js b/api-js/src/user/objects/reset-password-result.js new file mode 100644 index 0000000000..658ee9f284 --- /dev/null +++ b/api-js/src/user/objects/reset-password-result.js @@ -0,0 +1,15 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql' + +export const resetPasswordResultType = new GraphQLObjectType({ + name: 'ResetPasswordResult', + description: + 'This object is used to inform the user that no errors were encountered while resetting their password.', + fields: () => ({ + status: { + type: GraphQLString, + description: + 'Informs the user if the password reset was successful, and to redirect to sign in page.', + resolve: (payload) => payload.status, + }, + }), +}) diff --git a/api-js/src/user/unions/__tests__/reset-password-union.test.js b/api-js/src/user/unions/__tests__/reset-password-union.test.js new file mode 100644 index 0000000000..9535275e7f --- /dev/null +++ b/api-js/src/user/unions/__tests__/reset-password-union.test.js @@ -0,0 +1,45 @@ +import { resetPasswordErrorType, resetPasswordResultType } from '../../objects/index' +import { resetPasswordUnion } from '../reset-password-union' + +describe('given the resetPasswordUnion', () => { + describe('testing the field types', () => { + it('contains resetPasswordResultType', () => { + const demoType = resetPasswordUnion.getTypes() + + expect(demoType).toContain(resetPasswordResultType) + }) + it('contains resetPasswordErrorType', () => { + const demoType = resetPasswordUnion.getTypes() + + expect(demoType).toContain(resetPasswordErrorType) + }) + }) + describe('testing the field selection', () => { + describe('testing the resetPasswordResultType', () => { + it('returns the correct type', () => { + const obj = { + _type: 'regular', + authResult: {}, + } + + expect(resetPasswordUnion.resolveType(obj)).toMatchObject( + resetPasswordResultType, + ) + }) + }) + describe('testing the resetPasswordErrorType', () => { + it('returns the correct type', () => { + const obj = { + _type: 'error', + error: 'sign-in-error', + code: 401, + description: 'text', + } + + expect(resetPasswordUnion.resolveType(obj)).toMatchObject( + resetPasswordErrorType, + ) + }) + }) + }) +}) diff --git a/api-js/src/user/unions/index.js b/api-js/src/user/unions/index.js index 0eb396c567..41def62a31 100644 --- a/api-js/src/user/unions/index.js +++ b/api-js/src/user/unions/index.js @@ -1 +1,2 @@ +export * from './reset-password-union' export * from './sign-in-union' diff --git a/api-js/src/user/unions/reset-password-union.js b/api-js/src/user/unions/reset-password-union.js new file mode 100644 index 0000000000..ef85cb1e3f --- /dev/null +++ b/api-js/src/user/unions/reset-password-union.js @@ -0,0 +1,16 @@ +import { GraphQLUnionType } from 'graphql' +import { resetPasswordErrorType, resetPasswordResultType } from '../objects' + +export const resetPasswordUnion = new GraphQLUnionType({ + name: 'ResetPasswordUnion', + description: + 'This union is used with the `ResetPassword` mutation, allowing for users to reset their password, and support any errors that may occur', + types: [resetPasswordErrorType, resetPasswordResultType], + resolveType({ _type }) { + if (_type === 'regular') { + return resetPasswordResultType + } else { + return resetPasswordErrorType + } + }, +}) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index c7ea167d86..5afd192cf9 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -1402,6 +1402,14 @@ type RequestScanPayload { clientMutationId: String } +# This object is used to inform the user if any errors occurred while resetting their password. +type ResetPasswordError { + # Error code to inform user what the issue is related to. + code: Int + # Description of the issue that was encountered. + description: String +} + input ResetPasswordInput { # The users new password. password: String! @@ -1413,11 +1421,21 @@ input ResetPasswordInput { } type ResetPasswordPayload { + # `ResetPasswordUnion` returning either a `ResetPasswordResult`, or `ResetPasswordError` object. + result: ResetPasswordUnion + clientMutationId: String +} + +# This object is used to inform the user that no errors were encountered while resetting their password. +type ResetPasswordResult { # Informs the user if the password reset was successful, and to redirect to sign in page. status: String - clientMutationId: String } +# This union is used with the `ResetPassword` mutation, allowing for users to +# reset their password, and support any errors that may occur +union ResetPasswordUnion = ResetPasswordError | ResetPasswordResult + # An enum used to assign, and test users roles. enum RoleEnums { # A user who has been given access to view an organization. diff --git a/frontend/src/ResetPasswordPage.js b/frontend/src/ResetPasswordPage.js index 396c2656a2..1e3db604cc 100644 --- a/frontend/src/ResetPasswordPage.js +++ b/frontend/src/ResetPasswordPage.js @@ -35,16 +35,37 @@ export default function ResetPasswordPage() { position: 'top-left', }) }, - onCompleted() { - history.push('/sign-in') - toast({ - title: t`Password Updated`, - description: t`You may now sign in with your new password`, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-left', - }) + onCompleted({ resetPassword }) { + if (resetPassword.result.__typename === 'ResetPasswordResult') { + history.push('/sign-in') + toast({ + title: t`Password Updated`, + description: t`You may now sign in with your new password`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } else if (resetPassword.result.__typename === 'ResetPasswordError') { + toast({ + title: t`Unable to reset your password, please try again.`, + description: resetPassword.result.description, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } else { + toast({ + title: t`Incorrect send method received.`, + description: t`Incorrect resetPassword.result typename.`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + console.log('Incorrect resetPassword.result typename.') + } }, }) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 0d2fca387c..6d48623ebd 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -81,7 +81,15 @@ export const RESET_PASSWORD = gql` resetToken: $resetToken } ) { - status + result { + ... on ResetPasswordError { + code + description + } + ... on ResetPasswordResult { + status + } + } } } `