Skip to content

Commit

Permalink
SIR-672: Initial work for service working hours (#89)
Browse files Browse the repository at this point in the history
* Initial work for service working hours

* test for service unavailable page

* adding test coverage

* github actions timezone

* sonarcloud

* test coverage increase

* taking out commented code

* adding some error logging for working hours

* linting

* adding some date logs

* reject error for test coverage

* fixing test

* testing rejectUnauthorized to false with the bank holiday api

* adding in minutes for working hours test just in case business want to use minutes
  • Loading branch information
teddmason authored Mar 28, 2024
1 parent b35056b commit 2362bf5
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 12 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- name: Set Timezone to London
uses: szenius/[email protected]
with:
timezoneLinux: "Europe/London"
- name: Checkout Repository
uses: actions/checkout@v2
with:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,7 @@ via the environment variable `AUTH_ACCOUNTS`

AUTH_ACCOUNTS should be set as an array of objects as a JSON string in the following format:
`[{"id":1,"password":"PASSWORD_HASH"}]`

# Service availability

The service in production for private beta is only to be available in working hours, set this cron with the environment variable `SERVICE_AVAILABLE_CRON` with the value `'* * 9-16 * * 1-5'` this will disable the home login page outside of working hours and for bank holidays. If this env var is not set or if it is set to `'* * * * * *'` then the service will be available at all times and dates.
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"ajv-formats": "^2.1.1",
"bcrypt": "^5.1.1",
"blipp": "^4.0.2",
"cron-parser": "^4.9.0",
"govuk-frontend": "^4.6.0",
"hapi-pino": "12.1.0",
"joi": "^17.12.2",
Expand Down
16 changes: 16 additions & 0 deletions server/routes/__tests__/home-unavailable.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { submitGetRequest } from '../../__test-helpers__/server.js'
import constants from '../../utils/constants.js'
jest.mock('../../utils/is-working-hours', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve(false)) // outside of working hours
}))
const url = '/'

describe(url, () => {
describe('GET', () => {
it('Should Redirect to service unavailable when outside of working hours', async () => {
const response = await submitGetRequest({ url }, '', constants.statusCodes.REDIRECT)
expect(response.headers.location).toEqual(constants.routes.SERVICE_UNAVAILABLE)
})
})
})
8 changes: 7 additions & 1 deletion server/routes/__tests__/home.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { submitGetRequest, submitPostRequest } from '../../__test-helpers__/server.js'
// import constants from '../../utils/constants.js'
const url = '/'

// As the mocked return for this default es6 module export gets cached, separate test file
// named home-unavailable.spec.js tests the outside of working hours logic
jest.mock('../../utils/is-working-hours', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve(true))
}))

