From 1b5e7f210deaeb14286067efcb7c361ecf135400 Mon Sep 17 00:00:00 2001 From: pskushwaha1 <144677445+pskushwaha1@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:58:24 +0000 Subject: [PATCH] AUT-1706: Remove form-action header from /enter-code page --- .github/workflows/build-and-push-frontend.yml | 14 +- .github/workflows/deploy-frontend.yml | 11 +- src/app.constants.ts | 5 + src/app.ts | 5 +- .../contact-us/contact-us-controller.ts | 14 +- .../contact-us-questions-validation.ts | 33 ++- .../contact-us-service-smart-agent.ts | 5 + .../contact-us/contact-us-service.ts | 5 + .../questions/_sign_in_phone_number_issue.njk | 53 +++- .../contact-us-controller-integration.test.ts | 270 +++++++++++------- .../contact-us-questions-controller.test.ts | 18 ++ src/components/contact-us/types.ts | 2 + src/config/helmet.ts | 27 +- src/locales/cy/translation.json | 18 +- src/locales/en/translation.json | 18 +- src/middleware/set-csp-headers-middleware.ts | 11 + src/utils/validation.ts | 2 + 17 files changed, 370 insertions(+), 141 deletions(-) create mode 100644 src/middleware/set-csp-headers-middleware.ts diff --git a/.github/workflows/build-and-push-frontend.yml b/.github/workflows/build-and-push-frontend.yml index 1b39f2786f..3e7846143a 100644 --- a/.github/workflows/build-and-push-frontend.yml +++ b/.github/workflows/build-and-push-frontend.yml @@ -1,6 +1,8 @@ name: Build frontend env: - DEPLOYER_ROLE: arn:aws:iam::114407264696:role/deployers/github-actions-publish-to-s3-for-code-signing + AWS_REGION: eu-west-2 + +#Deployer role is github actions publish code signing role & ECR repo are from Prod AWS Tooling acct on: push: @@ -17,18 +19,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up AWS credentials + - name: Assume AWS DEPLOYER role in tooling acct uses: aws-actions/configure-aws-credentials@v1-node16 with: - role-to-assume: ${{ env.DEPLOYER_ROLE }} - aws-region: eu-west-2 + role-to-assume: ${{ secrets.DEPLOYER_ROLE }} + aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push frontend env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: frontend-image-repository + ECR_REPOSITORY: ${{ secrets.TOOLING_ECR_FRONTEND_REPO }} IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . @@ -37,7 +39,7 @@ jobs: working-directory: basic-auth-sidecar env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: basic-auth-sidecar-image-repository + ECR_REPOSITORY: ${{ secrets.BASIC_SIDECAR_ECR_REPO }} IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index 3839da6ae0..fde101fe6a 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -1,8 +1,9 @@ name: Deploy frontend env: - DEPLOY_ROLE: arn:aws:iam::761723964695:role/build-auth-deploy-pipeline-GitHubActionsRole-160U5ADTRKQ2O - ARTIFACT_BUCKET: build-auth-deploy-pipeli-githubartifactsourcebuck-1o4hcrnik6ayv + AWS_REGION: eu-west-2 + +# Deploy role & Artificate buckets are Logical id GitHubActionsRole & GitHubArtifactSourceBucket Value from Build Pipeline on: push: @@ -28,15 +29,15 @@ jobs: - name: Set up AWS credentials uses: aws-actions/configure-aws-credentials@v2 with: - role-to-assume: ${{ env.DEPLOY_ROLE }} - aws-region: eu-west-2 + role-to-assume: ${{ secrets.DEPLOY_ROLE }} + aws-region: ${{ env.AWS_REGION }} - name: Upload frontend Terraform files working-directory: ci/terraform run: | zip -r frontend.zip . S3_RESPONSE=`aws s3api put-object \ - --bucket ${{ env.ARTIFACT_BUCKET }} \ + --bucket ${{ secrets.ARTIFACT_BUCKET }} \ --key frontend.zip \ --body frontend.zip \ --metadata "repository=$GITHUB_REPOSITORY,commitsha=$GITHUB_SHA,committag=$GIT_TAG,commitmessage=$COMMIT_MSG"` diff --git a/src/app.constants.ts b/src/app.constants.ts index 8a0caa785e..8649a788db 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -171,12 +171,17 @@ export const ZENDESK_THEMES = { }; export const ZENDESK_FIELD_MAX_LENGTH = 1200; +export const ZENDESK_COUNTRY_MAX_LENGTH = 256; export const PLACEHOLDER_REPLACEMENTS = [ { search: "[maximumCharacters]", replacement: ZENDESK_FIELD_MAX_LENGTH.toLocaleString(), }, + { + search: "[maximumCountryCharacters]", + replacement: ZENDESK_COUNTRY_MAX_LENGTH.toLocaleString(), + }, ]; export enum NOTIFICATION_TYPE { diff --git a/src/app.ts b/src/app.ts index 075cd8936b..f206a29035 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,8 +8,6 @@ import i18nextMiddleware from "i18next-http-middleware"; import * as path from "path"; import { configureNunjucks } from "./config/nunchucks"; import { i18nextConfigurationOptions } from "./config/i18next"; -import { helmetConfiguration } from "./config/helmet"; -import helmet from "helmet"; import { setHtmlLangMiddleware } from "./middleware/html-lang-middleware"; import i18next from "i18next"; @@ -83,6 +81,7 @@ import { setInternationalPhoneNumberSupportMiddleware } from "./middleware/set-i import { checkYourEmailSecurityCodesRouter } from "./components/account-recovery/check-your-email-security-codes/check-your-email-security-codes-routes"; import { changeSecurityCodesConfirmationRouter } from "./components/account-recovery/change-security-codes-confirmation/change-security-codes-confirmation-routes"; import { outboundContactUsLinksMiddleware } from "./middleware/outbound-contact-us-links-middleware"; +import { setCspHeaders } from "./middleware/set-csp-headers-middleware"; const APP_VIEWS = [ path.join(__dirname, "components"), @@ -164,7 +163,7 @@ async function createApp(): Promise { ); app.use(i18nextMiddleware.handle(i18next)); - app.use(helmet(helmetConfiguration())); + app.use(setCspHeaders); const redisConfig = isProduction ? await getRedisConfig(getAppEnv()) diff --git a/src/components/contact-us/contact-us-controller.ts b/src/components/contact-us/contact-us-controller.ts index 41aabc953c..088adcdde0 100644 --- a/src/components/contact-us/contact-us-controller.ts +++ b/src/components/contact-us/contact-us-controller.ts @@ -4,6 +4,7 @@ import { SUPPORT_TYPE, ZENDESK_THEMES, ZENDESK_FIELD_MAX_LENGTH, + ZENDESK_COUNTRY_MAX_LENGTH, CONTACT_US_REFERER_ALLOWLIST, } from "../../app.constants"; import { contactUsService } from "./contact-us-service"; @@ -355,6 +356,7 @@ export function contactUsQuestionsGet(req: Request, res: Response): void { }), pageTitleHeading: pageTitle, zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: res.locals.ipnSupport, appErrorCode: getAppErrorCode(req.query.appErrorCode as string), appSessionId: getAppSessionId(req.query.appSessionId as string), @@ -395,6 +397,7 @@ export function contactUsQuestionsFormPostToSmartAgent( optionalDescription: req.body.optionalDescription, moreDetailDescription: req.body.moreDetailDescription, serviceTryingToUse: req.body.serviceTryingToUse, + countryPhoneNumberFrom: req.body.countryPhoneNumberFrom, }, themes: { theme: req.body.theme, subtheme: req.body.subtheme }, subject: "GOV.UK One Login", @@ -449,6 +452,7 @@ export function contactUsQuestionsFormPostToZendesk( optionalDescription: req.body.optionalDescription, moreDetailDescription: req.body.moreDetailDescription, serviceTryingToUse: req.body.serviceTryingToUse, + countryPhoneNumberFrom: req.body.countryPhoneNumberFrom, }, themes: { theme: req.body.theme, subtheme: req.body.subtheme }, subject: "GOV.UK One Login", @@ -566,10 +570,18 @@ export function getQuestionsFromFormTypeForMessageBody( ), }, signInPhoneNumberIssue: { - moreDetailDescription: req.t( + issueDescription: req.t( "pages.contactUsQuestions.signInPhoneNumberIssue.section1.header", { lng: "en" } ), + additionalDescription: req.t( + "pages.contactUsQuestions.signInPhoneNumberIssue.section2.header", + { lng: "en" } + ), + countryPhoneNumberFrom: req.t( + "pages.contactUsQuestions.signInPhoneNumberIssue.section3.header", + { lng: "en" } + ), }, signingInProblem: { issueDescription: req.t( diff --git a/src/components/contact-us/contact-us-questions-validation.ts b/src/components/contact-us/contact-us-questions-validation.ts index 02f1e667cd..02ec29777a 100644 --- a/src/components/contact-us/contact-us-questions-validation.ts +++ b/src/components/contact-us/contact-us-questions-validation.ts @@ -1,7 +1,11 @@ import { body, check } from "express-validator"; import { validateBodyMiddleware } from "../../middleware/form-validation-middleware"; import { ValidationChainFunc } from "../../types"; -import { ZENDESK_FIELD_MAX_LENGTH, ZENDESK_THEMES } from "../../app.constants"; +import { + ZENDESK_FIELD_MAX_LENGTH, + ZENDESK_THEMES, + ZENDESK_COUNTRY_MAX_LENGTH, +} from "../../app.constants"; import { supportWelshInSupportForms } from "../../config"; export function setLanguageToReflectSupportForWelsh( @@ -150,6 +154,24 @@ export function validateContactUsQuestionsRequest(): ValidationChainFunc { { value, lng: setLanguageToReflectSupportForWelsh(req.i18n.lng) } ); }), + body("countryPhoneNumberFrom") + .optional() + .notEmpty() + .withMessage((value, { req }) => { + return req.t( + "pages.contactUsQuestions.signInPhoneNumberIssue.section3.ifBlankErrorMessage", + { value, lng: setLanguageToReflectSupportForWelsh(req.i18n.lng) } + ); + }), + body("countryPhoneNumberFrom") + .optional() + .isLength({ max: ZENDESK_COUNTRY_MAX_LENGTH }) + .withMessage((value, { req }) => { + return req.t( + "pages.contactUsQuestions.signInPhoneNumberIssue.section3.ifTooLongErrorMessage", + { value, lng: setLanguageToReflectSupportForWelsh(req.i18n.lng) } + ); + }), validateBodyMiddleware("contact-us/questions/index.njk"), ]; } @@ -223,6 +245,9 @@ export function getErrorMessageForAccountCreationIssueDescription( if (subtheme === ZENDESK_THEMES.AUTHENTICATOR_APP_PROBLEM) { return "pages.contactUsQuestions.anotherProblem.section1.errorMessage"; } + if (subtheme === ZENDESK_THEMES.SIGN_IN_PHONE_NUMBER_ISSUE) { + return "pages.contactUsQuestions.signInPhoneNumberIssue.section1.errorMessage"; + } } export function getErrorMessageForSigningInIssueDescription( @@ -294,6 +319,9 @@ export function getLengthExceededErrorMessageForAccountCreationIssueDescription( if (subtheme === ZENDESK_THEMES.SOMETHING_ELSE) { return "pages.contactUsQuestions.issueDescriptionErrorMessage.anythingElseTooLongMessage"; } + if (subtheme === ZENDESK_THEMES.SIGN_IN_PHONE_NUMBER_ISSUE) { + return "pages.contactUsQuestions.issueDescriptionErrorMessage.entryTooLongMessage"; + } } export function getLengthExceededErrorMessageForIdCheckAppIssueDescription( @@ -365,4 +393,7 @@ export function getErrorMessageForAdditionalDescription( ) { return "pages.contactUsQuestions.provingIdentityFaceToFaceSomethingElse.section2.errorMessage"; } + if (subtheme === ZENDESK_THEMES.SIGN_IN_PHONE_NUMBER_ISSUE) { + return "pages.contactUsQuestions.signInPhoneNumberIssue.section2.errorMessage"; + } } diff --git a/src/components/contact-us/contact-us-service-smart-agent.ts b/src/components/contact-us/contact-us-service-smart-agent.ts index a17df7bdaa..b0ffe4f865 100644 --- a/src/components/contact-us/contact-us-service-smart-agent.ts +++ b/src/components/contact-us/contact-us-service-smart-agent.ts @@ -123,6 +123,11 @@ export function contactUsServiceSmartAgent( message.push(`

