From 29796247f5289cd64b8cc5112d2964f2b4167af8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 13:22:40 +0000 Subject: [PATCH] BAU: Bump xstate from 4.38.3 to 5.13.0 Bumps [xstate](https://github.com/statelyai/xstate) from 4.38.3 to 5.13.0. - [Release notes](https://github.com/statelyai/xstate/releases) - [Commits](https://github.com/statelyai/xstate/compare/xstate@4.38.3...xstate@5.13.0) --- updated-dependencies: - dependency-name: xstate dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- @types/express/index.d.ts | 2 + docker-compose.yml | 7 +- package-lock.json | 8 +- package.json | 2 +- src/app.constants.ts | 44 +++---- .../add-mfa-method-app-controller.ts | 7 +- .../add-mfa-methods-app-controller.test.ts | 2 +- .../tests/add-mfa-methods-controller.test.ts | 10 +- .../change-authenticator-app-controller.ts | 4 +- ...hange-authenticator-app-controller.test.ts | 1 + .../change-email/change-email-controller.ts | 4 +- .../tests/change-email-service.test.ts | 2 +- .../change-password-controller.ts | 4 +- .../tests/change-password-controller.test.ts | 1 + .../change-phone-number-controller.ts | 4 +- .../change-phone-number-controller.test.ts | 3 +- .../check-your-email-controller.ts | 6 +- .../tests/check-your-email-controller.test.ts | 4 +- .../tests/check-your-email-service.test.ts | 2 +- .../check-your-phone-controller.ts | 6 +- .../tests/check-your-phone-controller.test.ts | 4 +- .../delete-account-controller.ts | 4 +- .../tests/delete-account-controller.test.ts | 3 + .../enter-password-controller.ts | 3 +- .../tests/enter-password-controller.test.ts | 2 +- .../tests/enter-password-service.test.ts | 2 +- .../tests/global-logout-controller.test.ts | 11 +- .../logout/tests/logout-controller.test.ts | 2 +- .../resend-email-code-controller.ts | 4 +- .../resend-phone-code-controller.ts | 4 +- src/middleware/requires-auth-middleware.ts | 2 +- src/utils/session-store.ts | 2 +- src/utils/state-machine.ts | 124 +++++++++++------- test/unit/utils/state-machine.test.ts | 63 ++++----- 34 files changed, 201 insertions(+), 152 deletions(-) diff --git a/@types/express/index.d.ts b/@types/express/index.d.ts index c1af00aaf..4b9c61c00 100644 --- a/@types/express/index.d.ts +++ b/@types/express/index.d.ts @@ -3,6 +3,7 @@ import { User } from "../../src/types"; import { QueryParameters } from "../../src/app.constants"; import { MfaMethod } from "../../src/utils/mfa/types"; +import { logger } from "../../src/utils/logger"; declare module "express-session" { interface Session { @@ -29,6 +30,7 @@ declare module "express-serve-static-core" { csrfToken?: () => string; oidc?: Client; issuerJWKS?: any; + log: logger; } } interface Cookie { diff --git a/docker-compose.yml b/docker-compose.yml index d07b90d3e..23f005f1e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,16 +2,15 @@ version: "3.8" services: localstack: container_name: localstack - image: localstack/localstack:1.3.1 + image: localstack/localstack:3.0.0 ports: - "4566:4566" - "4569:4569" environment: - - SERVICES=kms,sns,dynamodb,sqs - - HOSTNAME_EXTERNAL=localhost + - SERVICES=kms,sns,dynamodb,sqs,stepfunctions + - LOCALSTACK_HOST=localhost - DYNAMODB_SHARE_DB=1 # Removes regions and allows NoSQL Workbench to work. - DEBUG=${DEBUG:-0} - - KMS_PROVIDER=local-kms - AWS_ACCESS_KEY_ID=na - AWS_SECRET_ACCESS_KEY=na - MY_ONE_LOGIN_USER_ID=${MY_ONE_LOGIN_USER_ID} diff --git a/package-lock.json b/package-lock.json index 5fa3d071f..8a6ab668c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "qrcode": "^1.5.0", "uuid": "^9.0.1", "xss": "^1.0.15", - "xstate": "^4.38.2" + "xstate": "^5.13.0" }, "devDependencies": { "@types/chai": "^4.3.6", @@ -11612,9 +11612,9 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/xstate": { - "version": "4.38.3", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz", - "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.13.0.tgz", + "integrity": "sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" diff --git a/package.json b/package.json index c2bf4f1e2..1be0f51f9 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "qrcode": "^1.5.0", "uuid": "^9.0.1", "xss": "^1.0.15", - "xstate": "^4.38.2" + "xstate": "^5.13.0" }, "devDependencies": { "@types/chai": "^4.3.6", diff --git a/src/app.constants.ts b/src/app.constants.ts index 92474aa16..f73b82ec4 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -1,7 +1,7 @@ -import { AccountManagementEvent, UserJourney } from "./utils/state-machine"; +import { EventType, UserJourney } from "./utils/state-machine"; export const PATH_DATA: { - [key: string]: { url: string; event?: string; type?: UserJourney }; + [key: string]: { url: string; event?: EventType; type?: UserJourney }; } = { CONTACT: { url: "/contact-gov-uk-one-login" }, SIGN_IN_HISTORY: { url: "/activity-history" }, @@ -13,92 +13,92 @@ export const PATH_DATA: { ENTER_PASSWORD: { url: "/enter-password" }, ADD_MFA_METHOD: { url: "/add-mfa-method", - event: "SELECTED_APP", + event: EventType.SelectedApp, type: UserJourney.AddMfaMethod, }, ADD_MFA_METHOD_APP: { url: "/add-mfa-method-app", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.AddMfaMethod, }, ADD_MFA_METHOD_APP_CONFIRMATION: { url: "/add-mfa-method-app-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.AddMfaMethod, }, ADD_MFA_METHOD_SMS: { url: "/add-mfa-method-sms", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.AddMfaMethod, }, CHANGE_EMAIL: { url: "/change-email", - event: "VERIFY_CODE_SENT", + event: EventType.VerifyCodeSent, type: UserJourney.ChangeEmail, }, CHECK_YOUR_EMAIL: { url: "/check-your-email", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.ChangeEmail, }, REQUEST_NEW_CODE_EMAIL: { url: "/request-new-email-code", - event: "RESEND_CODE", + event: EventType.ResendCode, type: UserJourney.ChangeEmail, }, EMAIL_UPDATED_CONFIRMATION: { url: "/email-updated-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.ChangeEmail, }, CHANGE_PASSWORD: { url: "/change-password", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.ChangePassword, }, PASSWORD_UPDATED_CONFIRMATION: { url: "/password-updated-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.ChangePassword, }, CHANGE_PHONE_NUMBER: { url: "/change-phone-number", - event: "VERIFY_CODE_SENT", + event: EventType.VerifyCodeSent, type: UserJourney.ChangePhoneNumber, }, CHANGE_AUTHENTICATOR_APP: { url: "/change-authenticator-app", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.ChangeAuthenticatorApp, }, CHECK_YOUR_PHONE: { url: "/check-your-phone", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.ChangePhoneNumber, }, REQUEST_NEW_CODE_OTP: { url: "/request-new-opt-code", - event: "RESEND_CODE", + event: EventType.ResendCode, type: UserJourney.ChangePhoneNumber, }, PHONE_NUMBER_UPDATED_CONFIRMATION: { url: "/phone-number-updated-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.ChangePhoneNumber, }, AUTHENTICATOR_APP_UPDATED_CONFIRMATION: { url: "/authenticator-app-updated-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.ChangeAuthenticatorApp, }, DELETE_ACCOUNT: { url: "/delete-account", - event: "VALUE_UPDATED", + event: EventType.ValueUpdated, type: UserJourney.DeleteAccount, }, ACCOUNT_DELETED_CONFIRMATION: { url: "/account-deleted-confirmation", - event: "CONFIRMATION", + event: EventType.Confirmation, type: UserJourney.DeleteAccount, }, AUTH_CALLBACK: { url: "/auth/callback" }, @@ -125,12 +125,12 @@ export const MFA_METHODS = { SMS: { type: "sms", path: PATH_DATA.ADD_MFA_METHOD_SMS, - event: "SELECTED_SMS" as AccountManagementEvent, + event: EventType.SelectedSms, }, APP: { type: "app", path: PATH_DATA.ADD_MFA_METHOD_APP, - event: "SELECTED_APP" as AccountManagementEvent, + event: EventType.SelectedApp, }, }; diff --git a/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts b/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts index 9cf753bfd..657e6358f 100755 --- a/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts +++ b/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts @@ -1,9 +1,9 @@ -import { Request, Response, NextFunction } from "express"; +import { NextFunction, Request, Response } from "express"; import { HTTP_STATUS_CODES, PATH_DATA } from "../../app.constants"; import { addMfaMethod, verifyMfaCode } from "../../utils/mfa"; import assert from "node:assert"; import { formatValidationError } from "../../utils/validation"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { renderMfaMethodPage } from "../common/mfa"; const ADD_MFA_METHOD_AUTH_APP_TEMPLATE = "add-mfa-method-app/index.njk"; @@ -93,9 +93,8 @@ export async function addMfaAppMethodPost( req.session.user.state.addMfaMethod = getNextState( req.session.user.state.addMfaMethod.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); - return res.redirect(PATH_DATA.ADD_MFA_METHOD_APP_CONFIRMATION.url); } catch (e) { req.log.error(e); diff --git a/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts b/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts index 38bc57f5f..a5593429b 100755 --- a/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts +++ b/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts @@ -91,7 +91,7 @@ describe("addMfaAppMethodPost", () => { user: { email: "test@test.com", tokens: { accessToken: "token" }, - state: { addMfaMethod: ["VALUE_UPDATED"] }, + state: { addMfaMethod: { value: "APP" } }, }, }, log: { error: sinon.fake() }, diff --git a/src/components/add-mfa-method/tests/add-mfa-methods-controller.test.ts b/src/components/add-mfa-method/tests/add-mfa-methods-controller.test.ts index 2ecf8663d..3c58ef902 100644 --- a/src/components/add-mfa-method/tests/add-mfa-methods-controller.test.ts +++ b/src/components/add-mfa-method/tests/add-mfa-methods-controller.test.ts @@ -9,6 +9,7 @@ import { addMfaMethodGet, } from "../add-mfa-methods-controller"; import { PATH_DATA } from "../../../app.constants"; +import { EventType } from "../../../utils/state-machine"; describe("addMfaMethodGet", () => { let sandbox: sinon.SinonSandbox; @@ -20,10 +21,13 @@ describe("addMfaMethodGet", () => { req = { body: {}, - session: { user: { state: { addMfaMethod: ["AUTHENTICATED"] } } } as any, + session: { + user: { state: { addMfaMethod: { value: EventType.Authenticated } } }, + } as any, cookies: { lng: "en" }, i18n: { language: "en" }, t: sandbox.fake(), + log: { error: sandbox.fake() }, }; res = { render: sandbox.fake(), @@ -70,7 +74,9 @@ describe("addMfaMethodPost", () => { req = { body: {}, log: { error: sandbox.fake() } as any, - session: { user: { state: { addMfaMethod: ["SELECT_APP"] } } } as any, + session: { + user: { state: { addMfaMethod: { value: "SMS" } } }, + } as any, }; res = { status: sandbox.fake(), diff --git a/src/components/change-authenticator-app/change-authenticator-app-controller.ts b/src/components/change-authenticator-app/change-authenticator-app-controller.ts index a63088e15..aa6b13c7d 100644 --- a/src/components/change-authenticator-app/change-authenticator-app-controller.ts +++ b/src/components/change-authenticator-app/change-authenticator-app-controller.ts @@ -3,7 +3,7 @@ import { PATH_DATA } from "../../app.constants"; import { ExpressRouteFunc } from "../../types"; import { ChangeAuthenticatorAppServiceInterface } from "./types"; import { changeAuthenticatorAppService } from "./change-authenticator-app-service"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { formatValidationError } from "../../utils/validation"; import { verifyMfaCode } from "../../utils/mfa"; import assert from "node:assert"; @@ -89,7 +89,7 @@ export function changeAuthenticatorAppPost( req.session.user.state.changeAuthenticatorApp = getNextState( req.session.user.state.changeAuthenticatorApp.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); return res.redirect(PATH_DATA.AUTHENTICATOR_APP_UPDATED_CONFIRMATION.url); diff --git a/src/components/change-authenticator-app/tests/change-authenticator-app-controller.test.ts b/src/components/change-authenticator-app/tests/change-authenticator-app-controller.test.ts index 50b2c02e0..293d57795 100644 --- a/src/components/change-authenticator-app/tests/change-authenticator-app-controller.test.ts +++ b/src/components/change-authenticator-app/tests/change-authenticator-app-controller.test.ts @@ -142,6 +142,7 @@ describe("change authenticator app controller", () => { updateAuthenticatorApp: sandbox.fake.resolves(true), }; req.session.user.tokens = { accessToken: "token" } as any; + req.session.user.state.changeAuthenticatorApp.value = "CHANGE_VALUE"; req.session.mfaMethods = [ { mfaIdentifier: 111111, diff --git a/src/components/change-email/change-email-controller.ts b/src/components/change-email/change-email-controller.ts index b5754dc86..88d26934b 100644 --- a/src/components/change-email/change-email-controller.ts +++ b/src/components/change-email/change-email-controller.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { PATH_DATA } from "../../app.constants"; import { ExpressRouteFunc } from "../../types"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { formatValidationError, renderBadRequest, @@ -54,7 +54,7 @@ export function changeEmailPost( req.session.user.state.changeEmail = getNextState( req.session.user.state.changeEmail.value, - "VERIFY_CODE_SENT" + EventType.VerifyCodeSent ); return res.redirect(PATH_DATA.CHECK_YOUR_EMAIL.url); diff --git a/src/components/change-email/tests/change-email-service.test.ts b/src/components/change-email/tests/change-email-service.test.ts index 47c4c64f1..52271cf89 100644 --- a/src/components/change-email/tests/change-email-service.test.ts +++ b/src/components/change-email/tests/change-email-service.test.ts @@ -19,7 +19,7 @@ describe("changeEmailService", () => { }); afterEach(() => { sandbox.restore(); - nock.cleanAll; + nock.cleanAll(); }); it("send Code verification Notification", async () => { diff --git a/src/components/change-password/change-password-controller.ts b/src/components/change-password/change-password-controller.ts index 46d1f5445..bc70b96e8 100644 --- a/src/components/change-password/change-password-controller.ts +++ b/src/components/change-password/change-password-controller.ts @@ -3,7 +3,7 @@ import { ExpressRouteFunc } from "../../types"; import { PATH_DATA, ERROR_CODES } from "../../app.constants"; import { ChangePasswordServiceInterface } from "./types"; import { changePasswordService } from "./change-password-service"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { renderBadRequest, formatValidationError, @@ -41,7 +41,7 @@ export function changePasswordPost( if (response.success) { req.session.user.state.changePassword = getNextState( req.session.user.state.changePassword.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); return res.redirect(PATH_DATA.PASSWORD_UPDATED_CONFIRMATION.url); diff --git a/src/components/change-password/tests/change-password-controller.test.ts b/src/components/change-password/tests/change-password-controller.test.ts index 6117c80c9..76592d2f8 100644 --- a/src/components/change-password/tests/change-password-controller.test.ts +++ b/src/components/change-password/tests/change-password-controller.test.ts @@ -76,6 +76,7 @@ describe("change password controller", () => { it("should redirect to /password-updated-confirmation page", async () => { // Arrange req.session.user.tokens = { accessToken: "token" } as any; + req.session.user.state.changePassword.value = "CHANGE_VALUE"; req.body.password = "Password1"; // Act diff --git a/src/components/change-phone-number/change-phone-number-controller.ts b/src/components/change-phone-number/change-phone-number-controller.ts index 71c1751e3..e58b2ea01 100644 --- a/src/components/change-phone-number/change-phone-number-controller.ts +++ b/src/components/change-phone-number/change-phone-number-controller.ts @@ -3,7 +3,7 @@ import { ERROR_CODES, PATH_DATA } from "../../app.constants"; import { ExpressRouteFunc } from "../../types"; import { ChangePhoneNumberServiceInterface } from "./types"; import { changePhoneNumberService } from "./change-phone-number-service"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { formatValidationError, renderBadRequest, @@ -53,7 +53,7 @@ export function changePhoneNumberPost( req.session.user.state.changePhoneNumber = getNextState( req.session.user.state.changePhoneNumber.value, - "VERIFY_CODE_SENT" + EventType.VerifyCodeSent ); return res.redirect(PATH_DATA.CHECK_YOUR_PHONE.url); diff --git a/src/components/change-phone-number/tests/change-phone-number-controller.test.ts b/src/components/change-phone-number/tests/change-phone-number-controller.test.ts index 493e5187e..4ce9c3641 100644 --- a/src/components/change-phone-number/tests/change-phone-number-controller.test.ts +++ b/src/components/change-phone-number/tests/change-phone-number-controller.test.ts @@ -68,6 +68,7 @@ describe("change phone number controller", () => { }), }; req.body.phoneNumber = "12345678991"; + req.session.user.state.changePhoneNumber.value = "CHANGE_VALUE"; // Act await changePhoneNumberPost(fakeService)(req as Request, res as Response); @@ -163,7 +164,7 @@ describe("change phone number controller", () => { }), }; req.body.phoneNumber = "+33645453322"; - + req.session.user.state.changePhoneNumber.value = "CHANGE_VALUE"; // Act await changePhoneNumberPost(fakeService)(req as Request, res as Response); diff --git a/src/components/check-your-email/check-your-email-controller.ts b/src/components/check-your-email/check-your-email-controller.ts index e9b59d8cd..fdfe8a9ec 100644 --- a/src/components/check-your-email/check-your-email-controller.ts +++ b/src/components/check-your-email/check-your-email-controller.ts @@ -7,7 +7,7 @@ import { } from "../../utils/validation"; import { checkYourEmailService } from "./check-your-email-service"; import { CheckYourEmailServiceInterface } from "./types"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { GovUkPublishingServiceInterface } from "../common/gov-uk-publishing/types"; import { govUkPublishingService } from "../common/gov-uk-publishing/gov-uk-publishing-service"; import xss from "xss"; @@ -72,7 +72,7 @@ export function checkYourEmailPost( req.session.user.state.changeEmail = getNextState( req.session.user.state.changeEmail.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); return res.redirect(PATH_DATA.EMAIL_UPDATED_CONFIRMATION.url); @@ -90,7 +90,7 @@ export function checkYourEmailPost( export function requestNewCodeGet(req: Request, res: Response): void { req.session.user.state.changeEmail = getNextState( req.session.user.state.changeEmail.value, - "RESEND_CODE" + EventType.ResendCode ); return res.redirect(PATH_DATA.CHANGE_EMAIL.url); diff --git a/src/components/check-your-email/tests/check-your-email-controller.test.ts b/src/components/check-your-email/tests/check-your-email-controller.test.ts index 964ebb2ea..b3ce10881 100644 --- a/src/components/check-your-email/tests/check-your-email-controller.test.ts +++ b/src/components/check-your-email/tests/check-your-email-controller.test.ts @@ -22,7 +22,9 @@ describe("check your email controller", () => { req = { body: {}, - session: { user: { state: { changeEmail: {} } } } as any, + session: { + user: { state: { changeEmail: { value: "CHANGE_VALUE" } } }, + } as any, cookies: { lng: "en" }, i18n: { language: "en" }, log: { error: sandbox.fake() }, diff --git a/src/components/check-your-email/tests/check-your-email-service.test.ts b/src/components/check-your-email/tests/check-your-email-service.test.ts index 216f51a59..93dc8f1a1 100644 --- a/src/components/check-your-email/tests/check-your-email-service.test.ts +++ b/src/components/check-your-email/tests/check-your-email-service.test.ts @@ -23,7 +23,7 @@ describe("checkYourEmailService", () => { }); afterEach(() => { sandbox.restore(); - nock.cleanAll; + nock.cleanAll(); }); it("update the email ", async () => { diff --git a/src/components/check-your-phone/check-your-phone-controller.ts b/src/components/check-your-phone/check-your-phone-controller.ts index 9475ed11e..3e58a1844 100644 --- a/src/components/check-your-phone/check-your-phone-controller.ts +++ b/src/components/check-your-phone/check-your-phone-controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { ExpressRouteFunc } from "../../types"; import { PATH_DATA } from "../../app.constants"; import { CheckYourPhoneServiceInterface } from "./types"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { checkYourPhoneService } from "./check-your-phone-service"; import { formatValidationError, @@ -65,7 +65,7 @@ export function checkYourPhonePost( req.session.user.state.changePhoneNumber = getNextState( req.session.user.state.changePhoneNumber.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); return res.redirect(PATH_DATA.PHONE_NUMBER_UPDATED_CONFIRMATION.url); @@ -83,7 +83,7 @@ export function checkYourPhonePost( export function requestNewOTPCodeGet(req: Request, res: Response): void { req.session.user.state.changePhoneNumber = getNextState( req.session.user.state.changePhoneNumber.value, - "RESEND_CODE" + EventType.ResendCode ); return res.redirect(PATH_DATA.CHANGE_PHONE_NUMBER.url); diff --git a/src/components/check-your-phone/tests/check-your-phone-controller.test.ts b/src/components/check-your-phone/tests/check-your-phone-controller.test.ts index 128c41323..48a77a29e 100644 --- a/src/components/check-your-phone/tests/check-your-phone-controller.test.ts +++ b/src/components/check-your-phone/tests/check-your-phone-controller.test.ts @@ -23,7 +23,7 @@ describe("check your phone controller", () => { req = { body: {}, session: { - user: { state: { changePhoneNumber: {} } }, + user: { state: { changePhoneNumber: { value: "CHANGE_VALUE" } } }, mfaMethods: [ { mfaIdentifier: 111111, @@ -67,6 +67,7 @@ describe("check your phone controller", () => { req.session.user.tokens = { accessToken: "token" } as any; req.body.code = "123456"; + req.session.user.state.changePhoneNumber.value = "CHANGE_VALUE"; await checkYourPhonePost(fakeService)(req as Request, res as Response); @@ -103,6 +104,7 @@ describe("check your phone controller", () => { }; req.session.user.tokens = { accessToken: "token" } as any; + req.session.user.state.changePhoneNumber.value = "CHANGE_VALUE"; req.body.code = "123456"; req.session.user.newPhoneNumber = "07111111111"; req.session.user.email = "test@test.com"; diff --git a/src/components/delete-account/delete-account-controller.ts b/src/components/delete-account/delete-account-controller.ts index 07c1ab40a..b5bebeb0d 100644 --- a/src/components/delete-account/delete-account-controller.ts +++ b/src/components/delete-account/delete-account-controller.ts @@ -3,7 +3,7 @@ import { ExpressRouteFunc } from "../../types"; import { DeleteAccountServiceInterface } from "./types"; import { deleteAccountService } from "./delete-account-service"; import { PATH_DATA } from "../../app.constants"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { getAppEnv, getBaseUrl, getSNSDeleteTopic } from "../../config"; import { clearCookies, destroyUserSessions } from "../../utils/session-store"; import { @@ -76,7 +76,7 @@ export function deleteAccountPost( req.session.user.state.deleteAccount = getNextState( req.session.user.state.deleteAccount.value, - "VALUE_UPDATED" + EventType.ValueUpdated ); const logoutUrl = req.oidc.endSessionUrl({ diff --git a/src/components/delete-account/tests/delete-account-controller.test.ts b/src/components/delete-account/tests/delete-account-controller.test.ts index 2ebcc021e..3fd32cb7c 100644 --- a/src/components/delete-account/tests/delete-account-controller.test.ts +++ b/src/components/delete-account/tests/delete-account-controller.test.ts @@ -134,6 +134,7 @@ describe("delete account controller", () => { req.session.user.email = "test@test.com"; req.session.user.subjectId = "public-subject-id"; req.session.user.tokens = { accessToken: "token" } as any; + req.session.user.state.deleteAccount.value = "CHANGE_VALUE"; req.oidc = { endSessionUrl: sandbox.fake.returns("logout-url"), } as any; @@ -151,6 +152,7 @@ describe("delete account controller", () => { req, "public-subject-id" ); + sessionStore.destroyUserSessions.restore(); }); it("should clear am cookie", async () => { req = validRequest(); @@ -159,6 +161,7 @@ describe("delete account controller", () => { publishToDeleteTopic: sandbox.fake(), }; req.session.user.tokens = { accessToken: "token" } as any; + req.session.user.state.deleteAccount.value = "CHANGE_VALUE"; req.oidc = { endSessionUrl: sandbox.fake.returns("logout-url"), } as any; diff --git a/src/components/enter-password/enter-password-controller.ts b/src/components/enter-password/enter-password-controller.ts index 146904070..d270ca70e 100644 --- a/src/components/enter-password/enter-password-controller.ts +++ b/src/components/enter-password/enter-password-controller.ts @@ -8,6 +8,7 @@ import { renderBadRequest, } from "../../utils/validation"; import { + EventType, getInitialState, getNextState, UserJourney, @@ -87,7 +88,7 @@ export function enterPasswordPost( if (isAuthenticated) { req.session.user.state[requestType] = getNextState( req.session.user.state[requestType].value, - "AUTHENTICATED" + EventType.Authenticated ); return res.redirect(REDIRECT_PATHS[requestType]); diff --git a/src/components/enter-password/tests/enter-password-controller.test.ts b/src/components/enter-password/tests/enter-password-controller.test.ts index 7c48f6317..10604582a 100644 --- a/src/components/enter-password/tests/enter-password-controller.test.ts +++ b/src/components/enter-password/tests/enter-password-controller.test.ts @@ -74,7 +74,7 @@ describe("enter password controller", () => { req.session.user = { email: "test@test.com", phoneNumber: "xxxxxxx7898", - state: { changeEmail: {} }, + state: { changeEmail: { value: "CHANGE_VALUE" } }, tokens: { accessToken: "token" }, } as any; diff --git a/src/components/enter-password/tests/enter-password-service.test.ts b/src/components/enter-password/tests/enter-password-service.test.ts index 9802e7e11..d1a1f0ff9 100644 --- a/src/components/enter-password/tests/enter-password-service.test.ts +++ b/src/components/enter-password/tests/enter-password-service.test.ts @@ -19,7 +19,7 @@ describe("enterPasswordService", () => { }); afterEach(() => { sandbox.restore(); - nock.cleanAll; + nock.cleanAll(); }); it("Check if Authenticated ", async () => { diff --git a/src/components/global-logout/tests/global-logout-controller.test.ts b/src/components/global-logout/tests/global-logout-controller.test.ts index 64b40236d..565b476b4 100644 --- a/src/components/global-logout/tests/global-logout-controller.test.ts +++ b/src/components/global-logout/tests/global-logout-controller.test.ts @@ -20,7 +20,7 @@ import { import { GetKeyFunction } from "jose/dist/types/types"; import { logger } from "../../../utils/logger"; -import { destroyUserSessions } from "../../../utils/session-store"; +import * as SessionStore from "../../../utils/session-store"; describe("global logout controller", () => { let sandbox: sinon.SinonSandbox; @@ -29,6 +29,7 @@ describe("global logout controller", () => { let issuerJWKS: GetKeyFunction; let keySet: GenerateKeyPairResult; let loggerSpy: sinon.SinonSpy; + let destroyUserSessionsSpy: sinon.SinonSpy; const validIssuer = "urn:example:issuer"; const validAudience = "urn:example:audience"; @@ -99,10 +100,13 @@ describe("global logout controller", () => { }; loggerSpy = sandbox.spy(logger, "error"); + destroyUserSessionsSpy = sandbox.spy(SessionStore, "destroyUserSessions"); }); afterEach(async () => { sandbox.restore(); + loggerSpy.restore(); + destroyUserSessionsSpy.restore(); }); describe("globalLogoutPost", async () => { @@ -304,13 +308,10 @@ describe("global logout controller", () => { it("should return 200 if logout_token is present and valid", async () => { req = validRequest(await generateValidToken(validLogoutToken)); - const sessionStore = require("../../../utils/session-store"); - sandbox.stub(sessionStore, "destroyUserSessions").resolves(); - await globalLogoutPost(req as Request, res as Response); expect(res.send).to.have.been.calledWith(HTTP_STATUS_CODES.OK); - expect(destroyUserSessions).to.have.been.calledWith(req, "123456"); + expect(destroyUserSessionsSpy).to.have.been.calledWith(req, "123456"); }); }); }); diff --git a/src/components/logout/tests/logout-controller.test.ts b/src/components/logout/tests/logout-controller.test.ts index bc62b7a61..288374a20 100644 --- a/src/components/logout/tests/logout-controller.test.ts +++ b/src/components/logout/tests/logout-controller.test.ts @@ -55,12 +55,12 @@ describe("logout controller", () => { sessionStore, "destroyUserSessions" ); - await logoutPost(req, res); expect(res.mockCookies.lo).to.equal("true"); expect(req.oidc.endSessionUrl).to.have.been.calledOnce; expect(res.redirect).to.have.called; expect(destroyUserSessionsStub.called); + destroyUserSessionsStub.restore(); }); }); diff --git a/src/components/resend-email-code/resend-email-code-controller.ts b/src/components/resend-email-code/resend-email-code-controller.ts index bbaaed3c5..4c29a82db 100644 --- a/src/components/resend-email-code/resend-email-code-controller.ts +++ b/src/components/resend-email-code/resend-email-code-controller.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { ExpressRouteFunc } from "../../types"; import xss from "xss"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import { PATH_DATA } from "../../app.constants"; import { ChangeEmailServiceInterface } from "../change-email/types"; import { changeEmailService } from "../change-email/change-email-service"; @@ -55,7 +55,7 @@ export function resendEmailCodePost( req.session.user.state.changeEmail = getNextState( req.session.user.state.changeEmail.value, - "VERIFY_CODE_SENT" + EventType.VerifyCodeSent ); return res.redirect(PATH_DATA.CHECK_YOUR_EMAIL.url); diff --git a/src/components/resend-phone-code/resend-phone-code-controller.ts b/src/components/resend-phone-code/resend-phone-code-controller.ts index 17d339f31..ce42e7e7f 100644 --- a/src/components/resend-phone-code/resend-phone-code-controller.ts +++ b/src/components/resend-phone-code/resend-phone-code-controller.ts @@ -5,7 +5,7 @@ import { ChangePhoneNumberServiceInterface } from "../change-phone-number/types" import { changePhoneNumberService } from "../change-phone-number/change-phone-number-service"; import { BadRequestError } from "../../utils/errors"; import { getLastNDigits } from "../../utils/phone-number"; -import { getNextState } from "../../utils/state-machine"; +import { EventType, getNextState } from "../../utils/state-machine"; import xss from "xss"; import { formatValidationError, @@ -46,7 +46,7 @@ export function resendPhoneCodePost( req.session.user.state.changePhoneNumber = getNextState( req.session.user.state.changePhoneNumber.value, - "VERIFY_CODE_SENT" + EventType.VerifyCodeSent ); return res.redirect(PATH_DATA.CHECK_YOUR_PHONE.url); diff --git a/src/middleware/requires-auth-middleware.ts b/src/middleware/requires-auth-middleware.ts index ca64c2dfe..1740bbfb2 100644 --- a/src/middleware/requires-auth-middleware.ts +++ b/src/middleware/requires-auth-middleware.ts @@ -15,7 +15,7 @@ export function requiresAuthMiddleware( { trace: res.locals.trace }, `isAuthenticated = ${isAuthenticated} , isLoggedOut = ${isLoggedOut}` ); - // if there is no session then should create a session and redirect to auth to sign in + // if there is no session, then should create a session and redirect to auth to sign in if (isAuthenticated === undefined) { redirectToLogIn(req, res); } else if (!isAuthenticated && isLoggedOut == "true") { diff --git a/src/utils/session-store.ts b/src/utils/session-store.ts index d43931395..45fe5b398 100644 --- a/src/utils/session-store.ts +++ b/src/utils/session-store.ts @@ -79,7 +79,7 @@ async function deleteAllUserSessionsFromSessionStore( } async function deleteExpressSession(req: Request) { - req.session.destroy((err) => { + req.session?.destroy((err) => { if (err) { logger.error(ERROR_MESSAGES.FAILED_TO_DESTROY_SESSION(err)); } diff --git a/src/utils/state-machine.ts b/src/utils/state-machine.ts index cd4b0ab38..894572973 100644 --- a/src/utils/state-machine.ts +++ b/src/utils/state-machine.ts @@ -1,4 +1,10 @@ -import { createMachine, EventType, StateValue } from "xstate"; +import { + AnyMachineSnapshot, + createActor, + createMachine, + getNextSnapshot, + StateValue, +} from "xstate"; enum UserJourney { ChangeEmail = "changeEmail", @@ -9,88 +15,112 @@ enum UserJourney { AddMfaMethod = "addMfaMethod", } -type AccountManagementEvent = - | "VALUE_UPDATED" - | "VERIFY_CODE_SENT" - | "AUTHENTICATED" - | "RESEND_CODE" - | "SELECTED_APP" - | "SELECTED_SMS"; +enum EventType { + Authenticated = "AUTHENTICATED", + ValueUpdated = "VALUE_UPDATED", + VerifyCodeSent = "VERIFY_CODE_SENT", + SelectedApp = "SELECTED_APP", + SelectedSms = "SELECTED_SMS", + ResendCode = "RESEND_CODE", + Confirmation = "CONFIRMATION", +} + +type Event = { type: EventType }; interface StateAction { value: StateValue; events: EventType[]; } -const amStateMachine = createMachine({ +export const amStateMachine = createMachine({ + types: { + context: {} as object, + events: {} as Event, + }, + context: {}, id: "AM", initial: "AUTHENTICATE", states: { AUTHENTICATE: { on: { - AUTHENTICATED: "CHANGE_VALUE", + AUTHENTICATED: { + target: "CHANGE_VALUE", + }, }, }, CHANGE_VALUE: { on: { - VALUE_UPDATED: "CONFIRMATION", - VERIFY_CODE_SENT: "VERIFY_CODE", - SELECTED_APP: "APP", - SELECTED_SMS: "SMS", + VALUE_UPDATED: { + target: "CONFIRMATION", + }, + VERIFY_CODE_SENT: { + target: "VERIFY_CODE", + }, + SELECTED_APP: { + target: "APP", + }, + SELECTED_SMS: { + target: "SMS", + }, }, }, - APP: { + CONFIRMATION: { + type: "final", + }, + VERIFY_CODE: { on: { - VALUE_UPDATED: "CONFIRMATION", + VALUE_UPDATED: { + target: "CONFIRMATION", + }, + RESEND_CODE: { + target: "CHANGE_VALUE", + }, + VERIFY_CODE_SENT: { + target: "CHANGE_VALUE", + }, }, }, - SMS: { + APP: { on: { - VALUE_UPDATED: "CONFIRMATION", + VALUE_UPDATED: { + target: "CONFIRMATION", + }, }, }, - VERIFY_CODE: { + SMS: { on: { - VALUE_UPDATED: "CONFIRMATION", - RESEND_CODE: "CHANGE_VALUE", - VERIFY_CODE_SENT: "CHANGE_VALUE", + VALUE_UPDATED: { + target: "CONFIRMATION", + }, }, }, - CONFIRMATION: { type: "final" }, }, - predictableActionArguments: true, }); -function getNextState( - from: StateValue, - to: AccountManagementEvent -): StateAction { - const t = amStateMachine.transition(from, to); +function getNextEvents(snapshot: AnyMachineSnapshot) { + return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]; +} + +function getNextState(from: StateValue, to: EventType): StateAction { + const t = getNextSnapshot( + amStateMachine, + amStateMachine.resolveState({ value: from, context: {} }), + { type: to } + ); return { value: t.value, - events: t.nextEvents, + events: getNextEvents(t), }; } function getInitialState(): StateAction { + const actor = createActor(amStateMachine); + const initialState = actor.getSnapshot(); + return { - value: amStateMachine.initialState.value, - events: amStateMachine.initialState.nextEvents, + value: initialState.value, + events: getNextEvents(initialState), }; } -function canTransition( - currentState: StateValue, - event: AccountManagementEvent -): boolean { - return !!amStateMachine.transition(currentState, event).changed; -} - -export { - getNextState, - canTransition, - UserJourney, - getInitialState, - StateAction, - AccountManagementEvent, -}; +export { UserJourney, EventType, StateAction, getNextState, getInitialState }; diff --git a/test/unit/utils/state-machine.test.ts b/test/unit/utils/state-machine.test.ts index 75b5c01c0..19bea96b2 100644 --- a/test/unit/utils/state-machine.test.ts +++ b/test/unit/utils/state-machine.test.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { describe } from "mocha"; import { + EventType, getInitialState, getNextState, } from "../../../src/utils/state-machine"; @@ -11,63 +12,63 @@ describe("state-machine", () => { const state = getInitialState(); expect(state.value).to.equal("AUTHENTICATE"); - expect(state.events).to.all.members(["AUTHENTICATED"]); + expect(state.events).to.all.members([EventType.Authenticated]); }); }); describe("getNextState with code verification", () => { it("should move state from initial state to change value state", () => { const state = getInitialState(); - const nextState = getNextState(state.value, "AUTHENTICATED"); + const nextState = getNextState(state.value, EventType.Authenticated); expect(nextState.value).to.equal("CHANGE_VALUE"); expect(nextState.events).to.all.members([ - "VALUE_UPDATED", - "VERIFY_CODE_SENT", - "SELECTED_APP", - "SELECTED_SMS", + EventType.ValueUpdated, + EventType.VerifyCodeSent, + EventType.SelectedApp, + EventType.SelectedSms, ]); }); it("should move state from change value state to verify code state", () => { - const nextState = getNextState("CHANGE_VALUE", "VERIFY_CODE_SENT"); + const nextState = getNextState("CHANGE_VALUE", EventType.VerifyCodeSent); expect(nextState.value).to.equal("VERIFY_CODE"); expect(nextState.events).to.all.members([ - "VALUE_UPDATED", + EventType.ValueUpdated, "RESEND_CODE", - "VERIFY_CODE_SENT", + EventType.VerifyCodeSent, ]); }); it("should move state from verify code state to value updated state", () => { - const nextState = getNextState("VERIFY_CODE", "VALUE_UPDATED"); - expect(nextState.value).to.equal("CONFIRMATION"); + const nextState = getNextState("VERIFY_CODE", EventType.ValueUpdated); + expect(nextState.value).to.equal(EventType.Confirmation); expect(nextState.events).to.all.members([]); }); it("should not allow getNext state to skip state", () => { const state = getInitialState(); - const nextState = getNextState(state.value, "VERIFY_CODE_SENT"); + const nextState = getNextState(state.value, EventType.VerifyCodeSent); expect(nextState.value).to.equal("AUTHENTICATE"); - expect(nextState.events).to.all.members(["AUTHENTICATED"]); + expect(nextState.events).to.all.members([EventType.Authenticated]); }); }); describe("getNextState without code verification", () => { it("should move state from initial state to change value state", () => { const state = getInitialState(); - const nextState = getNextState(state.value, "AUTHENTICATED"); + const nextState = getNextState(state.value, EventType.Authenticated); expect(nextState.value).to.equal("CHANGE_VALUE"); expect(nextState.events).to.all.members([ - "VALUE_UPDATED", - "VERIFY_CODE_SENT", - "SELECTED_APP", - "SELECTED_SMS", + EventType.ValueUpdated, + EventType.VerifyCodeSent, + EventType.SelectedApp, + EventType.SelectedSms, ]); }); it("should move state from change value state to value updated state", () => { - const nextState = getNextState("CHANGE_VALUE", "VALUE_UPDATED"); - expect(nextState.value).to.equal("CONFIRMATION"); + const nextState = getNextState("CHANGE_VALUE", EventType.ValueUpdated); + expect(nextState.value).to.equal(EventType.Confirmation); expect(nextState.events).to.all.members([]); }); }); @@ -75,31 +76,31 @@ describe("state-machine", () => { describe("getNextState for mfa process", () => { it("should move state from initial state to change value state", () => { const state = getInitialState(); - const nextState = getNextState(state.value, "AUTHENTICATED"); + const nextState = getNextState(state.value, EventType.Authenticated); expect(nextState.value).to.equal("CHANGE_VALUE"); expect(nextState.events).to.all.members([ - "VALUE_UPDATED", - "VERIFY_CODE_SENT", - "SELECTED_APP", - "SELECTED_SMS", + EventType.ValueUpdated, + EventType.VerifyCodeSent, + EventType.SelectedApp, + EventType.SelectedSms, ]); }); it("should move state from CHANGE_VALUE state to APP state when SELECTED_APP action event ", () => { - const nextState = getNextState("CHANGE_VALUE", "SELECTED_APP"); + const nextState = getNextState("CHANGE_VALUE", EventType.SelectedApp); expect(nextState.value).to.equal("APP"); - expect(nextState.events).to.all.members(["VALUE_UPDATED"]); + expect(nextState.events).to.all.members([EventType.ValueUpdated]); }); it("should move state from CHANGE_VALUE state to SMS state when SELECTED_SMS action event ", () => { - const nextState = getNextState("CHANGE_VALUE", "SELECTED_SMS"); + const nextState = getNextState("CHANGE_VALUE", EventType.SelectedSms); expect(nextState.value).to.equal("SMS"); - expect(nextState.events).to.all.members(["VALUE_UPDATED"]); + expect(nextState.events).to.all.members([EventType.ValueUpdated]); }); it("should move state from APP state to CONFIRMATION state when VALUE_UPDATED action event ", () => { - const nextState = getNextState("APP", "VALUE_UPDATED"); - expect(nextState.value).to.equal("CONFIRMATION"); + const nextState = getNextState("APP", EventType.ValueUpdated); + expect(nextState.value).to.equal(EventType.Confirmation); expect(nextState.events).to.all.members([]); }); });