Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HAAR-1742 Added event trigger button #74

Merged
merged 8 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration_tests/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Page, { PageElement } from './page'

export default class IndexPage extends Page {
constructor() {
super('This site is under construction...')
super('Click this button to trigger an audit event')
}

headerUserName = (): PageElement => cy.get('[data-qa=header-user-name]')
Expand Down
1,134 changes: 1,122 additions & 12 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
]
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.433.0",
"@ministryofjustice/frontend": "^1.8.0",
"agentkeepalive": "^4.5.0",
"applicationinsights": "^2.9.0",
Expand Down
5 changes: 5 additions & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export default {
expiryMinutes: Number(get('WEB_SESSION_TIMEOUT_IN_MINUTES', 120)),
},
apis: {
audit: {
region: get('AUDIT_SQS_REGION', 'eu-west-2', requiredInProduction),
queueUrl: get('AUDIT_SQS_QUEUE_URL', 'http://localhost:4566/000000000000/mainQueue', requiredInProduction),
serviceName: get('AUDIT_SERVICE_NAME', 'hmpps-audit-poc-ui', requiredInProduction),
},
hmppsAuth: {
url: get('HMPPS_AUTH_URL', 'http://localhost:9090/auth', requiredInProduction),
externalUrl: get('HMPPS_AUTH_EXTERNAL_URL', get('HMPPS_AUTH_URL', 'http://localhost:9090/auth')),
Expand Down
61 changes: 54 additions & 7 deletions server/routes/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,71 @@
import type { Express } from 'express'
import request from 'supertest'
import { appWithAllRoutes } from './testutils/appSetup'
import auditService from '../services/auditService'

let app: Express

beforeEach(() => {
app = appWithAllRoutes({})
})
const sendAuditMessageMock = jest.fn()

afterEach(() => {
jest.resetAllMocks()
})

describe('GET /', () => {
it('should render index page', () => {
describe('index.test.ts', () => {
jest.mock('../services/auditService', () => ({
sendAuditMessage: sendAuditMessageMock,
}))

beforeEach(() => {
app = appWithAllRoutes({})
jest.spyOn(auditService, 'sendAuditMessage').mockResolvedValue({} as never)
})

describe('GET /', () => {
it('should render index page', () => {
return request(app)
.get('/')
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('Click this button to trigger an audit event')
expect(res.text).toContain('Trigger Event')
})
})
})

it('GET /triggered-event', () => {
const mockPublishedEvent = {
action: 'TEST_EVENT',
who: 'some username',
subjectId: 'some user ID',
subjectType: 'USER_ID',
details: JSON.stringify({ testField: 'some value' }),
}

return request(app)
.get('/')
.get('/triggered-event')
.expect('Content-Type', /html/)
.expect(res => {
expect(res.text).toContain('This site is under construction...')
expect(res.text).toContain('Congratulations! 🎉')
expect(res.text).toContain(
"You've just published an event that has been pushed to a queue. This event will be stored in the audit",
)
expect(res.text).toContain('Published Message (JSON Format):')
expect(res.text).toContain("Here's an explanation of what this message means:")
expect(res.text).toContain('What:</b> Which action was performed?')
expect(res.text).toContain('<b>When:</b> Timestamp of the event.')
expect(res.text).toContain('<b>Who:</b> Which user initiated the event?')
expect(res.text).toContain('<b>Subject ID:</b> The subject against which the action was performed.')
expect(res.text).toContain(
'<b>Subject Type:</b> The type of subject ID we are using. This is most commonly a user ID.',
)
expect(res.text).toContain('<b>Service:</b> Which service performed this action?')
expect(res.text).toContain(
'<b>Details:</b> Any additional details specific to this action that may be relevant can go here.',
)
})
.expect(() => {
expect(auditService.sendAuditMessage).toBeCalledWith(mockPublishedEvent)
})
})
})
16 changes: 15 additions & 1 deletion server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@ import { type RequestHandler, Router } from 'express'

import asyncMiddleware from '../middleware/asyncMiddleware'
import type { Services } from '../services'
import auditService from '../services/auditService'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function routes(service: Services): Router {
const router = Router()
const get = (path: string | string[], handler: RequestHandler) => router.get(path, asyncMiddleware(handler))

get('/', (req, res, next) => {
res.render('pages/index')
res.render('pages/index', { eventLink: '/triggered-event' })
})

get('/triggered-event', async (req, res, next) => {
const publishedEvent = await auditService.sendAuditMessage({
action: 'TEST_EVENT',
who: 'some username',
subjectId: 'some user ID',
subjectType: 'USER_ID',
details: JSON.stringify({ testField: 'some value' }),
})
res.render('pages/triggeredEvent', {
publishedEvent,
})
})

return router
Expand Down
37 changes: 37 additions & 0 deletions server/services/auditService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SQSClient } from '@aws-sdk/client-sqs'
import auditService from './auditService'

const adminId = 'some admin'
const userId = 'some user'
const userIdSubjectType = 'USER_ID'

describe('Audit service', () => {
beforeEach(() => {
jest.resetAllMocks()
})

it('sends audit message', async () => {
jest.spyOn(SQSClient.prototype, 'send').mockResolvedValue({} as never)
const expectedWhat = 'DISABLE_USER'

jest.spyOn(SQSClient.prototype, 'send').mockResolvedValue({} as never)
await auditService.sendAuditMessage({
action: 'DISABLE_USER',
who: adminId,
subjectId: userId,
subjectType: userIdSubjectType,
details: JSON.stringify({ something: 'some details' }),
})
const { MessageBody, QueueUrl } = (SQSClient.prototype.send as jest.Mock).mock.calls[0][0].input
const { what, when, who, service, subjectId, subjectType, details } = JSON.parse(MessageBody)

expect(QueueUrl).toEqual('http://localhost:4566/000000000000/mainQueue')
expect(what).toEqual(expectedWhat)
expect(when).toBeDefined()
expect(who).toEqual(adminId)
expect(subjectId).toEqual(subjectId)
expect(subjectType).toEqual(userIdSubjectType)
expect(service).toEqual('hmpps-audit-poc-ui')
expect(details).toEqual('{"something":"some details"}')
})
})
54 changes: 54 additions & 0 deletions server/services/auditService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'
import logger from '../../logger'
import config from '../config'

class AuditService {
private sqsClient: SQSClient

constructor(private readonly queueUrl = config.apis.audit.queueUrl) {
this.sqsClient = new SQSClient({
region: config.apis.audit.region,
})
}

async sendAuditMessage({
action,
who,
subjectId,
subjectType,
details,
}: {
action: string
who: string
subjectId?: string
subjectType?: string
details?: string
}) {
try {
const message = JSON.stringify({
what: action,
when: new Date(),
who,
subjectId,
subjectType,
service: config.apis.audit.serviceName,
details,
})

const messageResponse = await this.sqsClient.send(
new SendMessageCommand({
MessageBody: message,
QueueUrl: this.queueUrl,
}),
)

logger.info(`SQS message sent (${messageResponse.MessageId})`)
return message
} catch (error) {
logger.error('Problem sending message to SQS queue', error)
throw error
}
}
}

export default new AuditService()
8 changes: 8 additions & 0 deletions server/utils/nunjucksSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ export default function nunjucksSetup(app: express.Express, applicationInfo: App
)

njkEnv.addFilter('initialiseName', initialiseName)
njkEnv.addFilter('prettifyJSON', jsonString => {
try {
const jsonObj = JSON.parse(jsonString)
return JSON.stringify(jsonObj, null, 2)
} catch (error) {
return jsonString
}
})
}
10 changes: 8 additions & 2 deletions server/views/pages/index.njk
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
{% extends "../partials/layout.njk" %}
{% from "govuk/components/button/macro.njk" import govukButton %}

{% set pageTitle = applicationName + " - Home" %}
{% set mainClasses = "app-container govuk-body" %}

{% block content %}

<h1>This site is under construction...</h1>
<p>Please check back later when there is content to view.</p>
<div class="govuk-body">
<h1>Click this button to trigger an audit event</h1>
<p>{{ govukButton({
text: "Trigger Event",
href: eventLink
}) }}</p>
</div>

{% endblock %}
35 changes: 35 additions & 0 deletions server/views/pages/triggeredEvent.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% extends "../partials/layout.njk" %}
{% from "govuk/components/button/macro.njk" import govukButton %}

{% set pageTitle = applicationName + " - Home" %}
{% set mainClasses = "app-container govuk-body" %}

{% block content %}
<div class="govuk-body">
<h1>Congratulations! 🎉</h1>
<h3>You've just published an event that has been pushed to a queue. This event will be stored in the audit
database.</h3>
<div>
<h3>Published Message (JSON Format):</h3>
<pre>{{ publishedEvent | prettifyJSON }}</pre>
</div>
<h3>Here's an explanation of what this message means:</h3>
<p>
<b>What:</b> Which action was performed? This can be any string but should follow the convention of being
brief,
descriptive and capitalised.<br>
<b>When:</b> Timestamp of the event.<br>
<b>Who:</b> Which user initiated the event?<br>
<b>Subject ID:</b> The subject against which the action was performed. For example, if the action being
audited was a change of John's email address, then the subject ID is John's user ID.<br>
<b>Subject Type:</b> The type of subject ID we are using. This is most commonly a user ID.<br>
<b>Service:</b> Which service performed this action?<br>
<b>Details:</b> Any additional details specific to this action that may be relevant can go here. This can be
anything but must be in the format of a stringifed JSON.<br>
</p>
<p>{{ govukButton({
text: "Back",
href: "/"
}) }}</p>
</div>
{% endblock %}