diff --git a/.circleci/config.yml b/.circleci/config.yml index 7aafa23..0675605 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,13 +1,14 @@ version: 2.1 orbs: - hmpps: ministryofjustice/hmpps@6.2 - slack: circleci/slack@4.8.3 + hmpps: ministryofjustice/hmpps@7 + slack: circleci/slack@4.12.1 parameters: alerts-slack-channel: type: string default: hmpps_tech_alerts_security + releases-slack-channel: type: string default: dps-releases @@ -55,28 +56,6 @@ jobs: - dist - .cache/Cypress - check_outdated: - executor: - name: hmpps/node - tag: << pipeline.parameters.node-version >> - steps: - - checkout - - restore_cache: - key: dependency-cache-{{ checksum "package-lock.json" }} - - run: - name: install-npm - command: 'npm ci --no-audit' - - run: - name: Check version - command: 'npm --version' - - run: - name: Run check - command: 'npm outdated typescript govuk-frontend' - - slack/notify: - event: fail - channel: << pipeline.parameters.alerts-slack-channel >> - template: basic_fail_1 - unit_test: executor: name: hmpps/node @@ -206,7 +185,8 @@ workflows: only: - main jobs: - - check_outdated: + - hmpps/npm_outdated: + slack_channel: << pipeline.parameters.alerts-slack-channel >> context: - hmpps-common-vars - hmpps/npm_security_audit: diff --git a/.eslintrc.json b/.eslintrc.json index 6fd4914..d89cae6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -52,8 +52,7 @@ "trailingComma": "es5", "singleQuote": true, "printWidth": 120, - "semi": false, - "arrowParens": "avoid" + "semi": false } ] } @@ -84,19 +83,10 @@ "tsx": "never" } ], - "comma-dangle": [ - "error", - { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "always-multiline", - "exports": "always-multiline", - "functions": "never" - } - ], + "comma-dangle": ["error", "always-multiline"], "import/no-extraneous-dependencies": [ "error", - { "devDependencies": ["**/*.test.js", "**/*.test.ts", "cypress.config.ts"] } + { "devDependencies": ["**/*.test.js", "**/*.test.ts", "**/testutils/**", "cypress.config.ts"] } ], "prettier/prettier": [ "error", @@ -104,8 +94,7 @@ "trailingComma": "es5", "singleQuote": true, "printWidth": 120, - "semi": false, - "arrowParens": "avoid" + "semi": false } ] } diff --git a/.husky/pre-commit b/.husky/pre-commit index fb683ae..ce6b52c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -NODE_ENV=dev && node_modules/.bin/lint-staged && node_modules/.bin/tsc && npm test +NODE_ENV=dev && node_modules/.bin/lint-staged && npm run typecheck && npm test diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..3c03207 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/.prettierrc b/.prettierrc index f4c1cc2..75b7f48 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,15 @@ { - "trailingComma": "es5", + "trailingComma": "all", "singleQuote": true, "printWidth": 120, "semi": false, - "arrowParens": "avoid" + "arrowParens": "avoid", + "overrides": [ + { + "files": "*.njk", + "options": { + "parser": "jinja-template" + } + } + ] } diff --git a/assets/sass/application-ie8.sass b/assets/sass/application-ie8.sass deleted file mode 100644 index 44deeb7..0000000 --- a/assets/sass/application-ie8.sass +++ /dev/null @@ -1,7 +0,0 @@ -$govuk-global-styles: true -$path: "/assets/images/" - -@import 'govuk/all-ie8' - -@import './components/header-bar' -@import './local' diff --git a/assets/sass/application.sass b/assets/sass/application.sass deleted file mode 100755 index 6cbd229..0000000 --- a/assets/sass/application.sass +++ /dev/null @@ -1,9 +0,0 @@ -$govuk-global-styles: true -$path: "/assets/images/" - -@import 'govuk/all' -@import 'moj/all' - -@import './components/header-bar' -@import './components/card' -@import './local' diff --git a/assets/sass/components/_card.scss b/assets/sass/components/_card.scss deleted file mode 100644 index bc0fd73..0000000 --- a/assets/sass/components/_card.scss +++ /dev/null @@ -1,100 +0,0 @@ -/* ========================================================================== - COMPONENTS / #CARD - ========================================================================== */ - -$card-border-width: 1px; -$card-border-bottom-width: govuk-spacing(1); -$card-border-hover-color: $govuk-border-colour; -$card-border-color: lighten($card-border-hover-color, 15%); - -.card { - margin-bottom: govuk-spacing(7); - background: $govuk-body-background-colour; - border: $card-border-width solid $card-border-color; - position: relative; - width: 100%; - padding: govuk-spacing(5); - - &__heading { - margin-top: 0; - margin-bottom: govuk-spacing(3); - } - - &__description { - margin-bottom: 0; - } - - /* Clickable card - ========================================================================== */ - &--clickable { - border-bottom-width: $card-border-bottom-width; - - &:hover, - &:active { - cursor: pointer; - - .card__heading a, - .card__link { - color: $govuk-link-hover-colour; - text-decoration: none; - - &:focus { - @include govuk-focused-text; - } - } - } - - &:hover { - border-color: $card-border-hover-color; - } - - &:active { - border-color: $card-border-hover-color; - bottom: -$card-border-width; - } - } -} - -/* Card group - ========================================================================== */ - -/** - * Card group allows you to have a row of cards. - * - * Flexbox is used to make each card in a row the same height. - */ - -.card-group { - display: flex; - flex-wrap: wrap; - margin-bottom: govuk-spacing(3); - padding: 0; - - @include govuk-media-query($until: desktop) { - margin-bottom: govuk-spacing(6); - } - - &__item { - display: flex; - list-style-type: none; - margin-bottom: 0; - - @include govuk-media-query($until: desktop) { - flex: 0 0 100%; - } - - .card { - margin-bottom: govuk-spacing(5); - } - - @include govuk-media-query($until: desktop) { - .card { - margin-bottom: govuk-spacing(3); - } - - &:last-child .card { - margin-bottom: 0; - } - } - } -} diff --git a/assets/sass/components/_header-bar.scss b/assets/sass/components/_header-bar.scss deleted file mode 100644 index 9f9dda1..0000000 --- a/assets/sass/components/_header-bar.scss +++ /dev/null @@ -1,83 +0,0 @@ -#header-contents { - display: flex; - align-items: center; - justify-content: space-between; - - padding: 5px 0; - - @include govuk-media-query($until: tablet) { - justify-content: space-around; - flex-wrap: wrap; - } -} - -#site-header-block { - display: flex; - align-items: center; - justify-content: space-around; -} - -#logo { - img { - max-width: 100%; - max-height: 100%; - } - height: 35px; - @include govuk-media-query($until: tablet) { - height: 30px; - } -} - -#hmpps-title { - border-right: white solid 1px; - padding: 0 15px; - font-size: 26px; - @include govuk-media-query($until: desktop) { - border-right: none; - display: none; - } - @include govuk-media-query($from: desktop ) { - display: inline; - } -} - -#main-title { - text-align: left; - font-size: 26px; - flex-grow: 1; - text-decoration: none; - padding-left: 15px; - font-weight: normal; - - @include govuk-media-query($until: desktop) { - font-size: 22px; - padding-left: 15px; - } -} - -#user-block { - color: white; - float: right; - display: flex; - justify-content: flex-end; - height: 30px; - - a { - color: white; - } - - div { - height: 100%; - padding: 0 15px; - display: flex; - align-items: center; - - &:not(:last-child) { - border-right: solid white 3px; - } - - @include govuk-media-query($until: tablet) { - text-align: center; - } - } -} diff --git a/assets/sass/local.sass b/assets/sass/local.sass deleted file mode 100644 index 5a7c6f3..0000000 --- a/assets/sass/local.sass +++ /dev/null @@ -1,2 +0,0 @@ -.govuk-main-wrapper - min-height: 600px diff --git a/assets/scss/application-ie8.scss b/assets/scss/application-ie8.scss new file mode 100644 index 0000000..1faa305 --- /dev/null +++ b/assets/scss/application-ie8.scss @@ -0,0 +1,7 @@ +$govuk-global-styles: true; +$path: "/assets/images/"; + +@import 'govuk/all-ie8'; + +@import './components/header-bar'; +@import 'assets/scss/local'; diff --git a/assets/scss/application.scss b/assets/scss/application.scss new file mode 100755 index 0000000..9273494 --- /dev/null +++ b/assets/scss/application.scss @@ -0,0 +1,11 @@ +$govuk-global-styles: true; +$path: "/assets/images/"; + +$moj-page-width: 1170px; +$govuk-page-width: $moj-page-width; + +@import 'govuk/all'; +@import 'moj/all'; + +@import './components/header-bar'; +@import 'assets/scss/local'; diff --git a/assets/scss/components/_header-bar.scss b/assets/scss/components/_header-bar.scss new file mode 100644 index 0000000..d67938f --- /dev/null +++ b/assets/scss/components/_header-bar.scss @@ -0,0 +1,141 @@ +.hmpps-header { + @include govuk-responsive-padding(3, 'top'); + @include govuk-responsive-padding(3, 'bottom'); + background-color: govuk-colour('black'); + + &__container { + @include govuk-width-container; + display: flex; + justify-content: space-between; + align-items: center; + } + + &__logo { + @include govuk-responsive-margin(2, 'right'); + position: relative; + top: -2px; + fill: govuk-colour('white'); + } + + &__title { + @include govuk-responsive-padding(3, 'right'); + display: flex; + align-items: center; + + &__organisation-name { + @include govuk-responsive-margin(2, 'right'); + @include govuk-font($size: 24, $weight: 'bold'); + display: flex; + align-items: center; + } + + &__service-name { + display: none; + @include govuk-font(24); + + @include govuk-media-query($from: desktop) { + display: inline-block; + } + } + } + + &__link { + @include govuk-link-common; + @include govuk-link-style-default; + + &:link, + &:visited, + &:active { + color: govuk-colour('white'); + text-decoration: none; + } + + &:hover { + text-decoration: underline; + } + + &:focus { + color: govuk-colour('black'); + + svg { + fill: govuk-colour('black'); + } + } + + &__sub-text { + @include govuk-font(16); + display: none; + + @include govuk-media-query($from: tablet) { + display: block; + } + } + } + + &__navigation { + display: flex; + flex-direction: column; + align-items: flex-end; + list-style: none; + margin: 0; + padding: 0; + + @include govuk-media-query($from: tablet) { + flex-direction: row; + align-items: center; + } + + &__item { + @include govuk-font(19); + margin-bottom: govuk-spacing(1); + text-align: right; + + @include govuk-media-query($from: tablet) { + @include govuk-responsive-margin(4, 'right'); + @include govuk-responsive-padding(4, 'right'); + margin-bottom: 0; + border-right: 1px solid govuk-colour('white'); + } + + a { + display: inline-block; + } + + &:last-child { + margin-right: 0; + border-right: 0; + padding-right: 0; + } + } + } + + @media print { + display: none; + } +} + +.location-bar { + @include govuk-width-container; + @include govuk-responsive-margin(3, 'bottom'); + @include govuk-responsive-padding(3, 'top'); + @include govuk-responsive-padding(3, 'bottom'); + width: 100%; + display: flex; + flex-wrap: wrap; + border-bottom: 1px solid $govuk-border-colour; + + &__location { + @include govuk-font($size: 19, $weight: 'bold'); + @include govuk-responsive-margin(3, 'right'); + } + + &__link { + @include govuk-link-common; + @include govuk-link-style-default; + @include govuk-font($size: 19, $weight: 'normal'); + } + + @media print { + display: none; + } +} diff --git a/assets/scss/local.scss b/assets/scss/local.scss new file mode 100644 index 0000000..35cba2b --- /dev/null +++ b/assets/scss/local.scss @@ -0,0 +1,3 @@ +.govuk-main-wrapper { + min-height: 600px; +} diff --git a/cypress.config.ts b/cypress.config.ts index 37de93d..841733b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'cypress' -import setupNodeEvents from './integration_tests/plugins/index' +import { resetStubs } from './integration_tests/mockApis/wiremock' +import auth from './integration_tests/mockApis/auth' +import tokenVerification from './integration_tests/mockApis/tokenVerification' export default defineConfig({ chromeWebSecurity: false, @@ -15,10 +17,16 @@ export default defineConfig({ e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. - setupNodeEvents, + setupNodeEvents(on) { + on('task', { + reset: resetStubs, + ...auth, + ...tokenVerification, + }) + }, baseUrl: 'http://localhost:3007', excludeSpecPattern: '**/!(*.cy).ts', - specPattern: 'integration_tests/integration/**/*.cy.{js,jsx,ts,tsx}', + specPattern: 'integration_tests/e2e/**/*.cy.{js,jsx,ts,tsx}', supportFile: 'integration_tests/support/index.ts', }, }) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 16192a0..ed806b5 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -2,18 +2,16 @@ version: '3.1' services: redis: - image: 'bitnami/redis:5.0' + image: 'redis:6.2' networks: - hmpps_int - environment: - - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379' wiremock: - image: rodolpheche/wiremock + image: wiremock/wiremock networks: - - hmpps_int + - hmpps_int container_name: wiremock restart: always ports: diff --git a/docker-compose.yml b/docker-compose.yml index 007db22..05a6d3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,10 @@ version: '3.1' services: redis: - image: 'bitnami/redis:5.0' + image: 'redis:6.2' networks: - hmpps - container_name: redis + container_name: redis environment: - ALLOW_EMPTY_PASSWORD=yes ports: @@ -15,7 +15,7 @@ services: image: quay.io/hmpps/hmpps-auth:latest networks: - hmpps - container_name: hmpps-auth + container_name: hmpps-auth ports: - "9090:8080" healthcheck: diff --git a/feature.env b/feature.env index fb0bb93..3517efe 100644 --- a/feature.env +++ b/feature.env @@ -1,6 +1,7 @@ PORT=3007 HMPPS_AUTH_URL=http://localhost:9091/auth TOKEN_VERIFICATION_API_URL=http://localhost:9091/verification +TOKEN_VERIFICATION_ENABLED=true NODE_ENV=development API_CLIENT_ID=clientid API_CLIENT_SECRET=clientsecret diff --git a/helm_deploy/hmpps-audit-poc-ui/Chart.yaml b/helm_deploy/hmpps-audit-poc-ui/Chart.yaml index ba544d4..d4adf70 100644 --- a/helm_deploy/hmpps-audit-poc-ui/Chart.yaml +++ b/helm_deploy/hmpps-audit-poc-ui/Chart.yaml @@ -5,8 +5,8 @@ name: hmpps-audit-poc-ui version: 0.2.0 dependencies: - name: generic-service - version: 1.5.0 + version: 2.5.0 repository: https://ministryofjustice.github.io/hmpps-helm-charts - name: generic-prometheus-alerts - version: 1.2.1 + version: 1.2.4 repository: https://ministryofjustice.github.io/hmpps-helm-charts diff --git a/helm_deploy/hmpps-audit-poc-ui/values.yaml b/helm_deploy/hmpps-audit-poc-ui/values.yaml index 87689c1..c88d4c7 100644 --- a/helm_deploy/hmpps-audit-poc-ui/values.yaml +++ b/helm_deploy/hmpps-audit-poc-ui/values.yaml @@ -11,8 +11,6 @@ generic-service: ingress: enabled: true - v1_2_enabled: true - v0_47_enabled: false host: app-hostname.local # override per environment tlsSecretName: hmpps-audit-poc-ui-cert path: / @@ -25,12 +23,18 @@ generic-service: httpGet: path: /ping + custommetrics: + enabled: true + scrapeInterval: 15s + metricsPath: /metrics + metricsPort: 3001 + # Environment variables to load into the deployment env: NODE_ENV: "production" REDIS_TLS_ENABLED: "true" TOKEN_VERIFICATION_ENABLED: "true" - GRAPH_QL_ENABLED: "false" + APPLICATIONINSIGHTS_CONNECTION_STRING: "InstrumentationKey=$(APPINSIGHTS_INSTRUMENTATIONKEY);IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/" # Pre-existing kubernetes secrets to load as environment variables in the deployment. # namespace_secrets: @@ -52,10 +56,13 @@ generic-service: allowlist: office: "217.33.148.210/32" health-kick: "35.177.252.195/32" + petty-france-wifi: "213.121.161.112/28" + global-protect: "35.176.93.186/32" mojvpn: "81.134.202.29/32" - cloudplatform-live1-1: "35.178.209.113/32" - cloudplatform-live1-2: "3.8.51.207/32" - cloudplatform-live1-3: "35.177.252.54/32" + cloudplatform-live-1: "35.178.209.113/32" + cloudplatform-live-2: "3.8.51.207/32" + cloudplatform-live-3: "35.177.252.54/32" + generic-prometheus-alerts: targetApplication: hmpps-audit-poc-ui diff --git a/helm_deploy/values-dev.yaml b/helm_deploy/values-dev.yaml index 5f3f64d..d3f4a8a 100644 --- a/helm_deploy/values-dev.yaml +++ b/helm_deploy/values-dev.yaml @@ -11,7 +11,6 @@ generic-service: INGRESS_URL: "https://hmpps-audit-poc-ui-dev.hmpps.service.justice.gov.uk" HMPPS_AUTH_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth" TOKEN_VERIFICATION_API_URL: "https://token-verification-api-dev.prison.service.justice.gov.uk" - GRAPHQL_API_URL: "https://gql-api-dev.hmpps.service.justice.gov.uk" generic-prometheus-alerts: alertSeverity: digital-prison-service-dev diff --git a/helm_deploy/values-preprod.yaml b/helm_deploy/values-preprod.yaml index 7ffe252..342ba81 100644 --- a/helm_deploy/values-preprod.yaml +++ b/helm_deploy/values-preprod.yaml @@ -11,7 +11,6 @@ generic-service: INGRESS_URL: "https://hmpps-audit-poc-ui-preprod.hmpps.service.justice.gov.uk" HMPPS_AUTH_URL: "https://sign-in-preprod.hmpps.service.justice.gov.uk/auth" TOKEN_VERIFICATION_API_URL: "https://token-verification-api-preprod.prison.service.justice.gov.uk" - GRAPHQL_API_URL: "https://gql-api-dev.hmpps.service.justice.gov.uk" generic-prometheus-alerts: alertSeverity: digital-prison-service-dev diff --git a/helm_deploy/values-prod.yaml b/helm_deploy/values-prod.yaml index 58b657a..48a36eb 100644 --- a/helm_deploy/values-prod.yaml +++ b/helm_deploy/values-prod.yaml @@ -11,4 +11,3 @@ generic-service: INGRESS_URL: "https://hmpps-audit-poc-ui.hmpps.service.justice.gov.uk" HMPPS_AUTH_URL: "https://sign-in.hmpps.service.justice.gov.uk/auth" TOKEN_VERIFICATION_API_URL: "https://token-verification-api.prison.service.justice.gov.uk" - GRAPHQL_API_URL: "https://gql-api-dev.hmpps.service.justice.gov.uk" diff --git a/integration_tests/e2e/health.cy.ts b/integration_tests/e2e/health.cy.ts new file mode 100644 index 0000000..3f2ae12 --- /dev/null +++ b/integration_tests/e2e/health.cy.ts @@ -0,0 +1,30 @@ +context('Healthcheck', () => { + context('All healthy', () => { + beforeEach(() => { + cy.task('reset') + cy.task('stubAuthPing') + cy.task('stubTokenVerificationPing') + }) + + it('Health check page is visible', () => { + cy.request('/health').its('body.healthy').should('equal', true) + }) + + it('Ping is visible and UP', () => { + cy.request('/ping').its('body.status').should('equal', 'UP') + }) + }) + + context('Some unhealthy', () => { + it('Reports correctly when token verification down', () => { + cy.task('reset') + cy.task('stubAuthPing') + cy.task('stubTokenVerificationPing', 500) + + cy.request({ url: '/health', method: 'GET', failOnStatusCode: false }).then(response => { + expect(response.body.checks.hmppsAuth).to.equal('OK') + expect(response.body.checks.tokenVerification).to.contain({ status: 500, retries: 2 }) + }) + }) + }) +}) diff --git a/integration_tests/e2e/login.cy.ts b/integration_tests/e2e/login.cy.ts new file mode 100644 index 0000000..7652ecc --- /dev/null +++ b/integration_tests/e2e/login.cy.ts @@ -0,0 +1,68 @@ +import IndexPage from '../pages/index' +import AuthSignInPage from '../pages/authSignIn' +import Page from '../pages/page' +import AuthManageDetailsPage from '../pages/authManageDetails' + +context('SignIn', () => { + beforeEach(() => { + cy.task('reset') + cy.task('stubSignIn') + cy.task('stubAuthUser') + }) + + it('Unauthenticated user directed to auth', () => { + cy.visit('/') + Page.verifyOnPage(AuthSignInPage) + }) + + it('Unauthenticated user navigating to sign in page directed to auth', () => { + cy.visit('/sign-in') + Page.verifyOnPage(AuthSignInPage) + }) + + it('User name visible in header', () => { + cy.signIn() + const indexPage = Page.verifyOnPage(IndexPage) + indexPage.headerUserName().should('contain.text', 'J. Smith') + }) + + it('User can log out', () => { + cy.signIn() + const indexPage = Page.verifyOnPage(IndexPage) + indexPage.signOut().click() + Page.verifyOnPage(AuthSignInPage) + }) + + it('User can manage their details', () => { + cy.signIn() + const indexPage = Page.verifyOnPage(IndexPage) + + indexPage.manageDetails().get('a').invoke('removeAttr', 'target') + indexPage.manageDetails().click() + Page.verifyOnPage(AuthManageDetailsPage) + }) + + it('Token verification failure takes user to sign in page', () => { + cy.signIn() + Page.verifyOnPage(IndexPage) + cy.task('stubVerifyToken', false) + + // can't do a visit here as cypress requires only one domain + cy.request('/').its('body').should('contain', 'Sign in') + }) + + it('Token verification failure clears user session', () => { + cy.signIn() + const indexPage = Page.verifyOnPage(IndexPage) + cy.task('stubVerifyToken', false) + + // can't do a visit here as cypress requires only one domain + cy.request('/').its('body').should('contain', 'Sign in') + + cy.task('stubVerifyToken', true) + cy.task('stubAuthUser', 'bobby brown') + cy.signIn() + + indexPage.headerUserName().contains('B. Brown') + }) +}) diff --git a/integration_tests/index.d.ts b/integration_tests/index.d.ts index 6a2512b..ce64a17 100644 --- a/integration_tests/index.d.ts +++ b/integration_tests/index.d.ts @@ -1,8 +1,9 @@ declare namespace Cypress { interface Chainable { - /** * Custom command to signIn. Set failOnStatusCode to false if you expect and non 200 return code + /** + * Custom command to signIn. Set failOnStatusCode to false if you expect and non 200 return code * @example cy.signIn({ failOnStatusCode: boolean }) */ - signIn(options?: { failOnStatusCode: boolean }): Chainable + signIn(options?: { failOnStatusCode: boolean }): Chainable } } diff --git a/integration_tests/integration/health.cy.ts b/integration_tests/integration/health.cy.ts deleted file mode 100644 index 4bf75ea..0000000 --- a/integration_tests/integration/health.cy.ts +++ /dev/null @@ -1,15 +0,0 @@ -context('Healthcheck', () => { - beforeEach(() => { - cy.task('reset') - cy.task('stubAuthPing') - cy.task('stubTokenVerificationPing') - }) - - it('Health check page is visible', () => { - cy.request('/health').its('body.healthy').should('equal', true) - }) - - it('Ping is visible and UP', () => { - cy.request('/ping').its('body.status').should('equal', 'UP') - }) -}) diff --git a/integration_tests/integration/login.cy.ts b/integration_tests/integration/login.cy.ts deleted file mode 100644 index 48ab62d..0000000 --- a/integration_tests/integration/login.cy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import IndexPage from '../pages/index' -import AuthSignInPage from '../pages/authSignIn' -import Page from '../pages/page' - -context('SignIn', () => { - beforeEach(() => { - cy.task('reset') - cy.task('stubSignIn') - cy.task('stubAuthUser') - }) - - it('Unauthenticated user directed to auth', () => { - cy.visit('/') - Page.verifyOnPage(AuthSignInPage) - }) - - it('User name visible in header', () => { - cy.signIn() - const indexPage = Page.verifyOnPage(IndexPage) - indexPage.headerUserName().should('contain.text', 'J. Smith') - }) - - it('User can log out', () => { - cy.signIn() - const indexPage = Page.verifyOnPage(IndexPage) - indexPage.signOut().click() - Page.verifyOnPage(AuthSignInPage) - }) -}) diff --git a/integration_tests/mockApis/auth.ts b/integration_tests/mockApis/auth.ts index 79cbc5a..40afa85 100644 --- a/integration_tests/mockApis/auth.ts +++ b/integration_tests/mockApis/auth.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken' import { Response } from 'superagent' -import { stubFor, getRequests } from './wiremock' +import { stubFor, getMatchingRequests } from './wiremock' import tokenVerification from './tokenVerification' const createToken = () => { @@ -18,10 +18,12 @@ const createToken = () => { } const getSignInUrl = (): Promise => - getRequests().then(data => { + getMatchingRequests({ + method: 'GET', + urlPath: '/auth/oauth/authorize', + }).then(data => { const { requests } = data.body - const stateParam = requests[0].request.queryParams.state - const stateValue = stateParam ? stateParam.values[0] : requests[1].request.queryParams.state.values[0] + const stateValue = requests[requests.length - 1].queryParams.state.values[0] return `/sign-in/callback?code=codexxxx&state=${stateValue}` }) @@ -78,6 +80,21 @@ const signOut = () => }, }) +const manageDetails = () => + stubFor({ + request: { + method: 'GET', + urlPattern: '/auth/account-details.*', + }, + response: { + status: 200, + headers: { + 'Content-Type': 'text/html', + }, + body: '

