diff --git a/src/components/authorize/authorize-controller.ts b/src/components/authorize/authorize-controller.ts index 3d0baedd5..42e41986f 100644 --- a/src/components/authorize/authorize-controller.ts +++ b/src/components/authorize/authorize-controller.ts @@ -8,10 +8,7 @@ import { import { getNextPathAndUpdateJourney, ERROR_CODES } from "../common/constants"; import { BadRequestError, QueryParamsError } from "../../utils/error"; import { ExpressRouteFunc } from "../../types"; -import { - CookieConsentModel, - CookieConsentServiceInterface, -} from "../common/cookie-consent/types"; +import { CookieConsentServiceInterface } from "../common/cookie-consent/types"; import { cookieConsentService } from "../common/cookie-consent/cookie-consent-service"; import { sanitize } from "../../utils/strings"; import { USER_JOURNEY_EVENTS } from "../common/state-machine/state-machine"; @@ -31,21 +28,9 @@ import { import { logger } from "../../utils/logger"; import { Claims } from "./claims-config"; -function createConsentCookie( - res: Response, - consentCookieValue: CookieConsentModel -) { - res.cookie(COOKIES_PREFERENCES_SET, consentCookieValue.value, { - expires: consentCookieValue.expiry, - secure: true, - httpOnly: true, - domain: res.locals.analyticsCookieDomain, - }); -} - export function authorizeGet( authService: AuthorizeServiceInterface = authorizeService(), - cookieService: CookieConsentServiceInterface = cookieConsentService(), + cookiesConsentService: CookieConsentServiceInterface = cookieConsentService(), kmsService: KmsDecryptionServiceInterface = new KmsDecryptionService(), jwtService: JwtServiceInterface = new JwtService() ): ExpressRouteFunc { @@ -164,9 +149,15 @@ export function authorizeGet( if (req.session.client.cookieConsentEnabled && cookieConsent) { const consentCookieValue = - cookieService.createConsentCookieValue(cookieConsent); - - createConsentCookie(res, consentCookieValue); + cookiesConsentService.createConsentCookieValue(cookieConsent); + + res.cookie(COOKIES_PREFERENCES_SET, consentCookieValue.value, { + expires: consentCookieValue.expires, + secure: true, + httpOnly: false, + domain: res.locals.analyticsCookieDomain, + encode: String, + }); if ( startAuthResponse.data.user.gaCrossDomainTrackingId && diff --git a/src/components/authorize/tests/authorize-controller.test.ts b/src/components/authorize/tests/authorize-controller.test.ts index da44351d1..ff279c30d 100644 --- a/src/components/authorize/tests/authorize-controller.test.ts +++ b/src/components/authorize/tests/authorize-controller.test.ts @@ -8,6 +8,7 @@ import { authorizeGet } from "../authorize-controller"; import { CookieConsentServiceInterface } from "../../common/cookie-consent/types"; import { COOKIE_CONSENT, + COOKIES_PREFERENCES_SET, OIDC_PROMPT, PATH_NAMES, } from "../../../app.constants"; @@ -22,6 +23,7 @@ import { createmockclaims } from "./test-data"; import { Claims } from "../claims-config"; import { getOrchToAuthExpectedClientId } from "../../../config"; import { createMockRequest } from "../../../../test/helpers/mock-request-helper"; +import { createMockCookieConsentService } from "../../../../test/helpers/mock-cookie-consent-service-helper"; describe("authorize controller", () => { let req: RequestOutput; @@ -55,11 +57,6 @@ describe("authorize controller", () => { success: true, }); - fakeCookieConsentService = { - getCookieConsent: sinon.fake(), - createConsentCookieValue: sinon.fake(), - }; - fakeKmsDecryptionService = { decrypt: sinon.fake.returns(Promise.resolve("jwt")), }; @@ -88,18 +85,24 @@ describe("authorize controller", () => { }); it("should redirect to /sign-in-or-create page with cookie preferences set", async () => { + req.body.cookie_preferences = "true"; + authServiceResponseData.data.user = { cookieConsent: COOKIE_CONSENT.ACCEPT, }; + fakeAuthorizeService = mockAuthService(authServiceResponseData); - const fakeCookieConsentService: CookieConsentServiceInterface = { - getCookieConsent: sinon.fake(), - createConsentCookieValue: sinon.fake.returns({ - value: JSON.stringify("cookieValue"), - expiry: "cookieExpires", - }), - } as unknown as CookieConsentServiceInterface; + const fakeCookieConsentService = createMockCookieConsentService( + req.body.cookie_preferences + ); + + const consentCookieValue = + fakeCookieConsentService.createConsentCookieValue( + req.body.cookie_preferences === "true" + ? COOKIE_CONSENT.ACCEPT + : COOKIE_CONSENT.REJECT + ); await authorizeGet( fakeAuthorizeService, @@ -108,7 +111,20 @@ describe("authorize controller", () => { fakeJwtService )(req as Request, res as Response); - expect(res.cookie).to.have.been.called; + expect(res.cookie).to.have.been.calledWith( + COOKIES_PREFERENCES_SET, + consentCookieValue.value, + sinon.match({ + expires: sinon.match((date: Date) => { + const expectedExpires = new Date(Date.now()); + expectedExpires.setFullYear(expectedExpires.getFullYear() + 1); + return Math.abs(date.getTime() - expectedExpires.getTime()) < 1000; + }), + secure: true, + httpOnly: false, + encode: String, + }) + ); expect(res.redirect).to.have.calledWith(PATH_NAMES.SIGN_IN_OR_CREATE); }); @@ -270,13 +286,16 @@ describe("authorize controller", () => { }; fakeAuthorizeService = mockAuthService(authServiceResponseData); - const fakeCookieConsentService: CookieConsentServiceInterface = { - getCookieConsent: sinon.fake(), - createConsentCookieValue: sinon.fake.returns({ - value: JSON.stringify("cookieValue"), - expiry: "cookieExpires", - }), - } as unknown as CookieConsentServiceInterface; + const fakeCookieConsentService = createMockCookieConsentService( + req.body.cookie_preferences + ); + + const consentCookieValue = + fakeCookieConsentService.createConsentCookieValue( + req.body.cookie_preferences === "true" + ? COOKIE_CONSENT.ACCEPT + : COOKIE_CONSENT.REJECT + ); await authorizeGet( fakeAuthorizeService, @@ -285,7 +304,15 @@ describe("authorize controller", () => { fakeJwtService )(req as Request, res as Response); - expect(res.cookie).to.have.been.called; + expect(res.cookie).to.have.been.calledWith( + COOKIES_PREFERENCES_SET, + consentCookieValue.value, + sinon.match({ + secure: true, + httpOnly: false, + encode: String, + }) + ); expect(res.redirect).to.have.calledWith( `${PATH_NAMES.SIGN_IN_OR_CREATE}?_ga=${gaTrackingId}` ); diff --git a/src/components/common/cookie-consent/cookie-consent-service.ts b/src/components/common/cookie-consent/cookie-consent-service.ts index 222bec6b0..7801a074f 100644 --- a/src/components/common/cookie-consent/cookie-consent-service.ts +++ b/src/components/common/cookie-consent/cookie-consent-service.ts @@ -39,7 +39,7 @@ export function cookieConsentService(): CookieConsentServiceInterface { cookieExpires.setFullYear(cookieExpires.getFullYear() - 1); } - return { value: JSON.stringify(cookieValue), expiry: cookieExpires }; + return { value: JSON.stringify(cookieValue), expires: cookieExpires }; }; return { diff --git a/src/components/common/cookie-consent/tests/cookie-consent-service.test.ts b/src/components/common/cookie-consent/tests/cookie-consent-service.test.ts index 93130c7f4..cb516459f 100644 --- a/src/components/common/cookie-consent/tests/cookie-consent-service.test.ts +++ b/src/components/common/cookie-consent/tests/cookie-consent-service.test.ts @@ -36,7 +36,7 @@ describe("cookie consent service", () => { COOKIE_CONSENT.NOT_ENGAGED ); expect(result.value).to.have.be.equal("{}"); - expect(result.expiry.getFullYear()).to.have.be.equal( + expect(result.expires.getFullYear()).to.have.be.equal( new Date().getFullYear() - 1 ); }); @@ -46,7 +46,7 @@ describe("cookie consent service", () => { COOKIE_CONSENT.ACCEPT ); expect(result.value).to.have.be.equal('{"analytics":true}'); - expect(result.expiry.getFullYear()).to.have.be.equal( + expect(result.expires.getFullYear()).to.have.be.equal( new Date().getFullYear() + 1 ); }); @@ -56,7 +56,7 @@ describe("cookie consent service", () => { COOKIE_CONSENT.REJECT ); expect(result.value).to.have.be.equal('{"analytics":false}'); - expect(result.expiry.getFullYear()).to.have.be.equal( + expect(result.expires.getFullYear()).to.have.be.equal( new Date().getFullYear() + 1 ); }); diff --git a/src/components/common/cookie-consent/types.ts b/src/components/common/cookie-consent/types.ts index a9460cbc1..763b99c3f 100644 --- a/src/components/common/cookie-consent/types.ts +++ b/src/components/common/cookie-consent/types.ts @@ -5,5 +5,5 @@ export interface CookieConsentServiceInterface { export interface CookieConsentModel { value: string; - expiry: Date; + expires: Date; } diff --git a/src/components/common/cookies/cookies-controller.ts b/src/components/common/cookies/cookies-controller.ts index 947e5f15c..c3ffd0054 100644 --- a/src/components/common/cookies/cookies-controller.ts +++ b/src/components/common/cookies/cookies-controller.ts @@ -5,7 +5,6 @@ import { COOKIES_PREFERENCES_SET, COOKIE_CONSENT, } from "../../../app.constants"; -import { CookieConsentModel } from "../cookie-consent/types"; const cookieService = cookieConsentService(); @@ -27,22 +26,16 @@ export function cookiesPost(req: Request, res: Response): void { consentValue === "true" ? COOKIE_CONSENT.ACCEPT : COOKIE_CONSENT.REJECT ); - createConsentCookie(res, consentCookieValue); + res.cookie(COOKIES_PREFERENCES_SET, consentCookieValue.value, { + expires: consentCookieValue.expires, + secure: true, + httpOnly: false, + domain: res.locals.analyticsCookieDomain, + encode: String, + }); res.locals.backUrl = req.body.originalReferer; res.locals.analyticsConsent = consentValue === "true"; res.locals.updated = true; res.render("common/cookies/index.njk"); } - -function createConsentCookie( - res: Response, - consentCookieValue: CookieConsentModel -) { - res.cookie(COOKIES_PREFERENCES_SET, consentCookieValue.value, { - expires: consentCookieValue.expiry, - secure: true, - httpOnly: true, - domain: res.locals.analyticsCookieDomain, - }); -} diff --git a/src/components/common/cookies/tests/cookies-controller.test.ts b/src/components/common/cookies/tests/cookies-controller.test.ts index 9e8c08ade..19ef0c56a 100644 --- a/src/components/common/cookies/tests/cookies-controller.test.ts +++ b/src/components/common/cookies/tests/cookies-controller.test.ts @@ -5,8 +5,13 @@ import { sinon } from "../../../../../test/utils/test-utils"; import { Request, Response } from "express"; import { cookiesGet, cookiesPost } from "../cookies-controller"; -import { COOKIES_PREFERENCES_SET, PATH_NAMES } from "../../../../app.constants"; +import { + COOKIE_CONSENT, + COOKIES_PREFERENCES_SET, + PATH_NAMES, +} from "../../../../app.constants"; import { mockResponse, RequestOutput, ResponseOutput } from "mock-req-res"; +import { createMockCookieConsentService } from "../../../../../test/helpers/mock-cookie-consent-service-helper"; import { createMockRequest } from "../../../../../test/helpers/mock-request-helper"; describe("cookies controller", () => { @@ -33,30 +38,88 @@ describe("cookies controller", () => { expect(res.locals.backUrl).to.equal("/last-page"); }); }); + describe("cookiesPost", () => { - it("should save analytics preferences as yes and render cookies page", () => { - req.body.cookie_preferences = "true"; - req.body.originalReferer = "/page-before-1"; + describe("where the user has consented to cookies", () => { + it("should call res.cookie with the expected arguments and flags", () => { + req.body.cookie_preferences = "true"; - cookiesPost(req as Request, res as Response); + const fakeCookieConsentService = createMockCookieConsentService( + req.body.cookie_preferences + ); - expect(res.render).to.have.been.calledWith("common/cookies/index.njk"); - expect(res.locals.analyticsConsent).to.equal(true); - expect(res.locals.updated).to.equal(true); - expect(res.locals.backUrl).to.equal("/page-before-1"); - expect(res.cookie).to.have.been.calledWith(COOKIES_PREFERENCES_SET); + const consentCookieValue = + fakeCookieConsentService.createConsentCookieValue( + req.body.cookie_preferences === "true" + ? COOKIE_CONSENT.ACCEPT + : COOKIE_CONSENT.REJECT + ); + + cookiesPost(req as Request, res as Response); + + expect(res.cookie).to.have.been.calledWith( + COOKIES_PREFERENCES_SET, + consentCookieValue.value, + sinon.match({ + secure: true, + httpOnly: false, + encode: String, + }) + ); + }); + + it("should render the page", () => { + req.body.cookie_preferences = "true"; + req.body.originalReferer = "/page-before-1"; + + cookiesPost(req as Request, res as Response); + + expect(res.render).to.have.been.calledWith("common/cookies/index.njk"); + expect(res.locals.analyticsConsent).to.equal(true); + expect(res.locals.updated).to.equal(true); + expect(res.locals.backUrl).to.equal("/page-before-1"); + }); }); - it("should save analytics preferences as no and render cookies page", () => { - req.body.cookie_preferences = "false"; - req.body.originalReferer = "/page-before-2"; - cookiesPost(req as Request, res as Response); + describe("where the user has not consented to cookies", () => { + it("should call res.cookie with the expected arguments and flags", () => { + req.body.cookie_preferences = "false"; - expect(res.render).to.have.been.calledWith("common/cookies/index.njk"); - expect(res.locals.analyticsConsent).to.equal(false); - expect(res.locals.updated).to.equal(true); - expect(res.locals.backUrl).to.equal("/page-before-2"); - expect(res.cookie).to.have.been.calledWith(COOKIES_PREFERENCES_SET); + const fakeCookieConsentService = createMockCookieConsentService( + req.body.cookie_preferences + ); + + const consentCookieValue = + fakeCookieConsentService.createConsentCookieValue( + req.body.cookie_preferences === "true" + ? COOKIE_CONSENT.ACCEPT + : COOKIE_CONSENT.REJECT + ); + + cookiesPost(req as Request, res as Response); + + expect(res.cookie).to.have.been.calledWith( + COOKIES_PREFERENCES_SET, + consentCookieValue.value, + sinon.match({ + secure: true, + httpOnly: false, + encode: String, + }) + ); + }); + + it("should render the page", () => { + req.body.cookie_preferences = "false"; + req.body.originalReferer = "/page-before-2"; + + cookiesPost(req as Request, res as Response); + + expect(res.render).to.have.been.calledWith("common/cookies/index.njk"); + expect(res.locals.analyticsConsent).to.equal(false); + expect(res.locals.updated).to.equal(true); + expect(res.locals.backUrl).to.equal("/page-before-2"); + }); }); }); }); diff --git a/test/helpers/mock-cookie-consent-service-helper.ts b/test/helpers/mock-cookie-consent-service-helper.ts new file mode 100644 index 000000000..ce1deb749 --- /dev/null +++ b/test/helpers/mock-cookie-consent-service-helper.ts @@ -0,0 +1,19 @@ +import { sinon } from "../utils/test-utils"; +import { CookieConsentServiceInterface } from "../../src/components/common/cookie-consent/types"; + +export function createMockCookieConsentService( + userCookieConsentPreference: string +): CookieConsentServiceInterface { + const expiryDate: Date = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + + return { + getCookieConsent: sinon.fake(), + createConsentCookieValue: sinon.fake.returns({ + value: JSON.stringify({ + analytics: userCookieConsentPreference === "true", + }), + expires: expiryDate, + }), + }; +}