describe(url, () => {
describe('GET', () => {
it('Should display sign in view', async () => {
Expand Down
12 changes: 12 additions & 0 deletions server/routes/__tests__/service-unavailable.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { submitGetRequest } from '../../__test-helpers__/server.js'
import constants from '../../utils/constants.js'

const url = constants.routes.SERVICE_UNAVAILABLE

describe(url, () => {
describe('GET', () => {
it(`Should return success response and correct view for ${url}`, async () => {
await submitGetRequest({ url }, 'Sorry, the service is unavailable')
})
})
})
8 changes: 7 additions & 1 deletion server/routes/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import constants from '../utils/constants.js'
import bcrypt from 'bcrypt'
import config from '../utils/config.js'
import { getErrorSummary } from '../utils/helpers.js'
import isWorkingHours from '../utils/is-working-hours.js'

const handlers = {
get: async (request, h) => {
request.cookieAuth.clear()
return h.view(constants.views.HOME)
if (await isWorkingHours()) {
return h.view(constants.views.HOME)
} else {
request.logger.warn('Service unavailable outside of working hours')
return h.redirect(constants.routes.SERVICE_UNAVAILABLE)
}
},
post: async (request, h) => {
const { fullName, phone, accessCode } = request.payload
Expand Down
16 changes: 16 additions & 0 deletions server/routes/service-unavailable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import constants from '../utils/constants.js'

const handlers = {
get: (_request, h) => h.view(constants.views.SERVICE_UNAVAILABLE)
}

export default [
{
method: 'GET',
path: constants.routes.SERVICE_UNAVAILABLE,
handler: handlers.get,
options: {
auth: false
}
}
]
96 changes: 96 additions & 0 deletions server/utils/__tests__/is-working-hours.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import wreck from '@hapi/wreck'
jest.mock('@hapi/wreck')

describe('Is working hours', () => {
it('Test some dates in a single IT to avoid all the hapi server wrapper stuff repeating', done => {
jest.isolateModules(async () => {
try {
process.env.SERVICE_AVAILABLE_CRON = '* * 9-16 * * 1-5'
const isWorkingHours = require('../is-working-hours').default
wreck.get.mockResolvedValue({
res: {
statusCode: 200
},
payload: {
'england-and-wales': {
events: [{
date: '2024-08-26'
}]
}
}
})

// Happy paths
// Standard date within working hours
expect(await isWorkingHours(new Date('2024-08-01T12:00:00.000Z'))).toBeTruthy()
expect(await isWorkingHours(new Date('2024-08-01T15:30:00.000Z'))).toBeTruthy()

// BST + GMT 16:30 and 08:30 times
expect(await isWorkingHours(new Date('2024-08-01T16:30:00.000Z'))).toBeFalsy()
expect(await isWorkingHours(new Date('2024-08-01T08:30:00.000Z'))).toBeTruthy()
expect(await isWorkingHours(new Date('2024-03-26T16:30:00.000Z'))).toBeTruthy()
expect(await isWorkingHours(new Date('2024-03-26T08:30:00.000Z'))).toBeFalsy()

// Weekend
expect(await isWorkingHours(new Date('2024-03-30T12:00:00.000Z'))).toBeFalsy()
expect(await isWorkingHours(new Date('2024-03-31T12:00:00.000Z'))).toBeFalsy()

// Week but outside hours
expect(await isWorkingHours(new Date('2024-03-26T17:00:00.000Z'))).toBeFalsy()
expect(await isWorkingHours(new Date('2024-03-26T08:59:59.999Z'))).toBeFalsy()

// bank holiday inside hours
expect(await isWorkingHours(new Date('2024-08-26T12:00:00.000Z'))).toBeFalsy()

// bank holiday outside hours
expect(await isWorkingHours(new Date('2024-08-26T18:00:00.000Z'))).toBeFalsy()

expect(wreck.get).toHaveBeenCalledTimes(12)

done()
} catch (e) {
done(e)
}
})
})

it('Should return true if cron set to * * * * * * and datetime outside working hours', done => {
jest.isolateModules(async () => {
try {
process.env.SERVICE_AVAILABLE_CRON = '* * * * * *'
const isWorkingHours = require('../is-working-hours').default
expect(await isWorkingHours(new Date('2024-03-26T00:00:00.000Z'))).toBeTruthy()
expect(wreck.get).toHaveBeenCalledTimes(0)
done()
} catch (e) {
done(e)
}
})
})
it('Should return true if cron set to * * * * * * and no datetime provided', done => {
jest.isolateModules(async () => {
try {
process.env.SERVICE_AVAILABLE_CRON = '* * * * * *'
const isWorkingHours = require('../is-working-hours').default
expect(await isWorkingHours()).toBeTruthy()
expect(wreck.get).toHaveBeenCalledTimes(0)
done()
} catch (e) {
done(e)
}
})
})
it('Should catch error and reject', done => {
jest.isolateModules(async () => {
try {
process.env.SERVICE_AVAILABLE_CRON = '* * 9-16 * * 1-5'
wreck.get = jest.fn().mockImplementation(() => Promise.reject(new Error('test error')))
const isWorkingHours = require('../is-working-hours').default
await expect(isWorkingHours()).rejects.toEqual(new Error('test error'))
done()
} catch (e) {
done(e)
}
})
})
})
6 changes: 4 additions & 2 deletions server/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const schema = Joi.object().keys({
accounts: Joi.array().items(Joi.object().keys({
id: Joi.number(),
password: Joi.string()
})).required()
})).required(),
serviceAvailableCron: Joi.string().default('* * * * * *')
})

// Build config
Expand All @@ -49,7 +50,8 @@ const config = {
osKey: process.env.OS_KEY,
serviceBusConnectionString: process.env.SERVICE_BUS_CONNECTION_STRING,
serviceBusQueueName: process.env.SERVICE_BUS_QUEUE_NAME,
accounts: JSON.parse(process.env.AUTH_ACCOUNTS)
accounts: JSON.parse(process.env.AUTH_ACCOUNTS),
serviceAvailableCron: process.env.SERVICE_AVAILABLE_CRON
}

// Validate config
Expand Down
2 changes: 2 additions & 0 deletions server/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const PRIVACY_NOTICE = 'privacy-notice'
const ERROR = 'error'
const PUBLIC = 'public'
const HOME = 'home'
const SERVICE_UNAVAILABLE = 'service-unavailable'
const REPORT_SENT = 'report-sent'
const FEEDBACK = 'feedback'
const FEEDBACK_SUCCESS = 'feedback-success'
Expand All @@ -40,6 +41,7 @@ const views = {
ERROR,
PUBLIC,
HOME,
SERVICE_UNAVAILABLE,
FEEDBACK,
FEEDBACK_SUCCESS,
REPORT_SENT,
Expand Down
43 changes: 43 additions & 0 deletions server/utils/is-working-hours.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Wreck from '@hapi/wreck'
import parser from 'cron-parser'
import config from './config.js'

const isWorkingHours = async (dateToTest = new Date()) => {
try {
if (config.serviceAvailableCron === '* * * * * *') {
return true
}

// logging for azure checking timezone correct when BST
console.log(`Testing date: ${dateToTest.toLocaleString()}`)
console.log(`Date TZ: ${dateToTest}`)
console.log(`Date TZ offset: ${dateToTest.getTimezoneOffset()}`)

// Get bank holiday data
const { payload } = await Wreck.get('https://www.gov.uk/bank-holidays.json', {
json: true,
rejectUnauthorized: false
})

const dateString = dateToTest.toLocaleDateString('en-GB', { timeZone: 'Europe/London' })

const bankHoliday = payload['england-and-wales'].events.find(event => dateString === new Date(event.date).toLocaleDateString('en-GB', { timeZone: 'Europe/London' }))

if (bankHoliday) {
return false
}

const interval = parser.parseExpression(config.serviceAvailableCron, {
currentDate: dateToTest
})

return interval.fields.dayOfWeek.includes(dateToTest.getDay()) &&
interval.fields.hour.includes(dateToTest.getHours()) &&
interval.fields.minute.includes(dateToTest.getMinutes())
} catch (err) {
console.error(err)
throw err
}
}

export default isWorkingHours
16 changes: 8 additions & 8 deletions server/views/report-sent.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends 'layout.html' %}

{% set hideBackLink = true %}
{% set pageTitle = 'Report received' %}

{% block content %}
Expand All @@ -9,23 +10,22 @@
{{ govukPanel({
titleText: "Report sent",
html: "We have received your report"
}) }}
}) }}
<h2 class="govuk-heading-m">
What happens next
</h2>
<p>An agent will look into the problem. This may include:
<ul class="govuk-list govuk-list--bullet">
<li>checking for similar reports in the area</li>
<li>sending someone to investigate if needed</li>
<li>passing details of the problem to a local team or another
organisation who can help</li>
<li>passing details of the problem to a local team or another organisation who can help</li>
</ul>

{{ govukInsetText({
text: "Do not contact the Environment Agency hotline - they
will not be able to give you any updates about your report."
}) }}
<p><a href="/">Make another report</a></p>
text: "Do not contact the Environment Agency - they cannot give updates about your report."
}) }}
<p>The online service is monitored from 9am to 5pm, Monday to Friday, not including bank holidays. See how to <a href="https://www.gov.uk/report-an-environmental-incident">report environmental problems</a> outside these times.</p>

<p><a href="/">Make another report</a>.</p>
<p><a href="/feedback">Give feedback</a> on the service to help improve it.</p>
</div>
</div>
Expand Down
Loading

0 comments on commit 2362bf5

Please sign in to comment.