Your account details

', + }, + }) + const token = () => stubFor({ request: { @@ -101,7 +118,7 @@ const token = () => }, }) -const stubUser = () => +const stubUser = (name: string) => stubFor({ request: { method: 'GET', @@ -116,7 +133,7 @@ const stubUser = () => staffId: 231232, username: 'USER1', active: true, - name: 'john smith', + name, }, }, }) @@ -132,14 +149,14 @@ const stubUserRoles = () => headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - jsonBody: [{ roleId: 'SOME_USER_ROLE' }], + jsonBody: [{ roleCode: 'SOME_USER_ROLE' }], }, }) export default { getSignInUrl, - stubPing: (): Promise<[Response, Response]> => Promise.all([ping(), tokenVerification.stubPing()]), - stubSignIn: (): Promise<[Response, Response, Response, Response, Response]> => - Promise.all([favicon(), redirect(), signOut(), token(), tokenVerification.stubVerifyToken()]), - stubUser: (): Promise<[Response, Response]> => Promise.all([stubUser(), stubUserRoles()]), + stubAuthPing: ping, + stubSignIn: (): Promise<[Response, Response, Response, Response, Response, Response]> => + Promise.all([favicon(), redirect(), signOut(), manageDetails(), token(), tokenVerification.stubVerifyToken()]), + stubAuthUser: (name = 'john smith'): Promise<[Response, Response]> => Promise.all([stubUser(name), stubUserRoles()]), } diff --git a/integration_tests/mockApis/tokenVerification.ts b/integration_tests/mockApis/tokenVerification.ts index 192a17c..1b9d47a 100644 --- a/integration_tests/mockApis/tokenVerification.ts +++ b/integration_tests/mockApis/tokenVerification.ts @@ -2,21 +2,20 @@ import { SuperAgentRequest } from 'superagent' import { stubFor } from './wiremock' export default { - stubPing: (): SuperAgentRequest => { - return stubFor({ + stubTokenVerificationPing: (status = 200): SuperAgentRequest => + stubFor({ request: { method: 'GET', urlPattern: '/verification/health/ping', }, response: { - status: 200, + status, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, jsonBody: { status: 'UP' }, }, - }) - }, - stubVerifyToken: (): SuperAgentRequest => { - return stubFor({ + }), + stubVerifyToken: (active = true): SuperAgentRequest => + stubFor({ request: { method: 'POST', urlPattern: '/verification/token/verify', @@ -24,8 +23,7 @@ export default { response: { status: 200, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - jsonBody: { active: 'true' }, + jsonBody: { active }, }, - }) - }, + }), } diff --git a/integration_tests/mockApis/wiremock.ts b/integration_tests/mockApis/wiremock.ts index 4f1d236..428707b 100644 --- a/integration_tests/mockApis/wiremock.ts +++ b/integration_tests/mockApis/wiremock.ts @@ -5,9 +5,9 @@ const url = 'http://localhost:9091/__admin' const stubFor = (mapping: Record): SuperAgentRequest => superagent.post(`${url}/mappings`).send(mapping) -const getRequests = (): SuperAgentRequest => superagent.get(`${url}/requests`) +const getMatchingRequests = body => superagent.post(`${url}/requests/find`).send(body) const resetStubs = (): Promise> => Promise.all([superagent.delete(`${url}/mappings`), superagent.delete(`${url}/requests`)]) -export { stubFor, getRequests, resetStubs } +export { stubFor, getMatchingRequests, resetStubs } diff --git a/integration_tests/pages/authManageDetails.ts b/integration_tests/pages/authManageDetails.ts new file mode 100644 index 0000000..bbaf244 --- /dev/null +++ b/integration_tests/pages/authManageDetails.ts @@ -0,0 +1,7 @@ +import Page from './page' + +export default class AuthManageDetailsPage extends Page { + constructor() { + super('Your account details') + } +} diff --git a/integration_tests/pages/index.ts b/integration_tests/pages/index.ts index b52f619..d20d261 100644 --- a/integration_tests/pages/index.ts +++ b/integration_tests/pages/index.ts @@ -2,7 +2,7 @@ import Page, { PageElement } from './page' export default class IndexPage extends Page { constructor() { - super('Sample Application') + super('This site is under construction...') } headerUserName = (): PageElement => cy.get('[data-qa=header-user-name]') diff --git a/integration_tests/pages/page.ts b/integration_tests/pages/page.ts index d8363e6..0b7c300 100644 --- a/integration_tests/pages/page.ts +++ b/integration_tests/pages/page.ts @@ -14,4 +14,6 @@ export default abstract class Page { } signOut = (): PageElement => cy.get('[data-qa=signOut]') + + manageDetails = (): PageElement => cy.get('[data-qa=manageDetails]') } diff --git a/integration_tests/plugins/index.ts b/integration_tests/plugins/index.ts deleted file mode 100644 index 5145a1f..0000000 --- a/integration_tests/plugins/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { resetStubs } from '../mockApis/wiremock' - -import auth from '../mockApis/auth' -import tokenVerification from '../mockApis/tokenVerification' - -export default (on: (string, Record) => void): void => { - on('task', { - reset: resetStubs, - - getSignInUrl: auth.getSignInUrl, - stubSignIn: auth.stubSignIn, - - stubAuthUser: auth.stubUser, - stubAuthPing: auth.stubPing, - - stubTokenVerificationPing: tokenVerification.stubPing, - }) -} diff --git a/integration_tests/support/commands.ts b/integration_tests/support/commands.ts index fe08e05..e8d0a00 100644 --- a/integration_tests/support/commands.ts +++ b/integration_tests/support/commands.ts @@ -1,4 +1,4 @@ Cypress.Commands.add('signIn', (options = { failOnStatusCode: true }) => { - cy.request(`/`) - cy.task('getSignInUrl').then((url: string) => cy.visit(url, options)) + cy.request('/') + return cy.task('getSignInUrl').then((url: string) => cy.visit(url, options)) }) diff --git a/integration_tests/tsconfig.json b/integration_tests/tsconfig.json index 567c8e2..5428ef1 100644 --- a/integration_tests/tsconfig.json +++ b/integration_tests/tsconfig.json @@ -3,8 +3,10 @@ "target": "es5", "noEmit": true, "lib": ["es5", "dom", "es2015.promise"], - "types": ["cypress"], - "esModuleInterop": true + "types": ["cypress", "express", "express-session"], + "esModuleInterop": true, + "skipLibCheck": true, + "typeRoots": ["../server/@types"] }, "include": ["**/*.ts"] } diff --git a/logger.ts b/logger.ts index 8814cda..ea342bb 100755 --- a/logger.ts +++ b/logger.ts @@ -1,7 +1,8 @@ import bunyan from 'bunyan' import bunyanFormat from 'bunyan-format' +import config from './server/config' -const formatOut = bunyanFormat({ outputMode: 'short', color: true }) +const formatOut = bunyanFormat({ outputMode: 'short', color: !config.production }) const logger = bunyan.createLogger({ name: 'Hmpps Audit Poc Ui', stream: formatOut, level: 'debug' }) diff --git a/package-lock.json b/package-lock.json index 857526b..46aa4d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,19 +10,17 @@ "license": "MIT", "dependencies": { "@ministryofjustice/frontend": "^1.6.4", - "agentkeepalive": "^4.3.0", - "applicationinsights": "^2.5.0", + "agentkeepalive": "^4.2.1", + "applicationinsights": "^2.4.2", "body-parser": "^1.20.2", "bunyan": "^1.8.15", "bunyan-format": "^0.2.1", "compression": "^1.7.4", "connect-flash": "^0.1.1", "connect-redis": "^6.1.3", - "cookie-session": "^2.0.0", "csurf": "^1.11.0", - "dotenv": "^16.0.3", - "eslint-plugin-no-only-tests": "^3.1.0", "express": "^4.18.2", + "express-prom-bundle": "^6.6.0", "express-session": "^1.17.3", "govuk-frontend": "^4.5.0", "helmet": "^6.0.1", @@ -32,7 +30,7 @@ "nocache": "^3.0.4", "nunjucks": "^3.2.3", "passport": "^0.6.0", - "passport-oauth2": "^1.7.0", + "passport-oauth2": "^1.6.1", "prom-client": "^14.1.1", "redis": "^4.6.5", "superagent": "^8.0.9", @@ -51,25 +49,28 @@ "@types/http-errors": "^2.0.1", "@types/jest": "^29.4.0", "@types/jsonwebtoken": "^9.0.1", - "@types/node": "^18.14.6", + "@types/node": "^18.14.2", "@types/nunjucks": "^3.2.2", "@types/passport": "^1.0.12", "@types/passport-oauth2": "^1.4.12", - "@types/redis": "^4.0.11", "@types/superagent": "^4.1.16", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.54.0", - "@typescript-eslint/parser": "^5.54.0", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", + "audit-ci": "^6.6.1", "concurrently": "^7.6.0", + "cookie-session": "^2.0.0", "cypress": "^12.7.0", "cypress-multi-reporters": "^1.6.2", + "dotenv": "^16.0.3", "eslint": "^8.35.0", "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^8.7.0", + "eslint-config-prettier": "^8.6.0", "eslint-import-resolver-typescript": "^3.5.3", "eslint-plugin-cypress": "^2.12.1", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-prettier": "^4.2.1", "husky": "^8.0.3", "jest": "^29.4.3", @@ -79,8 +80,9 @@ "lint-staged": "^13.1.2", "mocha-junit-reporter": "^2.2.0", "nock": "^13.3.0", - "nodemon": "^2.0.21", + "nodemon": "^2.0.20", "prettier": "^2.8.4", + "prettier-plugin-jinja-template": "^0.0.5", "sass": "^1.58.3", "supertest": "^6.3.3", "ts-jest": "^29.0.5", @@ -88,7 +90,7 @@ }, "engines": { "node": "^18", - "npm": "^8" + "npm": "^9" } }, "node_modules/@ampproject/remapping": { @@ -3166,16 +3168,6 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, - "node_modules/@types/redis": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", - "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", - "deprecated": "This is a stub types definition. redis provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "redis": "*" - } - }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -3843,6 +3835,28 @@ "node": ">= 4.0.0" } }, + "node_modules/audit-ci": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/audit-ci/-/audit-ci-6.6.1.tgz", + "integrity": "sha512-zqZEoYfEC4QwX5yBkDNa0h7YhZC63HWtKtP19BVq+RS0dxRBInfmHogxe4VUeOzoADQjuTLZUI7zp3Pjyl+a5g==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "escape-string-regexp": "^4.0.0", + "event-stream": "4.0.1", + "jju": "^1.4.0", + "JSONStream": "^1.3.5", + "readline-transform": "1.0.0", + "semver": "^7.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "audit-ci": "dist/bin.js" + }, + "engines": { + "node": ">=12.9.0" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -4825,6 +4839,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz", "integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==", + "dev": true, "dependencies": { "cookies": "0.8.0", "debug": "3.2.7", @@ -4839,6 +4854,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { "ms": "^2.1.1" } @@ -4847,6 +4863,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -4876,6 +4893,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, "dependencies": { "depd": "~2.0.0", "keygrip": "~1.1.0" @@ -4888,6 +4906,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -5315,6 +5334,7 @@ "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true, "engines": { "node": ">=12" } @@ -5332,6 +5352,12 @@ "node": ">=0.10" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5818,6 +5844,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz", "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==", + "dev": true, "engines": { "node": ">=5.0.0" } @@ -6012,6 +6039,21 @@ "node": ">= 0.6" } }, + "node_modules/event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -6119,6 +6161,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-prom-bundle": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.6.0.tgz", + "integrity": "sha512-tZh2P2p5a8/yxQ5VbRav011Poa4R0mHqdFwn9Swe/obXDe5F0jY9wtRAfNYnqk4LXY7akyvR/nrvAHxQPWUjsQ==", + "dependencies": { + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "prom-client": ">=12.0.0" + } + }, "node_modules/express-session": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", @@ -6564,6 +6621,12 @@ "node": ">= 0.6" } }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -8304,6 +8367,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/jquery": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", @@ -8409,6 +8478,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -8476,6 +8570,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, "dependencies": { "tsscmp": "1.0.6" }, @@ -9018,6 +9113,12 @@ "tmpl": "1.0.5" } }, + "node_modules/map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -9961,6 +10062,15 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -10121,6 +10231,15 @@ "node": ">=6.0.0" } }, + "node_modules/prettier-plugin-jinja-template": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-0.0.5.tgz", + "integrity": "sha512-Db9jtMQUlzE6R+sl/SF91RlgKxEXh0H3edo6W6yaliekZEknzyhqOeGiCNyFsL2UL9mGr74sSvNbncaIVlGmGg==", + "dev": true, + "peerDependencies": { + "prettier": "^2.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -10333,6 +10452,15 @@ "node": ">=8.10.0" } }, + "node_modules/readline-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readline-transform/-/readline-transform-1.0.0.tgz", + "integrity": "sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/redis": { "version": "4.6.5", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.5.tgz", @@ -10967,6 +11095,18 @@ "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", "dev": true }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11038,6 +11178,16 @@ "node": ">= 0.8" } }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -14338,15 +14488,6 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, - "@types/redis": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", - "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", - "dev": true, - "requires": { - "redis": "*" - } - }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -14813,6 +14954,22 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, + "audit-ci": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/audit-ci/-/audit-ci-6.6.1.tgz", + "integrity": "sha512-zqZEoYfEC4QwX5yBkDNa0h7YhZC63HWtKtP19BVq+RS0dxRBInfmHogxe4VUeOzoADQjuTLZUI7zp3Pjyl+a5g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "escape-string-regexp": "^4.0.0", + "event-stream": "4.0.1", + "jju": "^1.4.0", + "JSONStream": "^1.3.5", + "readline-transform": "1.0.0", + "semver": "^7.0.0", + "yargs": "^17.0.0" + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -15532,6 +15689,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz", "integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==", + "dev": true, "requires": { "cookies": "0.8.0", "debug": "3.2.7", @@ -15543,6 +15701,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -15550,7 +15709,8 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true } } }, @@ -15568,6 +15728,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, "requires": { "depd": "~2.0.0", "keygrip": "~1.1.0" @@ -15576,7 +15737,8 @@ "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true } } }, @@ -15897,7 +16059,8 @@ "dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true }, "dtrace-provider": { "version": "0.8.8", @@ -15908,6 +16071,12 @@ "nan": "^2.14.0" } }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -16315,7 +16484,8 @@ "eslint-plugin-no-only-tests": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz", - "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==" + "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==", + "dev": true }, "eslint-plugin-prettier": { "version": "4.2.1", @@ -16427,6 +16597,21 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, "eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -16576,6 +16761,15 @@ } } }, + "express-prom-bundle": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.6.0.tgz", + "integrity": "sha512-tZh2P2p5a8/yxQ5VbRav011Poa4R0mHqdFwn9Swe/obXDe5F0jY9wtRAfNYnqk4LXY7akyvR/nrvAHxQPWUjsQ==", + "requires": { + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" + } + }, "express-session": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", @@ -16859,6 +17053,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -18119,6 +18319,12 @@ "supports-color": "^8.0.0" } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "jquery": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", @@ -18203,6 +18409,22 @@ "universalify": "^2.0.0" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -18263,6 +18485,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, "requires": { "tsscmp": "1.0.6" } @@ -18646,6 +18869,12 @@ "tmpl": "1.0.5" } }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -19348,6 +19577,15 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -19459,6 +19697,13 @@ "fast-diff": "^1.1.2" } }, + "prettier-plugin-jinja-template": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-0.0.5.tgz", + "integrity": "sha512-Db9jtMQUlzE6R+sl/SF91RlgKxEXh0H3edo6W6yaliekZEknzyhqOeGiCNyFsL2UL9mGr74sSvNbncaIVlGmGg==", + "dev": true, + "requires": {} + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -19611,6 +19856,12 @@ "picomatch": "^2.2.1" } }, + "readline-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readline-transform/-/readline-transform-1.0.0.tgz", + "integrity": "sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==", + "dev": true + }, "redis": { "version": "4.6.5", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.5.tgz", @@ -20108,6 +20359,15 @@ "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", "dev": true }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -20164,6 +20424,16 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", diff --git a/package.json b/package.json index 1deb910..7645a8f 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "scripts": { "prepare": "husky install", "copy-views": "cp -R server/views dist/server/", - "compile-sass": "sass --quiet-deps --no-source-map --load-path=node_modules/govuk-frontend --load-path=node_modules/@ministryofjustice/frontend --load-path=. ./assets/sass/application.sass:./assets/stylesheets/application.css ./assets/sass/application-ie8.sass:./assets/stylesheets/application-ie8.css --style compressed", + "compile-sass": "sass --quiet-deps --no-source-map --load-path=node_modules/govuk-frontend --load-path=node_modules/@ministryofjustice/frontend --load-path=. assets/scss/application.scss:./assets/stylesheets/application.css assets/scss/application-ie8.scss:./assets/stylesheets/application-ie8.css --style compressed", "watch-ts": "tsc -w", "watch-views": "nodemon --watch server/views -e html,njk -x npm run copy-views", - "watch-node": "DEBUG=gov-starter-server* nodemon --watch dist/ dist/server.js | bunyan -o short", + "watch-node": "DEBUG=gov-starter-server* nodemon -r dotenv/config --watch dist/ dist/server.js | bunyan -o short", "watch-sass": "npm run compile-sass -- --watch", "build": "npm run compile-sass && tsc && npm run copy-views", "start": "node $NODE_OPTIONS dist/server.js | bunyan -o short", @@ -22,6 +22,7 @@ "lint": "eslint . --cache --max-warnings 0", "typecheck": "tsc && tsc -p integration_tests", "test": "jest", + "test:ci": "jest --runInBand", "security_audit": "npx audit-ci --config audit-ci.json", "int-test": "cypress run --config video=false", "int-test-ui": "cypress open", @@ -29,10 +30,17 @@ }, "engines": { "node": "^18", - "npm": "^8" + "npm": "^9" }, "jest": { - "preset": "ts-jest", + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "isolatedModules": true + } + ] + }, "collectCoverageFrom": [ "server/**/*.{ts,js,jsx,mjs}" ], @@ -85,19 +93,17 @@ }, "dependencies": { "@ministryofjustice/frontend": "^1.6.4", - "agentkeepalive": "^4.3.0", - "applicationinsights": "^2.5.0", + "agentkeepalive": "^4.2.1", + "applicationinsights": "^2.4.2", "body-parser": "^1.20.2", "bunyan": "^1.8.15", "bunyan-format": "^0.2.1", "compression": "^1.7.4", "connect-flash": "^0.1.1", "connect-redis": "^6.1.3", - "cookie-session": "^2.0.0", "csurf": "^1.11.0", - "dotenv": "^16.0.3", - "eslint-plugin-no-only-tests": "^3.1.0", "express": "^4.18.2", + "express-prom-bundle": "^6.6.0", "express-session": "^1.17.3", "govuk-frontend": "^4.5.0", "helmet": "^6.0.1", @@ -107,7 +113,7 @@ "nocache": "^3.0.4", "nunjucks": "^3.2.3", "passport": "^0.6.0", - "passport-oauth2": "^1.7.0", + "passport-oauth2": "^1.6.1", "prom-client": "^14.1.1", "redis": "^4.6.5", "superagent": "^8.0.9", @@ -126,25 +132,28 @@ "@types/http-errors": "^2.0.1", "@types/jest": "^29.4.0", "@types/jsonwebtoken": "^9.0.1", - "@types/node": "^18.14.6", + "@types/node": "^18.14.2", "@types/nunjucks": "^3.2.2", "@types/passport": "^1.0.12", "@types/passport-oauth2": "^1.4.12", - "@types/redis": "^4.0.11", "@types/superagent": "^4.1.16", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.54.0", - "@typescript-eslint/parser": "^5.54.0", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", + "audit-ci": "^6.6.1", "concurrently": "^7.6.0", + "cookie-session": "^2.0.0", "cypress": "^12.7.0", "cypress-multi-reporters": "^1.6.2", + "dotenv": "^16.0.3", "eslint": "^8.35.0", "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^8.7.0", + "eslint-config-prettier": "^8.6.0", "eslint-import-resolver-typescript": "^3.5.3", "eslint-plugin-cypress": "^2.12.1", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-prettier": "^4.2.1", "husky": "^8.0.3", "jest": "^29.4.3", @@ -154,8 +163,9 @@ "lint-staged": "^13.1.2", "mocha-junit-reporter": "^2.2.0", "nock": "^13.3.0", - "nodemon": "^2.0.21", + "nodemon": "^2.0.20", "prettier": "^2.8.4", + "prettier-plugin-jinja-template": "^0.0.5", "sass": "^1.58.3", "supertest": "^6.3.3", "ts-jest": "^29.0.5", diff --git a/server.ts b/server.ts index 6275b30..f04391c 100755 --- a/server.ts +++ b/server.ts @@ -1,16 +1,13 @@ -/* eslint-disable import/first */ -/* - * Do appinsights first as it does some magic instrumentation work, i.e. it affects other 'require's - * In particular, applicationinsights automatically collects bunyan logs - */ -import { initialiseAppInsights, buildAppInsightsClient } from './server/utils/azureAppInsights' +// Require app insights before anything else to allow for instrumentation of bunyan and express +import 'applicationinsights' -initialiseAppInsights() -buildAppInsightsClient() - -import app from './server/index' +import { app, metricsApp } from './server/index' import logger from './logger' app.listen(app.get('port'), () => { logger.info(`Server listening on port ${app.get('port')}`) }) + +metricsApp.listen(metricsApp.get('port'), () => { + logger.info(`Metrics server listening on port ${metricsApp.get('port')}`) +}) diff --git a/server/@types/express/index.d.ts b/server/@types/express/index.d.ts index 8c60718..7174b28 100644 --- a/server/@types/express/index.d.ts +++ b/server/@types/express/index.d.ts @@ -1,17 +1,10 @@ export default {} -export type RequestData = 'basicDetails' | 'sentences' | 'offences' | 'offenderManagers' -export interface PrisonerSearchForm { - lastName?: string - prisonerNumber?: string - data?: Array -} declare module 'express-session' { // Declare that the session will potentially contain these additional fields interface SessionData { returnTo: string nowInMinutes: number - prisonerSearchForm: PrisonerSearchForm } } @@ -26,8 +19,7 @@ export declare global { interface Request { verified?: boolean id: string - flash(type: string, message: Array>): number - flash(message: 'errors'): Array> + logout(done: (err: unknown) => void): void } } } diff --git a/server/app.ts b/server/app.ts index d97e0bc..09cc967 100755 --- a/server/app.ts +++ b/server/app.ts @@ -3,31 +3,31 @@ import express from 'express' import path from 'path' import createError from 'http-errors' -import indexRoutes from './routes' import nunjucksSetup from './utils/nunjucksSetup' import errorHandler from './errorHandler' -import standardRouter from './routes/standardRouter' -import type UserService from './services/userService' +import authorisationMiddleware from './middleware/authorisationMiddleware' +import { metricsMiddleware } from './monitoring/metricsApp' -import setUpWebSession from './middleware/setUpWebSession' -import setUpStaticResources from './middleware/setUpStaticResources' -import setUpWebSecurity from './middleware/setUpWebSecurity' import setUpAuthentication from './middleware/setUpAuthentication' +import setUpCsrf from './middleware/setUpCsrf' +import setUpCurrentUser from './middleware/setUpCurrentUser' import setUpHealthChecks from './middleware/setUpHealthChecks' +import setUpStaticResources from './middleware/setUpStaticResources' import setUpWebRequestParsing from './middleware/setupRequestParsing' -import authorisationMiddleware from './middleware/authorisationMiddleware' -import GraphQLDemoService from './services/graphQLDemoService' +import setUpWebSecurity from './middleware/setUpWebSecurity' +import setUpWebSession from './middleware/setUpWebSession' + +import routes from './routes' +import type { Services } from './services' -export default function createApp( - userService: UserService, - graphQLDemoService: GraphQLDemoService -): express.Application { +export default function createApp(services: Services): express.Application { const app = express() app.set('json spaces', 2) app.set('trust proxy', true) app.set('port', process.env.PORT || 3000) + app.use(metricsMiddleware) app.use(setUpHealthChecks()) app.use(setUpWebSecurity()) app.use(setUpWebSession()) @@ -36,8 +36,10 @@ export default function createApp( nunjucksSetup(app, path) app.use(setUpAuthentication()) app.use(authorisationMiddleware()) + app.use(setUpCsrf()) + app.use(setUpCurrentUser(services)) - app.use('/', indexRoutes(standardRouter(userService), { graphQLDemoService })) + app.use(routes(services)) app.use((req, res, next) => next(createError(404, 'Not found'))) app.use(errorHandler(process.env.NODE_ENV === 'production')) diff --git a/server/config.ts b/server/config.ts index e26a23e..fcc66a1 100755 --- a/server/config.ts +++ b/server/config.ts @@ -1,5 +1,3 @@ -import 'dotenv/config' - const production = process.env.NODE_ENV === 'production' function get(name: string, fallback: T, options = { requireInProduction: false }): T | string { @@ -15,11 +13,11 @@ function get(name: string, fallback: T, options = { requireInProduction: fals const requiredInProduction = { requireInProduction: true } export class AgentConfig { - maxSockets: 100 - - maxFreeSockets: 10 + timeout: number - freeSocketTimeout: 30000 + constructor(timeout = 8000) { + this.timeout = timeout + } } export interface ApiConfig { @@ -32,6 +30,7 @@ export interface ApiConfig { } export default { + production, https: production, staticResourceCacheDuration: 20, redis: { @@ -52,7 +51,7 @@ export default { response: Number(get('HMPPS_AUTH_TIMEOUT_RESPONSE', 10000)), deadline: Number(get('HMPPS_AUTH_TIMEOUT_DEADLINE', 10000)), }, - agent: new AgentConfig(), + agent: new AgentConfig(Number(get('HMPPS_AUTH_TIMEOUT_RESPONSE', 10000))), apiClientId: get('API_CLIENT_ID', 'clientid', requiredInProduction), apiClientSecret: get('API_CLIENT_SECRET', 'clientsecret', requiredInProduction), systemClientId: get('SYSTEM_CLIENT_ID', 'clientid', requiredInProduction), @@ -64,18 +63,9 @@ export default { response: Number(get('TOKEN_VERIFICATION_API_TIMEOUT_RESPONSE', 5000)), deadline: Number(get('TOKEN_VERIFICATION_API_TIMEOUT_DEADLINE', 5000)), }, - agent: new AgentConfig(), + agent: new AgentConfig(Number(get('TOKEN_VERIFICATION_API_TIMEOUT_RESPONSE', 5000))), enabled: get('TOKEN_VERIFICATION_ENABLED', 'false') === 'true', }, - graphQLEndpoint: { - url: get('GRAPHQL_API_URL', 'http://localhost:8080', requiredInProduction) as string, - timeout: { - response: Number(get('HMPPS_AUTH_TIMEOUT_RESPONSE', 10000)), - deadline: Number(get('HMPPS_AUTH_TIMEOUT_DEADLINE', 10000)), - }, - agent: new AgentConfig(), - enabled: get('GRAPH_QL_ENABLED', 'false') === 'true', - }, }, domain: get('INGRESS_URL', 'http://localhost:3000', requiredInProduction), } diff --git a/server/data/healthCheck.test.ts b/server/data/healthCheck.test.ts index e90bf29..636221b 100644 --- a/server/data/healthCheck.test.ts +++ b/server/data/healthCheck.test.ts @@ -15,6 +15,7 @@ describe('Service healthcheck', () => { }) afterEach(() => { + nock.abortPendingRequests() nock.cleanAll() }) diff --git a/server/data/hmppsAuthClient.ts b/server/data/hmppsAuthClient.ts index b11f545..6258e11 100644 --- a/server/data/hmppsAuthClient.ts +++ b/server/data/hmppsAuthClient.ts @@ -1,7 +1,7 @@ import superagent from 'superagent' -import querystring from 'querystring' -import type TokenStore from './tokenStore' +import { URLSearchParams } from 'url' +import type TokenStore from './tokenStore' import logger from '../../logger' import config from '../config' import generateOauthClientToken from '../authentication/clientCredentials' @@ -16,19 +16,18 @@ function getSystemClientTokenFromHmppsAuth(username?: string): Promise { logger.info(`Getting user details: calling HMPPS Auth`) - return this.restClient(token).get({ path: '/api/user/me' }) as Promise + return HmppsAuthClient.restClient(token).get({ path: '/api/user/me' }) as Promise } getUserRoles(token: string): Promise { - return this.restClient(token) + return HmppsAuthClient.restClient(token) .get({ path: '/api/user/me/roles' }) - .then(roles => (roles).map(role => role.roleCode)) as Promise + .then(roles => (roles).map(role => role.roleCode)) } async getSystemClientToken(username?: string): Promise { diff --git a/server/data/index.ts b/server/data/index.ts new file mode 100644 index 0000000..b6eeabd --- /dev/null +++ b/server/data/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable import/first */ +/* + * Do appinsights first as it does some magic instrumentation work, i.e. it affects other 'require's + * In particular, applicationinsights automatically collects bunyan logs + */ +import { initialiseAppInsights, buildAppInsightsClient } from '../utils/azureAppInsights' + +initialiseAppInsights() +buildAppInsightsClient() + +import HmppsAuthClient from './hmppsAuthClient' +import { createRedisClient } from './redisClient' +import TokenStore from './tokenStore' + +type RestClientBuilder = (token: string) => T + +export const dataAccess = () => ({ + hmppsAuthClient: new HmppsAuthClient(new TokenStore(createRedisClient({ legacyMode: false }))), +}) + +export type DataAccess = ReturnType + +export { HmppsAuthClient, RestClientBuilder } diff --git a/server/data/redisClient.ts b/server/data/redisClient.ts index a25ec2a..e9fdd70 100644 --- a/server/data/redisClient.ts +++ b/server/data/redisClient.ts @@ -1,5 +1,6 @@ import { createClient } from 'redis' +import logger from '../../logger' import config from '../config' export type RedisClient = ReturnType @@ -9,10 +10,22 @@ const url = ? `rediss://${config.redis.host}:${config.redis.port}` : `redis://${config.redis.host}:${config.redis.port}` -export const createRedisClient = (legacyMode = false): RedisClient => { - return createClient({ +export const createRedisClient = ({ legacyMode }: { legacyMode: boolean }): RedisClient => { + const client = createClient({ url, password: config.redis.password, legacyMode, + socket: { + reconnectStrategy: (attempts: number) => { + // Exponential back off: 20ms, 40ms, 80ms..., capped to retry every 30 seconds + const nextDelay = Math.min(2 ** attempts * 20, 30000) + logger.info(`Retry Redis connection attempt: ${attempts}, next attempt in: ${nextDelay}ms`) + return nextDelay + }, + }, }) + + client.on('error', (e: Error) => logger.error('Redis client error', e)) + + return client } diff --git a/server/data/restClient.ts b/server/data/restClient.ts index 4987d73..3d736a7 100644 --- a/server/data/restClient.ts +++ b/server/data/restClient.ts @@ -45,7 +45,7 @@ export default class RestClient { return this.config.timeout } - async get({ path = null, query = '', headers = {}, responseType = '', raw = false }: GetRequest): Promise { + async get({ path = null, query = '', headers = {}, responseType = '', raw = false }: GetRequest): Promise { logger.info(`Get using user credentials: calling ${this.name}: ${path} ${query}`) try { const result = await superagent @@ -70,13 +70,13 @@ export default class RestClient { } } - async post({ + async post({ path = null, headers = {}, responseType = '', data = {}, raw = false, - }: PostRequest = {}): Promise { + }: PostRequest = {}): Promise { logger.info(`Post using user credentials: calling ${this.name}: ${path}`) try { const result = await superagent @@ -101,7 +101,7 @@ export default class RestClient { } } - async stream({ path = null, headers = {} }: StreamRequest = {}): Promise { + async stream({ path = null, headers = {} }: StreamRequest = {}): Promise { logger.info(`Get using user credentials: calling ${this.name}: ${path}`) return new Promise((resolve, reject) => { superagent diff --git a/server/data/restClientMetricsMiddleware.test.ts b/server/data/restClientMetricsMiddleware.test.ts index e995bae..4b9cf08 100644 --- a/server/data/restClientMetricsMiddleware.test.ts +++ b/server/data/restClientMetricsMiddleware.test.ts @@ -1,4 +1,5 @@ import superagent from 'superagent' +import nock from 'nock' import { restClientMetricsMiddleware, normalizePath, @@ -32,6 +33,8 @@ describe('restClientMetricsMiddleware', () => { describe('request timers', () => { it('times the whole request', async () => { + const fakeApi = nock('https://httpbin.org/') + fakeApi.get('/', '').reply(200) const requestHistogramLabelsSpy = jest.spyOn(requestHistogram, 'labels').mockReturnValue(requestHistogram) const requestHistogramStartSpy = jest.spyOn(requestHistogram, 'observe') @@ -49,6 +52,7 @@ describe('restClientMetricsMiddleware', () => { expect(requestHistogramLabelsSpy).toHaveBeenCalledTimes(1) expect(requestHistogramLabelsSpy).toHaveBeenCalledWith('httpbin.org', 'GET', '/', '200') expect(requestHistogramStartSpy).toHaveBeenCalledTimes(1) + nock.cleanAll() }) }) diff --git a/server/errorHandler.test.ts b/server/errorHandler.test.ts index d5b4421..7feb5dc 100644 --- a/server/errorHandler.test.ts +++ b/server/errorHandler.test.ts @@ -1,6 +1,6 @@ import type { Express } from 'express' import request from 'supertest' -import appWithAllRoutes from './routes/testutils/appSetup' +import { appWithAllRoutes } from './routes/testutils/appSetup' let app: Express diff --git a/server/index.ts b/server/index.ts index f2157e2..3252fb4 100755 --- a/server/index.ts +++ b/server/index.ts @@ -1,14 +1,11 @@ +import promClient from 'prom-client' +import { createMetricsApp } from './monitoring/metricsApp' import createApp from './app' -import HmppsAuthClient from './data/hmppsAuthClient' -import { createRedisClient } from './data/redisClient' -import TokenStore from './data/tokenStore' -import GraphQLDemoService from './services/graphQLDemoService' -import UserService from './services/userService' +import { services } from './services' -const hmppsAuthClient = new HmppsAuthClient(new TokenStore(createRedisClient())) -const userService = new UserService(hmppsAuthClient) -const graphQLDemoService = new GraphQLDemoService(hmppsAuthClient) +promClient.collectDefaultMetrics() -const app = createApp(userService, graphQLDemoService) +const app = createApp(services()) +const metricsApp = createMetricsApp() -export default app +export { app, metricsApp } diff --git a/server/middleware/authorisationMiddleware.test.ts b/server/middleware/authorisationMiddleware.test.ts index 8793d4c..b44318a 100644 --- a/server/middleware/authorisationMiddleware.test.ts +++ b/server/middleware/authorisationMiddleware.test.ts @@ -27,33 +27,38 @@ describe('authorisationMiddleware', () => { token: createToken(authorities), }, }, - redirect: (redirectUrl: string) => { - return redirectUrl - }, + redirect: jest.fn(), } as unknown as Response } - it('should return next when no required roles', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should return next when no required roles', async () => { const res = createResWithToken({ authorities: [] }) - const authorisationResponse = authorisationMiddleware()(req, res, next) + await authorisationMiddleware()(req, res, next) - expect(authorisationResponse).toEqual(next()) + expect(next).toHaveBeenCalled() + expect(res.redirect).not.toHaveBeenCalled() }) - it('should redirect when user has no authorised roles', () => { + it('should redirect when user has no authorised roles', async () => { const res = createResWithToken({ authorities: [] }) - const authorisationResponse = authorisationMiddleware(['SOME_REQUIRED_ROLE'])(req, res, next) + await authorisationMiddleware(['SOME_REQUIRED_ROLE'])(req, res, next) - expect(authorisationResponse).toEqual('/authError') + expect(next).not.toHaveBeenCalled() + expect(res.redirect).toHaveBeenCalledWith('/authError') }) - it('should return next when user has authorised role', () => { + it('should return next when user has authorised role', async () => { const res = createResWithToken({ authorities: ['SOME_REQUIRED_ROLE'] }) - const authorisationResponse = authorisationMiddleware(['SOME_REQUIRED_ROLE'])(req, res, next) + await authorisationMiddleware(['SOME_REQUIRED_ROLE'])(req, res, next) - expect(authorisationResponse).toEqual(next()) + expect(next).toHaveBeenCalled() + expect(res.redirect).not.toHaveBeenCalled() }) }) diff --git a/server/middleware/authorisationMiddleware.ts b/server/middleware/authorisationMiddleware.ts index 0d921b5..e0a7514 100644 --- a/server/middleware/authorisationMiddleware.ts +++ b/server/middleware/authorisationMiddleware.ts @@ -1,11 +1,12 @@ import jwtDecode from 'jwt-decode' -import { RequestHandler } from 'express' +import type { RequestHandler } from 'express' import logger from '../../logger' +import asyncMiddleware from './asyncMiddleware' export default function authorisationMiddleware(authorisedRoles: string[] = []): RequestHandler { - return (req, res, next) => { - if (res.locals && res.locals.user && res.locals.user.token) { + return asyncMiddleware((req, res, next) => { + if (res.locals?.user?.token) { const { authorities: roles = [] } = jwtDecode(res.locals.user.token) as { authorities?: string[] } if (authorisedRoles.length && !roles.some(role => authorisedRoles.includes(role))) { @@ -18,5 +19,5 @@ export default function authorisationMiddleware(authorisedRoles: string[] = []): req.session.returnTo = req.originalUrl return res.redirect('/sign-in') - } + }) } diff --git a/server/middleware/setUpCsrf.ts b/server/middleware/setUpCsrf.ts new file mode 100644 index 0000000..6f9a1a5 --- /dev/null +++ b/server/middleware/setUpCsrf.ts @@ -0,0 +1,22 @@ +import { Router } from 'express' +import csurf from 'csurf' + +const testMode = process.env.NODE_ENV === 'test' + +export default function setUpCsrf(): Router { + const router = Router({ mergeParams: true }) + + // CSRF protection + if (!testMode) { + router.use(csurf()) + } + + router.use((req, res, next) => { + if (typeof req.csrfToken === 'function') { + res.locals.csrfToken = req.csrfToken() + } + next() + }) + + return router +} diff --git a/server/middleware/setUpCurrentUser.ts b/server/middleware/setUpCurrentUser.ts new file mode 100644 index 0000000..0fdde09 --- /dev/null +++ b/server/middleware/setUpCurrentUser.ts @@ -0,0 +1,12 @@ +import { Router } from 'express' +import auth from '../authentication/auth' +import tokenVerifier from '../data/tokenVerification' +import populateCurrentUser from './populateCurrentUser' +import type { Services } from '../services' + +export default function setUpCurrentUser({ userService }: Services): Router { + const router = Router({ mergeParams: true }) + router.use(auth.authenticationMiddleware(tokenVerifier)) + router.use(populateCurrentUser(userService)) + return router +} diff --git a/server/middleware/setUpStaticResources.ts b/server/middleware/setUpStaticResources.ts index 5a71cc9..fee3529 100644 --- a/server/middleware/setUpStaticResources.ts +++ b/server/middleware/setUpStaticResources.ts @@ -12,7 +12,8 @@ export default function setUpStaticResources(): Router { // Static Resources Configuration const cacheControl = { maxAge: config.staticResourceCacheDuration * 1000 } - ;[ + + Array.of( '/assets', '/assets/stylesheets', '/assets/js', @@ -20,14 +21,16 @@ export default function setUpStaticResources(): Router { '/node_modules/govuk-frontend', '/node_modules/@ministryofjustice/frontend/moj/assets', '/node_modules/@ministryofjustice/frontend', - '/node_modules/jquery/dist', - ].forEach(dir => { + '/node_modules/jquery/dist' + ).forEach(dir => { router.use('/assets', express.static(path.join(process.cwd(), dir), cacheControl)) }) - ;['/node_modules/govuk_frontend_toolkit/images'].forEach(dir => { + + Array.of('/node_modules/govuk_frontend_toolkit/images').forEach(dir => { router.use('/assets/images/icons', express.static(path.join(process.cwd(), dir), cacheControl)) }) - ;['/node_modules/jquery/dist/jquery.min.js'].forEach(dir => { + + Array.of('/node_modules/jquery/dist/jquery.min.js').forEach(dir => { router.use('/assets/js/jquery.min.js', express.static(path.join(process.cwd(), dir), cacheControl)) }) diff --git a/server/middleware/setUpWebSecurity.ts b/server/middleware/setUpWebSecurity.ts index 8f54524..1d76da3 100644 --- a/server/middleware/setUpWebSecurity.ts +++ b/server/middleware/setUpWebSecurity.ts @@ -1,5 +1,6 @@ -import express, { Router } from 'express' +import express, { Router, Request, Response, NextFunction } from 'express' import helmet from 'helmet' +import crypto from 'crypto' export default function setUpWebSecurity(): Router { const router = express.Router() @@ -7,17 +8,27 @@ export default function setUpWebSecurity(): Router { // Secure code best practice - see: // 1. https://expressjs.com/en/advanced/best-practice-security.html, // 2. https://www.npmjs.com/package/helmet + router.use((_req: Request, res: Response, next: NextFunction) => { + res.locals.cspNonce = crypto.randomBytes(16).toString('hex') + next() + }) router.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], - // Hash allows inline script pulled in from https://github.com/alphagov/govuk-frontend/blob/master/src/govuk/template.njk - scriptSrc: ["'self'", 'code.jquery.com', "'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU='"], - styleSrc: ["'self'", 'code.jquery.com'], + // This nonce allows us to use scripts with the use of the `cspNonce` local, e.g (in a Nunjucks template): + // - + {% endblock %}