${descriptions.moreDetailDescription}

`); } + if (descriptions.countryPhoneNumberFrom) { + message.push(`

${questions.countryPhoneNumberFrom}

`); + message.push(`

${descriptions.countryPhoneNumberFrom}

`); + } + return message.join(""); } diff --git a/src/components/contact-us/contact-us-service.ts b/src/components/contact-us/contact-us-service.ts index ace9ac35a9..ae716f3f3f 100644 --- a/src/components/contact-us/contact-us-service.ts +++ b/src/components/contact-us/contact-us-service.ts @@ -208,6 +208,11 @@ export function contactUsService( htmlBody.push(`

${descriptions.serviceTryingToUse}

`); } + if (descriptions.countryPhoneNumberFrom) { + htmlBody.push(`[${questions.countryPhoneNumberFrom}]`); + htmlBody.push(`

${descriptions.countryPhoneNumberFrom}

`); + } + htmlBody.push(`[Ticket Identifier]`); if (optionalData.ticketIdentifier) { htmlBody.push(`

${optionalData.ticketIdentifier}

`); diff --git a/src/components/contact-us/questions/_sign_in_phone_number_issue.njk b/src/components/contact-us/questions/_sign_in_phone_number_issue.njk index 627b9c3252..c6a62e6c6c 100644 --- a/src/components/contact-us/questions/_sign_in_phone_number_issue.njk +++ b/src/components/contact-us/questions/_sign_in_phone_number_issue.njk @@ -18,13 +18,36 @@ text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section1.header' | translateEnOnly, classes: "govuk-label--s" }, + id: "issueDescription", + name: "issueDescription", + maxlength: zendeskFieldMaxLength, + value: issueDescription, + charactersAtLimitText: 'pages.contactUsQuestions.characterCountComponent.charactersAtLimitText' | translateEnOnly, + charactersUnderLimitText: { + other: 'pages.contactUsQuestions.characterCountComponent.charactersUnderLimitText.other' | translateEnOnly, + one: 'pages.contactUsQuestions.characterCountComponent.charactersUnderLimitText.one' | translateEnOnly + }, + charactersOverLimitText: { + other: 'pages.contactUsQuestions.characterCountComponent.charactersOverLimitText.other' | translateEnOnly, + one: 'pages.contactUsQuestions.characterCountComponent.charactersOverLimitText.one' | translateEnOnly + }, + errorMessage: { + text: errors['issueDescription'].text | translateEnOnly | replace('[maximumCharacters]', zendeskFieldMaxLength.toLocaleString()) + } if (errors['issueDescription']) +}) }} + +{{ govukCharacterCount({ + label: { + text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section2.header' | translateEnOnly, + classes: "govuk-label--s" + }, hint: { - text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section1.paragraph1' | translateEnOnly + text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section2.hintText' | translateEnOnly }, - id: "moreDetailDescription", - name: "moreDetailDescription", + id: "additionalDescription", + name: "additionalDescription", maxlength: zendeskFieldMaxLength, - value: moreDetailDescription, + value: additionalDescription, charactersAtLimitText: 'pages.contactUsQuestions.characterCountComponent.charactersAtLimitText' | translateEnOnly, charactersUnderLimitText: { other: 'pages.contactUsQuestions.characterCountComponent.charactersUnderLimitText.other' | translateEnOnly, @@ -35,12 +58,28 @@ one: 'pages.contactUsQuestions.characterCountComponent.charactersOverLimitText.one' | translateEnOnly }, errorMessage: { - text: errors['moreDetailDescription'].text | translateEnOnly | replace('[maximumCharacters]', zendeskFieldMaxLength.toLocaleString()) - } if (errors['moreDetailDescription']) + text: errors['additionalDescription'].text | translateEnOnly | replace('[maximumCharacters]', zendeskFieldMaxLength.toLocaleString()) + } if (errors['additionalDescription']) +}) }} + +{{ govukInput({ + label: { + text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section3.header' | translateEnOnly, + classes: "govuk-label--s" + }, + hint: { + text: 'pages.contactUsQuestions.signInPhoneNumberIssue.section3.hintText' | translateEnOnly + }, + id: "countryPhoneNumberFrom", + name: "countryPhoneNumberFrom", + value: countryPhoneNumberFrom, + errorMessage: { + text: errors['countryPhoneNumberFrom'].text | translateEnOnly + } if (errors['countryPhoneNumberFrom']) }) }} {{ govukWarningText({ - text:'pages.contactUsQuestions.personalInformation.paragraph1' | translateEnOnly, + text:'pages.contactUsQuestions.signInPhoneNumberIssue.personalInformation.paragraph' | translateEnOnly, iconFallbackText: "Warning" }) }} diff --git a/src/components/contact-us/tests/contact-us-controller-integration.test.ts b/src/components/contact-us/tests/contact-us-controller-integration.test.ts index 76cbf99160..67e77c4858 100644 --- a/src/components/contact-us/tests/contact-us-controller-integration.test.ts +++ b/src/components/contact-us/tests/contact-us-controller-integration.test.ts @@ -49,6 +49,25 @@ describe("Integration:: contact us - public user", () => { app = undefined; }); + const expectValidationErrorOnPost = ( + url: string, + data: Record, + errorElement: string, + errorDescription: string, + done: Mocha.Done + ) => { + request(app) + .post(url) + .type("form") + .set("Cookie", cookies) + .send(data) + .expect(function (res) { + const $ = cheerio.load(res.text); + expect($(errorElement).text()).to.contains(errorDescription); + }) + .expect(400, done); + }; + it("should return contact us page", (done) => { request(app) .get(PATH_NAMES.CONTACT_US) @@ -175,130 +194,171 @@ describe("Integration:: contact us - public user", () => { }); it("should return validation error when no radio boxes are selected on the signing in contact-us-further-information page", (done) => { - request(app) - .post("/contact-us-further-information") - .type("form") - .set("Cookie", cookies) - .send({ - _csrf: token, - theme: "signing_in", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#subtheme-error").text()).to.contains( - "Select the problem you had when signing in to your GOV.UK One Login" - ); - }) - .expect(400, done); + const data = { + _csrf: token, + theme: "signing_in", + }; + expectValidationErrorOnPost( + "/contact-us-further-information", + data, + "#subtheme-error", + "Select the problem you had when signing in to your GOV.UK One Login", + done + ); }); it("should return validation error when issue description are not entered on the contact-us-questions page", (done) => { - request(app) - .post("/contact-us-questions") - .type("form") - .set("Cookie", cookies) - .send({ - _csrf: token, - issueDescription: "", - theme: "signing_in", - subtheme: "technical_error", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#issueDescription-error").text()).to.contains( - "Enter what you were trying to do" - ); - }) - .expect(400, done); + const data = { + _csrf: token, + issueDescription: "", + theme: "signing_in", + subtheme: "technical_error", + }; + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#issueDescription-error", + "Enter what you were trying to do", + done + ); }); it("should return validation error when user selected yes to contact for feedback and left email field empty", (done) => { - request(app) - .post("/contact-us-questions") - .type("form") - .set("Cookie", cookies) - .send({ - _csrf: token, - theme: "signing_in", - subtheme: "something_else", - issueDescription: "issue", - additionalDescription: "additional", - contact: "true", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#email-error").text()).to.contains( - "Enter your email address" - ); - }) - .expect(400, done); + const data = { + _csrf: token, + theme: "signing_in", + subtheme: "something_else", + issueDescription: "issue", + additionalDescription: "additional", + contact: "true", + }; + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#email-error", + "Enter your email address", + done + ); }); it("should return validation error when user selected yes to contact for feedback but email is in an invalid format", (done) => { - request(app) - .post("/contact-us-questions") - .type("form") - .set("Cookie", cookies) - .send({ - _csrf: token, - theme: "signing_in", - subtheme: "something_else", - issueDescription: "issue", - additionalDescription: "additional", - contact: "true", - email: "test", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#email-error").text()).to.contains( - "Enter an email address in the correct format, like name@example.com" - ); - }) - .expect(400, done); + const data = { + _csrf: token, + theme: "signing_in", + subtheme: "something_else", + issueDescription: "issue", + additionalDescription: "additional", + contact: "true", + email: "test", + }; + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#email-error", + "Enter an email address in the correct format, like name@example.com", + done + ); }); it("should return validation error when user has not selected how the security code was sent whilst creating an account", (done) => { - request(app) - .post("/contact-us-questions?radio_buttons=true") - .type("form") - .set("Cookie", cookies) - .send({ - _csrf: token, - theme: "account_creation", - subtheme: "no_security_code", - moreDetailDescription: "issue", - formType: "noSecurityCode", - contact: "false", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#securityCodeSentMethod-error").text()).to.contains( - "Select whether you expected to get the code by email, text message or authenticator app" - ); - }) - .expect(400, done); + const data = { + _csrf: token, + theme: "account_creation", + subtheme: "no_security_code", + moreDetailDescription: "issue", + formType: "noSecurityCode", + contact: "false", + }; + expectValidationErrorOnPost( + "/contact-us-questions?radio_buttons=true", + data, + "#securityCodeSentMethod-error", + "Select whether you expected to get the code by email, text message or authenticator app", + done + ); }); describe("when a user had a problem taking a photo of your identity document using the GOV.UK ID Check app", () => { it("should return validation error when user has not selected which identity document they were using", (done) => { + const data = { + _csrf: token, + theme: "id_check_app", + subtheme: "taking_photo_of_id_problem", + moreDetailDescription: "There was a problem", + contact: "false", + }; + expectValidationErrorOnPost( + "/contact-us-questions?radio_buttons=true", + data, + "#identityDocumentUsed-error", + "Select which identity document you were using", + done + ); + }); + }); + + describe("when a user had a problem with their phone number when creating an account", () => { + const phoneNumberIssueData = ( + issueDescription: string, + additionalDescription: string, + countryPhoneNumberFrom: string + ) => { + return { + _csrf: token, + theme: "account_creation", + subtheme: "sign_in_phone_number_issue", + issueDescription: issueDescription, + additionalDescription: additionalDescription, + countryPhoneNumberFrom: countryPhoneNumberFrom, + contact: "false", + formType: "signInPhoneNumberIssue", + referer: "https://gov.uk/sign-in", + }; + }; + + it("should return validation error when user has not entered what they were trying to do", (done) => { + const data = phoneNumberIssueData("", "additional detail", "UK"); + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#issueDescription-error", + "Enter what you were trying to do", + done + ); + }); + + it("should return validation error when user has not entered what happened", (done) => { + const data = phoneNumberIssueData("more detail", "", "UK"); + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#additionalDescription-error", + "Enter what happened", + done + ); + }); + + it("should return validation error when user has not entered country the phone number is from", (done) => { + const data = phoneNumberIssueData("more detail", "additional detail", ""); + expectValidationErrorOnPost( + "/contact-us-questions", + data, + "#countryPhoneNumberFrom-error", + "Enter which country your phone number is from", + done + ); + }); + + it("should redirect to success page when valid form submitted", (done) => { + nock(zendeskApiUrl).post("/tickets.json").once().reply(200); + request(app) - .post("/contact-us-questions?radio_buttons=true") + .post("/contact-us-questions") .type("form") .set("Cookie", cookies) - .send({ - _csrf: token, - theme: "id_check_app", - subtheme: "taking_photo_of_id_problem", - moreDetailDescription: "There was a problem", - contact: "false", - }) - .expect(function (res) { - const $ = cheerio.load(res.text); - expect($("#identityDocumentUsed-error").text()).to.contains( - "Select which identity document you were using" - ); - }) - .expect(400, done); + .send(phoneNumberIssueData("detail", "description", "UK")) + .expect("Location", PATH_NAMES.CONTACT_US_SUBMIT_SUCCESS) + .expect(302, done); }); }); diff --git a/src/components/contact-us/tests/contact-us-questions-controller.test.ts b/src/components/contact-us/tests/contact-us-questions-controller.test.ts index 582bbe3a93..8226e0f61a 100644 --- a/src/components/contact-us/tests/contact-us-questions-controller.test.ts +++ b/src/components/contact-us/tests/contact-us-questions-controller.test.ts @@ -11,6 +11,7 @@ import { ZENDESK_THEMES, ZENDESK_FIELD_MAX_LENGTH, PATH_NAMES, + ZENDESK_COUNTRY_MAX_LENGTH, } from "../../../app.constants"; import { ContactUsServiceInterface } from "../types"; import { RequestGet, ResponseRedirect } from "../../../types"; @@ -59,6 +60,7 @@ describe("contact us questions controller", () => { pageTitleHeading: "pages.contactUsQuestions.anotherProblem.title", referer: REFERER, zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -78,6 +80,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.emailSubscriptions.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -97,6 +100,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.suggestionOrFeedback.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -117,6 +121,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.provingIdentity.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -146,6 +151,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.noSecurityCode.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -166,6 +172,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.invalidSecurityCode.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -186,6 +193,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.noPhoneNumberAccess.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -206,6 +214,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.forgottenPassword.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -226,6 +235,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.accountNotFound.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -246,6 +256,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.technicalError.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -266,6 +277,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.anotherProblem.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -289,6 +301,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.noSecurityCode.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -309,6 +322,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.invalidSecurityCode.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -330,6 +344,7 @@ describe("contact us questions controller", () => { pageTitleHeading: "pages.contactUsQuestions.signInPhoneNumberIssue.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -350,6 +365,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.technicalError.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -370,6 +386,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.accountCreation.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", @@ -390,6 +407,7 @@ describe("contact us questions controller", () => { referer: REFERER, pageTitleHeading: "pages.contactUsQuestions.authenticatorApp.title", zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, ipnSupport: undefined, appErrorCode: "", appSessionId: "", diff --git a/src/components/contact-us/types.ts b/src/components/contact-us/types.ts index 94597df47b..aab62d4e5d 100644 --- a/src/components/contact-us/types.ts +++ b/src/components/contact-us/types.ts @@ -29,6 +29,7 @@ export interface Questions { moreDetailDescription?: string; radioButtons?: string; serviceTryingToUse?: string; + countryPhoneNumberFrom?: string; } export interface ThemeQuestions { @@ -42,6 +43,7 @@ export interface Descriptions { optionalDescription?: string; moreDetailDescription?: string; serviceTryingToUse?: string; + countryPhoneNumberFrom?: string; } export interface Themes { diff --git a/src/config/helmet.ts b/src/config/helmet.ts index 57b8212eef..ba633ba61c 100644 --- a/src/config/helmet.ts +++ b/src/config/helmet.ts @@ -2,7 +2,9 @@ import helmet from "helmet"; import e, { Request, Response } from "express"; import { supportFrameAncestorsFormActionsCspHeaders } from "../config"; // Helmet does not export the config type - This is the way the recommend getting it on GitHub. -export function helmetConfiguration(): Parameters[0] { +export function helmetConfiguration( + req: Request +): Parameters[0] { const helmetConfig: { permittedCrossDomainPolicies: boolean; referrerPolicy: boolean; @@ -63,14 +65,21 @@ export function helmetConfiguration(): Parameters[0] { expectCt: false, }; if (supportFrameAncestorsFormActionsCspHeaders()) { - helmetConfig.contentSecurityPolicy.directives["frame-ancestors"] = [ - "'self'", - "https://*.account.gov.uk", - ]; - helmetConfig.contentSecurityPolicy.directives["form-action"] = [ - "'self'", - "https://*.account.gov.uk", - ]; + if (req.url == "/enter-code") { + helmetConfig.contentSecurityPolicy.directives["frame-ancestors"] = [ + "'self'", + "https://*.account.gov.uk", + ]; + } else { + helmetConfig.contentSecurityPolicy.directives["frame-ancestors"] = [ + "'self'", + "https://*.account.gov.uk", + ]; + helmetConfig.contentSecurityPolicy.directives["form-action"] = [ + "'self'", + "https://*.account.gov.uk", + ]; + } } return helmetConfig; } diff --git a/src/locales/cy/translation.json b/src/locales/cy/translation.json index 735a0a3052..da1ffd3872 100644 --- a/src/locales/cy/translation.json +++ b/src/locales/cy/translation.json @@ -1826,8 +1826,22 @@ "title": "Problem arall gyda rhif ffôn", "header": "Problem arall gyda rhif ffôn", "section1": { - "header": "Ydych chi eisiau dweud unrhyw beth arall wrthym", - "paragraph1": "Gallwch ychwanegu mwy o fanylion, fel beth oeddech yn ceisio ei wneud, neu roi adborth i ni" + "header": "Beth oeddech chi’n ceisio ei wneud?", + "errorMessage": "Rhowch yr hyn roeddech yn ceisio ei wneud" + }, + "section2": { + "header": "Beth ddigwyddodd?", + "hintText": "Er enghraifft ni dderbyniwyd eich rhif ffôn, neu fe gawsoch drafferth rhoi cod gwlad i mewn", + "errorMessage": "Rhowch beth ddigwyddodd" + }, + "section3": { + "header": "O ba wlad mae eich rhif ffôn?", + "hintText": "Peidiwch â rhoi eich rhif ffôn", + "ifBlankErrorMessage": "Rhowch o ba wlad y mae eich rhif ffôn yn dod", + "ifTooLongErrorMessage": "Rhowch y wlad gan ddefnyddio llai na 256 nod" + }, + "personalInformation": { + "paragraph": "Peidiwch â chynnwys gwybodaeth bersonol fel eich rhif ffôn, manylion pasbort neu fanylion cardiau credyd." } }, "issueDescriptionErrorMessage": { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 7783b1b43b..a7691353eb 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1826,8 +1826,22 @@ "title": "Another problem with a phone number", "header": "Another problem with a phone number", "section1": { - "header": "Anything else you want to tell us", - "paragraph1": "You can add more detail, such as what you were trying to do, or give us feedback" + "header": "What were you trying to do", + "errorMessage": "Enter what you were trying to do" + }, + "section2": { + "header": "What happened?", + "hintText": "For example your phone number was not accepted, or you had trouble entering a country code", + "errorMessage": "Enter what happened" + }, + "section3": { + "header": "What country is your phone number from?", + "hintText": "Do not enter your phone number", + "ifBlankErrorMessage": "Enter which country your phone number is from", + "ifTooLongErrorMessage": "Enter the country using less than 256 characters" + }, + "personalInformation": { + "paragraph": "Do not include personal information such as your phone number, passport details or credit card details." } }, "issueDescriptionErrorMessage": { diff --git a/src/middleware/set-csp-headers-middleware.ts b/src/middleware/set-csp-headers-middleware.ts new file mode 100644 index 0000000000..51f1c32bb3 --- /dev/null +++ b/src/middleware/set-csp-headers-middleware.ts @@ -0,0 +1,11 @@ +import { NextFunction, Request, Response } from "express"; +import helmet from "helmet"; +import { helmetConfiguration } from "../config/helmet"; + +export function setCspHeaders( + req: Request, + res: Response, + next: NextFunction +): void { + helmet(helmetConfiguration(req))(req, res, next); +} diff --git a/src/utils/validation.ts b/src/utils/validation.ts index dc2370be0d..a9f8cf7506 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -3,6 +3,7 @@ import { HTTP_STATUS_CODES, PLACEHOLDER_REPLACEMENTS, ZENDESK_FIELD_MAX_LENGTH, + ZENDESK_COUNTRY_MAX_LENGTH, } from "../app.constants"; import { Error, PlaceholderReplacement } from "../types"; @@ -98,6 +99,7 @@ export function renderBadRequest( ...convertStringBooleanPropertiesToJavaScriptBoolean(req.body), language: req.i18n.language, zendeskFieldMaxLength: ZENDESK_FIELD_MAX_LENGTH, + zendeskCountryMaxLength: ZENDESK_COUNTRY_MAX_LENGTH, }; const params = postValidationLocals