diff --git a/ci/terraform/authdev1.tfvars b/ci/terraform/authdev1.tfvars index 672d0bd5a..7f3dc0c7a 100644 --- a/ci/terraform/authdev1.tfvars +++ b/ci/terraform/authdev1.tfvars @@ -22,6 +22,10 @@ frontend_auto_scaling_min_count = 1 frontend_auto_scaling_max_count = 2 ecs_desired_count = 1 +#cloudfront flag +cloudfront_auth_frontend_enabled = true +cloudfront_auth_dns_enabled = true + alb_idle_timeout = 30 url_for_support_links = "https://home.build.account.gov.uk/contact-gov-uk-one-login" diff --git a/ci/terraform/authdev2.tfvars b/ci/terraform/authdev2.tfvars index 0dd123f27..04d61b7fa 100644 --- a/ci/terraform/authdev2.tfvars +++ b/ci/terraform/authdev2.tfvars @@ -22,6 +22,10 @@ frontend_auto_scaling_min_count = 1 frontend_auto_scaling_max_count = 2 ecs_desired_count = 1 +#cloudfront flag +cloudfront_auth_frontend_enabled = true +cloudfront_auth_dns_enabled = true + alb_idle_timeout = 30 url_for_support_links = "https://home.build.account.gov.uk/contact-gov-uk-one-login" diff --git a/ci/terraform/build.tfvars b/ci/terraform/build.tfvars index 63a4724a2..c823290a3 100644 --- a/ci/terraform/build.tfvars +++ b/ci/terraform/build.tfvars @@ -5,6 +5,9 @@ frontend_auto_scaling_v2_enabled = true frontend_task_definition_cpu = 512 frontend_task_definition_memory = 1024 +frontend_auto_scaling_min_count = 4 +frontend_auto_scaling_max_count = 6 +ecs_desired_count = 4 alb_idle_timeout = 30 @@ -32,3 +35,7 @@ orch_to_auth_client_id = "orchestrationAuth" orch_to_auth_audience = "https://signin.build.account.gov.uk/" dynatrace_secret_arn = "arn:aws:secretsmanager:eu-west-2:216552277552:secret:DynatraceNonProductionVariables" + +#cloudfront enabled flag +cloudfront_auth_frontend_enabled = true +cloudfront_auth_dns_enabled = true diff --git a/ci/terraform/cloudfront.tf b/ci/terraform/cloudfront.tf index f2ee4def2..35617f2ae 100644 --- a/ci/terraform/cloudfront.tf +++ b/ci/terraform/cloudfront.tf @@ -1,32 +1,37 @@ resource "aws_cloudformation_stack" "cloudfront" { count = var.cloudfront_auth_frontend_enabled ? 1 : 0 name = "${var.environment}-auth-fe-cloudfront" - #using fixed version of template for now - template_url = "https://template-storage-templatebucket-1upzyw6v9cs42.s3.amazonaws.com/cloudfront-distribution/template.yaml?versionId=EKk9m9vMv10qF5vHzWZogFLnQQw6_Yjc" + #using fixed version of cloudfron disturbution template for now + template_url = "https://template-storage-templatebucket-1upzyw6v9cs42.s3.amazonaws.com/cloudfront-distribution/template.yaml?versionId=r_TJE_Uw3BHA0FFMX7WE84B39D9ucuG8" capabilities = ["CAPABILITY_NAMED_IAM"] parameters = { - AddWWWPrefix = var.Add_WWWPrefix - ApplyCloakingHeaderWAFToOrigin = var.Apply_CloakingHeader_WAFToOrigin - CloudFrontCertArn = aws_acm_certificate.cloudfront_frontend_certificate[0].arn - CloudfrontWafAcl = aws_wafv2_web_acl.frontend_cloudfront_waf_web_acl[0].arn - DistributionAlias = local.frontend_fqdn - FraudHeaderEnabled = var.Fraud_Header_Enabled - OriginCloakingHeader = var.auth_origin_cloakingheader - OriginResourceArn = aws_lb.frontend_alb.id - OriginWafAcl = "none" - PreviousOriginCloakingHeader = var.previous_auth_origin_cloakingheader - StandardLoggingEnabled = true + AddWWWPrefix = var.Add_WWWPrefix + CloudFrontCertArn = aws_acm_certificate.cloudfront_frontend_certificate[0].arn + CloudfrontWafAcl = aws_wafv2_web_acl.frontend_cloudfront_waf_web_acl[0].arn + DistributionAlias = local.frontend_fqdn + FraudHeaderEnabled = var.Fraud_Header_Enabled + OriginCloakingHeader = var.auth_origin_cloakingheader + PreviousOriginCloakingHeader = var.previous_auth_origin_cloakingheader + StandardLoggingEnabled = true } tags = local.default_tags + + #ignoring below parameter as these parameter are been read via secret manager and terraform continually detects changes + # Note : we need to remove the below lifecycle if the Header are changed in Secret manager to appy new cloainking header value + lifecycle { + ignore_changes = [parameters["OriginCloakingHeader"], parameters["PreviousOriginCloakingHeader"]] + } + } resource "aws_cloudformation_stack" "cloudfront-monitoring" { - count = var.cloudfront_auth_frontend_enabled ? 1 : 0 - provider = aws.cloudfront - name = "${var.environment}-auth-fe-cloudfront-monitoring" - template_url = "https://template-storage-templatebucket-1upzyw6v9cs42.s3.amazonaws.com/cloudfront-monitoring-alarm/template.yaml?z=${timestamp()}" + count = var.cloudfront_auth_frontend_enabled ? 1 : 0 + provider = aws.cloudfront + name = "${var.environment}-auth-fe-cloudfront-monitoring" + #using fixed version of cloudfront monitoring disturbution template for now + template_url = "https://template-storage-templatebucket-1upzyw6v9cs42.s3.amazonaws.com/cloudfront-monitoring-alarm/template.yaml?versionId=td2KHIlG7KGXl0mkMrRDkgBWxdXPEMZ." capabilities = ["CAPABILITY_NAMED_IAM"] diff --git a/ci/terraform/integration.tfvars b/ci/terraform/integration.tfvars index 88c0fea8e..3b9cf4ee6 100644 --- a/ci/terraform/integration.tfvars +++ b/ci/terraform/integration.tfvars @@ -5,10 +5,17 @@ frontend_auto_scaling_v2_enabled = true frontend_task_definition_cpu = 512 frontend_task_definition_memory = 1024 -support_account_recovery = "1" -support_account_interventions = "1" -support_authorize_controller = "1" -support_2fa_b4_password_reset = "1" +support_account_recovery = "1" +support_account_interventions = "1" +support_authorize_controller = "1" +support_2fa_b4_password_reset = "1" +support_2hr_lockout = "1" +code_request_blocked_minutes = "120" +account_recovery_code_entered_wrong_blocked_minutes = "120" +code_entered_wrong_blocked_minutes = "120" +email_entered_wrong_blocked_minutes = "120" +password_reset_code_entered_wrong_blocked_minutes = "120" +reduced_code_block_duration_minutes = "15" url_for_support_links = "https://home.integration.account.gov.uk/contact-gov-uk-one-login" diff --git a/ci/terraform/production.tfvars b/ci/terraform/production.tfvars index f555017a9..758f2c4ed 100644 --- a/ci/terraform/production.tfvars +++ b/ci/terraform/production.tfvars @@ -2,16 +2,23 @@ environment = "production" common_state_bucket = "digital-identity-prod-tfstate" redis_node_size = "cache.m4.xlarge" -frontend_auto_scaling_v2_enabled = true -frontend_task_definition_cpu = 512 -frontend_task_definition_memory = 1024 -frontend_auto_scaling_min_count = 4 -frontend_auto_scaling_max_count = 240 -ecs_desired_count = 4 -support_account_recovery = "1" -support_account_interventions = "1" -support_authorize_controller = "1" -support_2fa_b4_password_reset = "1" +frontend_auto_scaling_v2_enabled = true +frontend_task_definition_cpu = 512 +frontend_task_definition_memory = 1024 +frontend_auto_scaling_min_count = 4 +frontend_auto_scaling_max_count = 240 +ecs_desired_count = 4 +support_account_recovery = "1" +support_account_interventions = "1" +support_authorize_controller = "1" +support_2fa_b4_password_reset = "1" +support_2hr_lockout = "1" +code_request_blocked_minutes = "120" +account_recovery_code_entered_wrong_blocked_minutes = "120" +code_entered_wrong_blocked_minutes = "120" +email_entered_wrong_blocked_minutes = "120" +password_reset_code_entered_wrong_blocked_minutes = "120" +reduced_code_block_duration_minutes = "15" url_for_support_links = "https://home.account.gov.uk/contact-gov-uk-one-login" diff --git a/ci/terraform/route53.tf b/ci/terraform/route53.tf index c10494486..05becbcb2 100644 --- a/ci/terraform/route53.tf +++ b/ci/terraform/route53.tf @@ -13,8 +13,8 @@ resource "aws_route53_record" "frontend" { alias { evaluate_target_health = false - name = aws_lb.frontend_alb.dns_name - zone_id = aws_lb.frontend_alb.zone_id + name = var.cloudfront_auth_dns_enabled ? aws_cloudformation_stack.cloudfront[0].outputs["DistributionDomain"] : aws_lb.frontend_alb.dns_name + zone_id = var.cloudfront_auth_dns_enabled ? var.cloudfront_zoneid : aws_lb.frontend_alb.zone_id } } @@ -25,8 +25,8 @@ resource "aws_route53_record" "frontend_record" { alias { evaluate_target_health = false - name = aws_lb.frontend_alb.dns_name - zone_id = aws_lb.frontend_alb.zone_id + name = var.cloudfront_auth_dns_enabled ? aws_cloudformation_stack.cloudfront[0].outputs["DistributionDomain"] : aws_lb.frontend_alb.dns_name + zone_id = var.cloudfront_auth_dns_enabled ? var.cloudfront_zoneid : aws_lb.frontend_alb.zone_id } } @@ -102,7 +102,7 @@ resource "aws_route53_record" "Cloudfront_frontend_record" { resource "aws_acm_certificate" "cloudfront_frontend_certificate" { provider = aws.cloudfront count = var.cloudfront_auth_frontend_enabled ? 1 : 0 - domain_name = aws_route53_record.frontend.name + domain_name = local.frontend_fqdn validation_method = "DNS" tags = local.default_tags diff --git a/ci/terraform/sandpit.tfvars b/ci/terraform/sandpit.tfvars index 0184b049e..bb59585ba 100644 --- a/ci/terraform/sandpit.tfvars +++ b/ci/terraform/sandpit.tfvars @@ -27,6 +27,7 @@ ecs_desired_count = 1 #cloudfront enabled flag cloudfront_auth_frontend_enabled = true +cloudfront_auth_dns_enabled = true alb_idle_timeout = 30 diff --git a/ci/terraform/staging.tfvars b/ci/terraform/staging.tfvars index f1cfca056..bd0855c4c 100644 --- a/ci/terraform/staging.tfvars +++ b/ci/terraform/staging.tfvars @@ -2,8 +2,9 @@ environment = "staging" common_state_bucket = "di-auth-staging-tfstate" redis_node_size = "cache.m4.xlarge" - +#cloudfront enabled flag cloudfront_auth_frontend_enabled = true +cloudfront_auth_dns_enabled = true frontend_auto_scaling_v2_enabled = true frontend_task_definition_cpu = 1024 diff --git a/ci/terraform/variables.tf b/ci/terraform/variables.tf index 4ddd3031c..c3442fc23 100644 --- a/ci/terraform/variables.tf +++ b/ci/terraform/variables.tf @@ -286,6 +286,18 @@ variable "cloudfront_auth_frontend_enabled" { description = "Feature flag to control the creation cloudfront DNS record origin & Cloudfront Certificate" } +variable "cloudfront_auth_dns_enabled" { + type = bool + default = false + description = "Feature flag to control the switch of DNS record to cloudfront" +} + +variable "cloudfront_zoneid" { + type = string + default = "Z2FDTNDATAQYW2" + description = "This global zone id of CloudFront distribution " +} + variable "auth_origin_cloakingheader" { type = string sensitive = true @@ -304,12 +316,6 @@ variable "Add_WWWPrefix" { description = "flag to to add subdomain (www) to the frontend url eg www.signin.sandpit.account.gov.uk" } -variable "Apply_CloakingHeader_WAFToOrigin" { - type = bool - default = false - description = "flag to add a cloacking header WAf to ALB so only requiest comming from cloudfront are allowed " -} - variable "Fraud_Header_Enabled" { type = bool default = false diff --git a/ci/terraform/waf-cf.tf b/ci/terraform/waf-cf.tf index c8ac498ff..4f5c826a9 100644 --- a/ci/terraform/waf-cf.tf +++ b/ci/terraform/waf-cf.tf @@ -477,7 +477,7 @@ resource "aws_wafv2_web_acl_logging_configuration" "frontend_cloudfront_waf_logg resource_arn = aws_wafv2_web_acl.frontend_cloudfront_waf_web_acl[0].arn logging_filter { - default_behavior = "DROP" + default_behavior = "KEEP" filter { behavior = "KEEP" @@ -497,7 +497,7 @@ resource "aws_wafv2_web_acl_logging_configuration" "frontend_cloudfront_waf_logg resource "aws_cloudwatch_log_subscription_filter" "frontend_cloudfront_waf_subscription" { provider = aws.cloudfront - count = var.cloudfront_auth_frontend_enabled ? 1 : 0 + count = var.cloudfront_auth_frontend_enabled && var.environment == "production" || var.environment == "staging" ? 1 : 0 name = "${aws_cloudwatch_log_group.frontend_cloudfront_waf_log_group[0].name}-splunk-subscription-${count.index}" log_group_name = aws_cloudwatch_log_group.frontend_cloudfront_waf_log_group[0].name filter_pattern = "" diff --git a/ci/terraform/waf.tf b/ci/terraform/waf.tf index f86ab279d..b170dbcdc 100644 --- a/ci/terraform/waf.tf +++ b/ci/terraform/waf.tf @@ -34,6 +34,10 @@ resource "aws_wafv2_ip_set" "gds_ip_set" { tags = local.default_tags } +locals { + cloudfront_origin_cloaking_header_name = "origin-cloaking-secret" +} + resource "aws_wafv2_web_acl" "frontend_alb_waf_regional_web_acl" { name = "${var.environment}-frontend-alb-waf-web-acl" scope = "REGIONAL" @@ -82,6 +86,49 @@ resource "aws_wafv2_web_acl" "frontend_alb_waf_regional_web_acl" { rate_based_statement { limit = var.environment == "staging" ? 20000000 : 25000 aggregate_key_type = "IP" + scope_down_statement { + and_statement { + statement { + not_statement { + statement { + byte_match_statement { + field_to_match { + single_header { + name = local.cloudfront_origin_cloaking_header_name + } + } + positional_constraint = "EXACTLY" + search_string = var.auth_origin_cloakingheader + text_transformation { + priority = 0 + type = "NONE" + } + } + } + } + } + + statement { + not_statement { + statement { + byte_match_statement { + field_to_match { + single_header { + name = local.cloudfront_origin_cloaking_header_name + } + } + positional_constraint = "EXACTLY" + search_string = var.previous_auth_origin_cloakingheader + text_transformation { + priority = 0 + type = "NONE" + } + } + } + } + } + } + } } } visibility_config { @@ -404,7 +451,7 @@ resource "aws_wafv2_web_acl_logging_configuration" "frontend_alb_waf_logging_con resource_arn = aws_wafv2_web_acl.frontend_alb_waf_regional_web_acl.arn logging_filter { - default_behavior = "DROP" + default_behavior = "KEEP" filter { behavior = "KEEP" diff --git a/docker-compose.frontend.yml b/docker-compose.frontend.yml index 134673220..d2c0b5c76 100644 --- a/docker-compose.frontend.yml +++ b/docker-compose.frontend.yml @@ -9,24 +9,18 @@ services: - "${DOCKER_FRONTEND_NODEMON_PORT:-9230}:${DOCKER_FRONTEND_NODEMON_PORT:-9230}" volumes: - ./:/app + env_file: + - .env environment: - ENVIRONMENT: ${ENVIRONMENT:?this should be set in your .env file.} - - SESSION_EXPIRY: ${SESSION_EXPIRY:?this should be set in your .env file.} - SESSION_SECRET: ${SESSION_SECRET:?this should be set in your .env file.} - - API_BASE_URL: ${API_BASE_URL:?this should be set in your .env file.} - API_KEY: ${API_KEY:?this should be set in your .env file.} - FRONTEND_API_BASE_URL: ${FRONTEND_API_BASE_URL:?this should be set in your .env file.} - ANALYTICS_COOKIE_DOMAIN: localhost - SUPPORT_ACCOUNT_RECOVERY: ${SUPPORT_ACCOUNT_RECOVERY:?this should be set in your .env file.} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required. This should be set by `startup.sh` so there may be an issue with the startup script.} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required. This should be set by `startup.sh` so there may be an issue with the startup script.} - REDIS_PORT: ${DOCKER_REDIS_PORT:-6379} + # We ignore `.env` values here, as we're using the docker network for redis. + REDIS_PORT: 6379 # This is the default port for Redis + REDIS_HOST: redis # This is the name of the service in `docker-compose.yml` + PORT: ${DOCKER_FRONTEND_PORT:-3000} restart: on-failure networks: diff --git a/docker-compose.yml b/docker-compose.yml index 5a7e464bd..ee3d612a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: redis: image: redis:6.0.5-alpine ports: - - "${DOCKER_REDIS_PORT:-6379}:6379" + - "${REDIS_PORT:-6379}:6379" networks: - di-net diff --git a/local.Dockerfile b/local.Dockerfile index be16c1859..3d1448596 100644 --- a/local.Dockerfile +++ b/local.Dockerfile @@ -8,4 +8,4 @@ WORKDIR /app EXPOSE $PORT -CMD yarn install && yarn copy-assets && yarn dev +CMD yarn install && yarn test:dev-evironment-variables && yarn copy-assets && yarn dev diff --git a/scripts/_create_env_file.py b/scripts/_create_env_file.py index 1abdf5dca..ed774acc6 100644 --- a/scripts/_create_env_file.py +++ b/scripts/_create_env_file.py @@ -13,6 +13,7 @@ import boto3 from botocore.exceptions import BotoCoreError +from botocore.exceptions import SSOTokenLoadError as BotoSSOTokenLoadError from botocore.exceptions import TokenRetrievalError as BotoTokenRetrievalError from dotenv import dotenv_values @@ -176,7 +177,7 @@ def __init__(self, deployment_name: str, state_bucket: str, aws_profile_name: st self._validate_aws_credentials() self.s3_client = self.boto_client.client("s3") self.dynamodb_client = self.boto_client.client("dynamodb") - except BotoTokenRetrievalError: + except (BotoTokenRetrievalError, BotoSSOTokenLoadError): logger.fatal( "AWS auth error: Your SSO session has expired. Please run `aws sso " "login --profile di-auth-development-admin` to refresh your session." @@ -186,6 +187,22 @@ def __init__(self, deployment_name: str, state_bucket: str, aws_profile_name: st logger.fatal("AWS error: %s. Are you connected to the VPN?", e) sys.exit(1) + if not self._check_environment_exists(): + logger.fatal( + "Environment %s does not exist. Please check you have the correct name", + deployment_name, + ) + sys.exit(1) + + def _check_environment_exists(self): + try: + self._api_remote_state + except boto3.exceptions.botocore.exceptions.ClientError as e: + if e.response["Error"]["Code"] == "404": + return False + raise LookupError("Error checking if environment exists") from e + return True + def _validate_aws_credentials(self): self.boto_client.client("sts").get_caller_identity() diff --git a/scripts/set-password-by-email.sh b/scripts/set-password-by-email.sh new file mode 100755 index 000000000..814a2966f --- /dev/null +++ b/scripts/set-password-by-email.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +#Set the AWS_PROFILE for the environment in which you want to set the password +export AWS_PROFILE="" +#Set the credential table name for the environment (ex: authdev1-user-credentials) +table_name="" +#Set the account email and desired password for the account +email="" +password="" + +# shellcheck source=./scripts/export_aws_creds.sh +source "${DIR}/export_aws_creds.sh" +hashed_pwd=$(echo -n "$password" | argon2 "$(openssl rand -hex 32)" -e -id -v 13 -k 15360 -t 2 -p 1 | cat -u) + +export AWS_PAGER="" +echo "Trying to update the AWS dynamodb record:" +aws dynamodb update-item \ + --table-name "$table_name" \ + --key "{\"Email\":{\"S\":\"$email\"}}" \ + --update-expression "SET Password = :pw" \ + --expression-attribute-values "{\":pw\":{\"S\":\"$hashed_pwd\"}}" \ + --region "eu-west-2" \ + --return-values ALL_NEW 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/check-reauth-users/check-reauth-users-service.ts b/src/components/check-reauth-users/check-reauth-users-service.ts index 4bbd84b18..7a97a90df 100644 --- a/src/components/check-reauth-users/check-reauth-users-service.ts +++ b/src/components/check-reauth-users/check-reauth-users-service.ts @@ -23,7 +23,7 @@ export function checkReauthUsersService( const config = getRequestConfig({ sessionId, sourceIp, - validationStatues: [ + validationStatuses: [ HTTP_STATUS_CODES.OK, HTTP_STATUS_CODES.BAD_REQUEST, HTTP_STATUS_CODES.NOT_FOUND, 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/src/components/common/send-notification/send-notification-service.ts b/src/components/common/send-notification/send-notification-service.ts index 9d9a4b17c..735914186 100644 --- a/src/components/common/send-notification/send-notification-service.ts +++ b/src/components/common/send-notification/send-notification-service.ts @@ -48,7 +48,7 @@ export function sendNotificationService( clientSessionId: clientSessionId, sourceIp: sourceIp, persistentSessionId: persistentSessionId, - validationStatues: [ + validationStatuses: [ HTTP_STATUS_CODES.NO_CONTENT, HTTP_STATUS_CODES.BAD_REQUEST, ], diff --git a/src/components/enter-password/enter-password-service.ts b/src/components/enter-password/enter-password-service.ts index 768d0a245..2b169942a 100644 --- a/src/components/enter-password/enter-password-service.ts +++ b/src/components/enter-password/enter-password-service.ts @@ -43,7 +43,7 @@ export function enterPasswordService( getRequestConfig({ sessionId: sessionId, clientSessionId: clientSessionId, - validationStatues: [ + validationStatuses: [ HTTP_STATUS_CODES.OK, HTTP_STATUS_CODES.UNAUTHORIZED, HTTP_STATUS_CODES.BAD_REQUEST, diff --git a/src/components/landing/landing-service.ts b/src/components/landing/landing-service.ts deleted file mode 100644 index 12b825edf..000000000 --- a/src/components/landing/landing-service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LandingServiceInterface, StartAuthResponse } from "./types"; -import { ApiResponseResult } from "../../types"; -import { API_ENDPOINTS } from "../../app.constants"; -import { - createApiResponse, - getRequestConfig, - http, - Http, -} from "../../utils/http"; - -export function landingService(axios: Http = http): LandingServiceInterface { - const start = async function ( - sessionId: string, - clientSessionId: string, - sourceIp: string, - persistentSessionId: string - ): Promise> { - const response = await axios.client.get( - API_ENDPOINTS.START, - getRequestConfig({ - sessionId: sessionId, - clientSessionId: clientSessionId, - sourceIp: sourceIp, - persistentSessionId: persistentSessionId, - }) - ); - - return createApiResponse(response); - }; - - return { - start, - }; -} diff --git a/src/components/landing/tests/landing-integration.test.ts b/src/components/landing/tests/landing-integration.test.ts index 178a166d0..b6453f594 100644 --- a/src/components/landing/tests/landing-integration.test.ts +++ b/src/components/landing/tests/landing-integration.test.ts @@ -3,19 +3,14 @@ import { describe } from "mocha"; import { sinon } from "../../../../test/utils/test-utils"; import nock = require("nock"); import decache from "decache"; -import { HTTP_STATUS_CODES, PATH_NAMES } from "../../../app.constants"; -import { LandingServiceInterface, StartAuthResponse } from "../types"; -import { createApiResponse } from "../../../utils/http"; -import { AxiosResponse } from "axios"; +import { PATH_NAMES } from "../../../app.constants"; describe("Integration:: landing", () => { let app: any; before(async () => { decache("../../../app"); - decache("../landing-service"); decache("../../../middleware/session-middleware"); - const landingService = require("../landing-service"); const sessionMiddleware = require("../../../middleware/session-middleware"); sinon .stub(sessionMiddleware, "validateSessionMiddleware") @@ -30,37 +25,6 @@ describe("Integration:: landing", () => { next(); }); - sinon - .stub(landingService, "landingService") - .callsFake((): LandingServiceInterface => { - async function start() { - const fakeAxiosResponse: AxiosResponse = { - data: { - client: { - serviceType: "MANDATORY", - clientName: "test-client", - scopes: ["openid"], - cookieConsentEnabled: true, - consentEnabled: true, - redirectUri: "http://test-redirect.gov.uk/callback", - state: "jasldasl12312", - isOneLoginService: false, - }, - user: { - consentRequired: true, - upliftRequired: false, - identityRequired: false, - authenticated: false, - }, - }, - status: HTTP_STATUS_CODES.OK, - } as AxiosResponse; - - return createApiResponse(fakeAxiosResponse); - } - - return { start }; - }); app = await require("../../../app").createApp(); }); diff --git a/src/components/landing/types.ts b/src/components/landing/types.ts index 76cda9b0f..42002be96 100644 --- a/src/components/landing/types.ts +++ b/src/components/landing/types.ts @@ -1,4 +1,4 @@ -import { ApiResponseResult, DefaultApiResponse } from "../../types"; +import { DefaultApiResponse } from "../../types"; export interface StartAuthResponse extends DefaultApiResponse { client: ClientInfo; @@ -26,12 +26,3 @@ export interface UserSessionInfo { docCheckingAppUser: boolean; mfaMethodType?: string; } - -export interface LandingServiceInterface { - start: ( - sessionId: string, - clientSessionId: string, - sourceIp: string, - persistentSessionId: string - ) => Promise>; -} diff --git a/src/components/reset-password-check-email/reset-password-check-email-controller.ts b/src/components/reset-password-check-email/reset-password-check-email-controller.ts index cad97c81b..822e34d31 100644 --- a/src/components/reset-password-check-email/reset-password-check-email-controller.ts +++ b/src/components/reset-password-check-email/reset-password-check-email-controller.ts @@ -50,7 +50,8 @@ export function resetPasswordCheckEmailGet( sessionId, req.ip, res.locals.clientSessionId, - res.locals.persistentSessionId + res.locals.persistentSessionId, + req.session.user.withinForcedPasswordResetJourney ); } diff --git a/src/components/reset-password-check-email/reset-password-check-email-service.ts b/src/components/reset-password-check-email/reset-password-check-email-service.ts index ac2896564..f8eb7b80d 100644 --- a/src/components/reset-password-check-email/reset-password-check-email-service.ts +++ b/src/components/reset-password-check-email/reset-password-check-email-service.ts @@ -20,11 +20,13 @@ export function resetPasswordCheckEmailService( sessionId: string, sourceIp: string, clientSessionId: string, - persistentSessionId: string + persistentSessionId: string, + withinForcedPasswordResetJourney: boolean ): Promise> { const response = await axios.client.post( API_ENDPOINTS.RESET_PASSWORD_REQUEST, { + withinForcedPasswordResetJourney: withinForcedPasswordResetJourney, email: email, }, getRequestConfig({ diff --git a/src/components/reset-password-check-email/types.ts b/src/components/reset-password-check-email/types.ts index 62feb8534..55661da4e 100644 --- a/src/components/reset-password-check-email/types.ts +++ b/src/components/reset-password-check-email/types.ts @@ -6,7 +6,8 @@ export interface ResetPasswordCheckEmailServiceInterface { sessionId: string, sourceIp: string, clientSessionId: string, - persistentSessionId: string + persistentSessionId: string, + withinForcedPasswordResetJourney: boolean ) => Promise>; } diff --git a/src/utils/http.ts b/src/utils/http.ts index 55961d7ed..aaa07a12b 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -22,7 +22,7 @@ const headers: CustomAxiosRequestHeaders = { export interface ConfigOptions { sessionId?: string; clientSessionId?: string; - validationStatues?: number[]; + validationStatuses?: number[]; sourceIp?: string; persistentSessionId?: string; baseURL?: string; @@ -60,9 +60,9 @@ export function getRequestConfig(options: ConfigOptions): AxiosRequestConfig { config.headers["X-Forwarded-For"] = options.sourceIp; } - if (options.validationStatues) { + if (options.validationStatuses) { config.validateStatus = function (status: number) { - return options.validationStatues.includes(status); + return options.validationStatuses.includes(status); }; } diff --git a/startup.sh b/startup.sh index 2a288924a..ea105a9ea 100755 --- a/startup.sh +++ b/startup.sh @@ -33,6 +33,19 @@ while getopts ":clx" opt; do esac done +if [ "${ACTION_LOCAL:-0}" == "1" ] && [ "${ACTION_DEPS_ONLY:-0}" == "1" ]; then + usage "Cannot use -l and -x together" +fi + +if [ "${ACTION_LOCAL:-0}" == "0" ]; then + echo "WARNING: Running frontend in docker. This CAN be a bit flaky. If you encounter issues, try running natively with \`startup.sh -l\`" + for ((n = 0; n < 3; n++)); do + echo -n "." + sleep 1 + done + echo +fi + if [ "${ACTION_CLEAN:-0}" == "1" ]; then echo "Cleaning dist and node_modules..." rm -rf dist @@ -55,9 +68,9 @@ if [ "${ACTION_LOCAL:-0}" == "1" ]; then docker compose -f docker-compose.yml up --build -d --wait echo "No-MFA stub listening on http://localhost:${DOCKER_STUB_NO_MFA_PORT:-5000}" echo "Default stub listening on http://localhost:${DOCKER_STUB_DEFAULT_PORT:-2000}" - echo "Redis listening on redis://localhost:${DOCKER_REDIS_PORT:-6379}" - export REDIS_PORT=${DOCKER_REDIS_PORT:-6379} + export REDIS_PORT=${REDIS_PORT:-6379} export REDIS_HOST=localhost + echo "Redis listening on redis://localhost:${REDIS_PORT:-6379}" if [ "${ACTION_DEPS_ONLY:-0}" == "0" ]; then export PORT="${DOCKER_FRONTEND_PORT:-3000}" yarn install && yarn test:dev-evironment-variables && yarn copy-assets && yarn dev @@ -69,7 +82,9 @@ else docker compose -f docker-compose.yml -f docker-compose.frontend.yml up -d --wait --build echo "No-MFA stub listening on http://localhost:${DOCKER_STUB_NO_MFA_PORT:-5000}" echo "Default stub listening on http://localhost:${DOCKER_STUB_DEFAULT_PORT:-2000}" - echo "Redis listening on redis://localhost:${DOCKER_REDIS_PORT:-6379}" + echo "Redis listening on redis://localhost:${REDIS_PORT:-6379}" echo "Frontend listening on http://localhost:${DOCKER_FRONTEND_PORT:-3000}" echo "Frontend nodemon listening on localhost:${DOCKER_FRONTEND_NODEMON_PORT:-9230}" + + docker compose -f docker-compose.yml -f docker-compose.frontend.yml logs -f fi 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, + }), + }; +} diff --git a/test/unit/utils/http.test.ts b/test/unit/utils/http.test.ts new file mode 100644 index 000000000..82bed9e57 --- /dev/null +++ b/test/unit/utils/http.test.ts @@ -0,0 +1,68 @@ +import { expect } from "chai"; +import { describe } from "mocha"; +import { getRequestConfig } from "../../../src/utils/http"; +import { HTTP_STATUS_CODES } from "../../../src/app.constants"; + +describe("getRequestConfig", () => { + const apiKey = "123"; + + beforeEach(() => { + process.env.API_KEY = apiKey; + }); + + afterEach(() => { + delete process.env.API_KEY; + }); + + it("should set the relevant headers on a request", () => { + const sessionId = "someSessionId"; + const sourceIp = "123.123.123.123"; + const clientSessionId = "someClientSessionId"; + const persistentSessionId = "somePersistentSessionId"; + const reauthenticate = true; + const userLanguage = "cy"; + const actualConfig = getRequestConfig({ + sessionId: sessionId, + sourceIp: sourceIp, + clientSessionId: clientSessionId, + persistentSessionId: persistentSessionId, + reauthenticate: reauthenticate, + userLanguage: userLanguage, + }); + + const expectedHeaders = { + "X-API-Key": apiKey, + "Session-Id": sessionId, + "Client-Session-Id": clientSessionId, + "X-Forwarded-For": sourceIp, + "di-persistent-session-id": persistentSessionId, + Reauthenticate: reauthenticate, + "User-Language": userLanguage, + }; + + expect(actualConfig.headers).to.deep.eq(expectedHeaders); + }); + + it("should set the correct baseURL", () => { + const baseURL = "https://www.example.com"; + + const actualConfig = getRequestConfig({ + baseURL: baseURL, + }); + + expect(actualConfig.baseURL).to.eq(baseURL); + }); + + it("should set the relevant validation status function", () => { + const validStatus = HTTP_STATUS_CODES.OK; + + const actualConfig = getRequestConfig({ + validationStatuses: [validStatus], + }); + + expect(actualConfig.validateStatus(validStatus)).to.eq(true); + expect(actualConfig.validateStatus(HTTP_STATUS_CODES.BAD_REQUEST)).to.eq( + false + ); + }); +}); diff --git a/test/unit/utils/mfa.ts b/test/unit/utils/mfa.test.ts similarity index 63% rename from test/unit/utils/mfa.ts rename to test/unit/utils/mfa.test.ts index 928a5a109..a9d66eba5 100644 --- a/test/unit/utils/mfa.ts +++ b/test/unit/utils/mfa.test.ts @@ -12,11 +12,15 @@ describe("mfa", () => { describe("generateQRCodeValue", () => { it("should generate QR code value with correct inputs", () => { - const expected = - "otpauth://totp/GOV.UK%20SignIn:test%40test.com?secret=testsecret&period=30&digits=6&algorithm=SHA1&issuer=GOV.UK%20SignIn"; - expect(generateQRCodeValue("testsecret", "test@test.com")).to.equal( - expected + const actual = generateQRCodeValue( + "testsecret", + "test@test.com", + "GOV.UK SignIn" ); + + const expected = + "otpauth://totp/GOV.UK%20SignIn%20-%20local:test%40test.com?secret=testsecret&period=30&digits=6&algorithm=SHA1&issuer=GOV.UK%20SignIn%20-%20local"; + expect(actual).to.equal(expected); }); }); });