diff --git a/src/app.constants.ts b/src/app.constants.ts index f5859a231..d77c1b984 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -343,3 +343,8 @@ export const WEB_TO_MOBILE_ERROR_MESSAGE_MAPPINGS: Record = { "pages.reEnterEmailAccount.enterYourEmailAddressError": "mobileAppPages.reEnterEmailAccount.enterYourEmailAddressError", }; + +export const CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION = { + HELP_DELETE_ACCOUNT: "help-to-delete-account", + RETRY_SECURITY_CODE: "retry-security-code", +}; diff --git a/src/components/common/state-machine/state-machine.ts b/src/components/common/state-machine/state-machine.ts index 82f527b54..da41a87a8 100644 --- a/src/components/common/state-machine/state-machine.ts +++ b/src/components/common/state-machine/state-machine.ts @@ -761,10 +761,20 @@ const authStateMachine = createMachine( }, }, [PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES]: { - type: "final", + on: { + [USER_JOURNEY_EVENTS.VERIFY_MFA]: [PATH_NAMES.ENTER_MFA], + [USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE]: [ + PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE, + ], + }, }, [PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES_IDENTITY_FAIL]: { - type: "final", + on: { + [USER_JOURNEY_EVENTS.VERIFY_MFA]: [PATH_NAMES.ENTER_MFA], + [USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE]: [ + PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE, + ], + }, }, }, }, diff --git a/src/components/enter-password/enter-password-controller.ts b/src/components/enter-password/enter-password-controller.ts index a3f7ce377..62a977e96 100644 --- a/src/components/enter-password/enter-password-controller.ts +++ b/src/components/enter-password/enter-password-controller.ts @@ -153,6 +153,7 @@ export function enterPasswordPost( req.session.user.isLatestTermsAndConditionsAccepted = userLogin.data.latestTermsAndConditionsAccepted; req.session.user.isPasswordChangeRequired = isPasswordChangeRequired; + req.session.user.mfaMethodType = userLogin.data.mfaMethodType; if (isPasswordChangeRequired && supportAccountInterventions()) { const accountInterventionsResponse = diff --git a/src/components/ipv-callback/ipv-callback-controller.ts b/src/components/ipv-callback/ipv-callback-controller.ts index 67ce37e43..81c2cce00 100644 --- a/src/components/ipv-callback/ipv-callback-controller.ts +++ b/src/components/ipv-callback/ipv-callback-controller.ts @@ -10,7 +10,11 @@ import { reverificationResultService } from "./reverification-result-service"; import { BadRequestError } from "../../utils/error"; import { getNextPathAndUpdateJourney } from "../common/constants"; import { USER_JOURNEY_EVENTS } from "../common/state-machine/state-machine"; -import { PATH_NAMES } from "../../app.constants"; +import { + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION, + MFA_METHOD_TYPE, + PATH_NAMES, +} from "../../app.constants"; const ERROR_TO_EVENT_MAP = new Map(); ERROR_TO_EVENT_MAP.set( @@ -98,9 +102,29 @@ export function cannotChangeSecurityCodesGet( }); } -export function cannotChangeSecurityCodesPost( +export async function cannotChangeSecurityCodesPost( req: Request, res: Response -): void { - res.send("In development"); +): Promise { + const { sessionId } = res.locals; + const cannotChangeHowGetSecurityCodeAction = + req.body.cannotChangeHowGetSecurityCodeAction; + if ( + cannotChangeHowGetSecurityCodeAction === + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE + ) { + return res.redirect( + await getNextPathAndUpdateJourney( + req, + req.path, + req.session.user.mfaMethodType === MFA_METHOD_TYPE.SMS + ? USER_JOURNEY_EVENTS.VERIFY_MFA + : USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE, + {}, + sessionId + ) + ); + } else { + res.send("In development"); + } } diff --git a/src/components/ipv-callback/tests/ipv-callback-controller.test.ts b/src/components/ipv-callback/tests/ipv-callback-controller.test.ts index f584bb103..abc41086d 100644 --- a/src/components/ipv-callback/tests/ipv-callback-controller.test.ts +++ b/src/components/ipv-callback/tests/ipv-callback-controller.test.ts @@ -1,11 +1,16 @@ import { mockResponse, RequestOutput, ResponseOutput } from "mock-req-res"; import sinon from "sinon"; import { createMockRequest } from "../../../../test/helpers/mock-request-helper"; -import { PATH_NAMES } from "../../../app.constants"; +import { + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION, + MFA_METHOD_TYPE, + PATH_NAMES, +} from "../../../app.constants"; import { expect } from "chai"; import { Request, Response } from "express"; import { cannotChangeSecurityCodesGet, + cannotChangeSecurityCodesPost, ipvCallbackGet, } from "../ipv-callback-controller"; import { @@ -203,5 +208,31 @@ describe("ipv callback controller", () => { ); }); }); + + describe("cannotChangeSecurityCodePost", () => { + it("should redirect to enter sms mfa page when sms mfa user selects 'ry entering a security code again with the method you already have set up' radio button", async () => { + req.path = PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES; + req.body.cannotChangeHowGetSecurityCodeAction = + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE; + req.session.user.mfaMethodType = MFA_METHOD_TYPE.SMS; + + await cannotChangeSecurityCodesPost(req as Request, res as Response); + + expect(res.redirect).to.have.calledWith(PATH_NAMES.ENTER_MFA); + }); + + it("should redirect to enter auth app mfa page when auth app mfa user selects 'ry entering a security code again with the method you already have set up' radio button", async () => { + req.path = PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES; + req.body.cannotChangeHowGetSecurityCodeAction = + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE; + req.session.user.mfaMethodType = MFA_METHOD_TYPE.AUTH_APP; + + await cannotChangeSecurityCodesPost(req as Request, res as Response); + + expect(res.redirect).to.have.calledWith( + PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE + ); + }); + }); }); }); diff --git a/src/components/ipv-callback/tests/ipv-callback-integration.test.ts b/src/components/ipv-callback/tests/ipv-callback-integration.test.ts index a7d54b11c..ce0647485 100644 --- a/src/components/ipv-callback/tests/ipv-callback-integration.test.ts +++ b/src/components/ipv-callback/tests/ipv-callback-integration.test.ts @@ -1,7 +1,12 @@ import { describe } from "mocha"; import decache from "decache"; import { expect, request, sinon } from "../../../../test/utils/test-utils"; -import { API_ENDPOINTS, PATH_NAMES } from "../../../app.constants"; +import { + API_ENDPOINTS, + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION, + MFA_METHOD_TYPE, + PATH_NAMES, +} from "../../../app.constants"; import express from "express"; import nock from "nock"; import * as cheerio from "cheerio"; @@ -9,7 +14,10 @@ import * as cheerio from "cheerio"; describe("Integration:: ipv callback", () => { let app: express.Application; let baseApi: string; - let sessionMiddleware: any; + + before(async () => { + process.env.SUPPORT_MFA_RESET_WITH_IPV = "1"; + }); after(() => { delete process.env.SUPPORT_MFA_RESET_WITH_IPV; @@ -17,29 +25,8 @@ describe("Integration:: ipv callback", () => { describe("ipv callback", () => { before(async () => { - decache("../../../app"); - decache("../../../middleware/session-middleware"); - process.env.SUPPORT_MFA_RESET_WITH_IPV = "1"; baseApi = process.env.FRONTEND_API_BASE_URL; - sessionMiddleware = require("../../../middleware/session-middleware"); - - sinon - .stub(sessionMiddleware, "validateSessionMiddleware") - .callsFake(function (req: any, res: any, next: any): void { - res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854"; - - req.session.user = { - email: "test@test.com", - phoneNumber: "7867", - journey: { - nextPath: PATH_NAMES.IPV_CALLBACK, - }, - }; - - next(); - }); - - app = await require("../../../app").createApp(); + app = await stubSessionMiddlewareAndCreateApp(PATH_NAMES.IPV_CALLBACK); }); after(() => { @@ -92,53 +79,18 @@ describe("Integration:: ipv callback", () => { }); describe("cannot change how get security codes", () => { - let token: string | string[]; - let cookies: string; - - before(async () => { - decache("../../../app"); - decache("../../../middleware/session-middleware"); - process.env.SUPPORT_MFA_RESET_WITH_IPV = "1"; - sessionMiddleware = require("../../../middleware/session-middleware"); - - sinon - .stub(sessionMiddleware, "validateSessionMiddleware") - .callsFake(function (req: any, res: any, next: any): void { - res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854"; - - req.session.user = { - email: "test@test.com", - phoneNumber: "7867", - journey: { - nextPath: PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES, - }, - }; - - next(); - }); - - app = await require("../../../app").createApp(); - - await request( - app, - (test) => test.get(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), - { - expectAnalyticsPropertiesMatchSnapshot: false, - } - ).then((res) => { - const $ = cheerio.load(res.text); - token = $("[name=_csrf]").val(); - cookies = res.headers["set-cookie"]; - }); - }); - - after(() => { + afterEach(() => { app = undefined; - nock.cleanAll(); sinon.restore(); }); it("returns a dummy page when an option is selected", async () => { + const app = await stubSessionMiddlewareAndCreateApp( + PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES + ); + const { token, cookies } = + await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app); + await request( app, (test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), @@ -150,7 +102,8 @@ describe("Integration:: ipv callback", () => { .set("Cookie", cookies) .send({ _csrf: token, - cannotChangeHowGetSecurityCodeAction: "help-to-delete-account", + cannotChangeHowGetSecurityCodeAction: + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.HELP_DELETE_ACCOUNT, }) .expect(function (res) { expect(res.text).to.equals("In development"); @@ -159,6 +112,12 @@ describe("Integration:: ipv callback", () => { }); it("returns a validation error when no option is selected", async () => { + const app = await stubSessionMiddlewareAndCreateApp( + PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES + ); + const { token, cookies } = + await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app); + await request( app, (test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), @@ -180,5 +139,103 @@ describe("Integration:: ipv callback", () => { }) .expect(400); }); + + it("goes to /enter-code when user selects retry security code radio button and their mfaMethodType is SMS", async () => { + const app = await stubSessionMiddlewareAndCreateApp( + PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES, + MFA_METHOD_TYPE.SMS + ); + const { token, cookies } = + await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app); + + await request( + app, + (test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), + { + expectAnalyticsPropertiesMatchSnapshot: false, + } + ) + .type("form") + .set("Cookie", cookies) + .send({ + _csrf: token, + cannotChangeHowGetSecurityCodeAction: + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE, + }) + .expect("Location", PATH_NAMES.ENTER_MFA) + .expect(302); + }); + + it("goes to /enter-authenticator-app-code when user selects retry security code radio button and their mfaMethodType is AUTH_APP", async () => { + const app = await stubSessionMiddlewareAndCreateApp( + PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES, + MFA_METHOD_TYPE.AUTH_APP + ); + const { token, cookies } = + await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app); + + await request( + app, + (test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), + { + expectAnalyticsPropertiesMatchSnapshot: false, + } + ) + .type("form") + .set("Cookie", cookies) + .send({ + _csrf: token, + cannotChangeHowGetSecurityCodeAction: + CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE, + }) + .expect("Location", PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE) + .expect(302); + }); }); }); + +const stubSessionMiddlewareAndCreateApp = async ( + nextPath: string, + mfaMethodType?: MFA_METHOD_TYPE +): Promise => { + decache("../../../app"); + decache("../../../middleware/session-middleware"); + const sessionMiddleware = require("../../../middleware/session-middleware"); + + sinon + .stub(sessionMiddleware, "validateSessionMiddleware") + .callsFake(function (req: any, res: any, next: any): void { + res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854"; + + req.session.user = { + email: "test@test.com", + phoneNumber: "7867", + journey: { + nextPath: nextPath, + }, + mfaMethodType: mfaMethodType, + }; + + next(); + }); + + return await require("../../../app").createApp(); +}; + +const getCannotChangeSecurityCodesAndReturnTokenAndCookies = async ( + app: express.Application +) => { + let cookies, token; + await request( + app, + (test) => test.get(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES), + { + expectAnalyticsPropertiesMatchSnapshot: false, + } + ).then((res) => { + const $ = cheerio.load(res.text); + token = $("[name=_csrf]").val(); + cookies = res.headers["set-cookie"]; + }); + return { token, cookies }; +}; diff --git a/src/types.ts b/src/types.ts index e5b9a06da..8ad65062a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,6 +85,7 @@ export interface UserSession { isSignInJourney?: boolean; isVerifyEmailCodeResendRequired?: boolean; channel?: string; + mfaMethodType?: string; } export interface UserSessionClient {