Skip to content

Commit 96a481c

Browse files
committed
Merge branch 'main' into RA-243/add-log-prisoner-details-tests
2 parents 5ab3449 + 1ba25b6 commit 96a481c

12 files changed

+346
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Page from '../pages/page'
2+
import SwapVosPinCreditDetailsPage from '../pages/swapVosPinCreditDetailsPage'
3+
4+
context('Swap VOs for PIN Credit Details Page', () => {
5+
let page: SwapVosPinCreditDetailsPage
6+
beforeEach(() => {
7+
cy.task('reset')
8+
cy.task('stubSignIn')
9+
cy.signIn()
10+
cy.visit('/log/swap-vos-pin-credit-details')
11+
12+
// Navigate through the pages
13+
cy.contains('Swap visiting orders (VOs) for PIN credit').click()
14+
cy.contains('button', 'Continue').click() // Click the Continue button on the 'Select application type' page
15+
cy.contains('button', 'Continue').click() // Click the Continue button on the 'Log prisoner details' page
16+
page = Page.verifyOnPage(SwapVosPinCreditDetailsPage)
17+
})
18+
19+
it('should direct the user to the correct page', () => {
20+
Page.verifyOnPage(SwapVosPinCreditDetailsPage)
21+
})
22+
it('should display the correct page title', () => {
23+
page.pageTitle().should('include', 'Log swap VOs for PIN credit details')
24+
})
25+
it('should render the page heading correctly', () => {
26+
page.checkOnPage()
27+
})
28+
it('should render the back link with correct text and href', () => {
29+
page.backLink().should('have.text', 'Back').and('have.attr', 'href', '/log/prisoner-details')
30+
})
31+
it('should render the correct app type title', () => {
32+
page.appTypeTitle().should('have.text', 'Swap VOs for PIN credit')
33+
})
34+
it('should render the correct form label for the textarea', () => {
35+
page.formLabel().should('contain.text', 'Details (optional)')
36+
})
37+
it('should display the hint text correctly', () => {
38+
page.hintText().should('contain.text', 'Add a brief summary, for example, if this person is a Foreign National')
39+
})
40+
it('should contain a textarea with the correct ID', () => {
41+
page.textArea().should('have.attr', 'id', 'swap-vos-pin-credit-details')
42+
})
43+
it('should include a hidden CSRF token input field', () => {
44+
page.csrfToken().should('exist')
45+
})
46+
it('should render a Continue button with the correct text', () => {
47+
page.continueButton().should('contain.text', 'Continue')
48+
})
49+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Page from './page'
2+
3+
export default class SwapVosPinCreditDetailsPage extends Page {
4+
constructor() {
5+
super('Log details')
6+
}
7+
8+
backLink = () => cy.get('.govuk-back-link')
9+
10+
appTypeTitle = () => cy.get('.govuk-caption-xl')
11+
12+
pageTitle = () => cy.title()
13+
14+
hintText = () => cy.get('#swap-vos-pin-credit-details-hint')
15+
16+
formLabel = () => cy.get('label[for="swap-vos-pin-credit-details"]')
17+
18+
textArea = () => cy.get('#swap-vos-pin-credit-details')
19+
20+
csrfToken = () => cy.get('input[name="_csrf"]')
21+
22+
continueButton = () => cy.get('.govuk-button--primary')
23+
}

server/@types/express/index.d.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import { HmppsUser } from '../../interfaces/hmppsUser'
22

33
export declare module 'express-session' {
4-
// Declare that the session will potentially contain these additional fields
54
interface SessionData {
65
returnTo: string
76
nowInMinutes: number
8-
applicationData?: {
9-
type: {
10-
value: string
11-
name: string
12-
}
13-
}
7+
applicationData?: ApplicationData
8+
}
9+
10+
interface ApplicationData {
11+
type?: ApplicationType
12+
prisonerName?: string
13+
date?: Date
14+
additionalData?: AdditionalApplicationData
15+
}
16+
17+
interface ApplicationType {
18+
value: string
19+
name: string
20+
}
21+
22+
type AdditionalApplicationData = SwapVOsForPinCreditDetails
23+
24+
interface SwapVOsForPinCreditDetails {
25+
swapVOsToPinCreditDetails: string
1426
}
1527
}
1628

server/routes/applications/applicationTypeRoutes.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express'
22
import { APPLICATION_TYPES } from '../../constants/applicationTypes'
33
import asyncMiddleware from '../../middleware/asyncMiddleware'
44
import AuditService, { Page } from '../../services/auditService'
5+
import { updateSessionData } from '../../utils/session'
56

67
export default function applicationTypeRoutes({ auditService }: { auditService: AuditService }): Router {
78
const router = Router()
@@ -36,9 +37,7 @@ export default function applicationTypeRoutes({ auditService }: { auditService:
3637
return
3738
}
3839

39-
req.session.applicationData = {
40-
type: selectedAppType,
41-
}
40+
updateSessionData(req, { type: selectedAppType })
4241

4342
res.redirect(`/log/prisoner-details`)
4443
}),

server/routes/applications/prisonerDetailsRoutes.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Request, Response, Router } from 'express'
22
import asyncMiddleware from '../../middleware/asyncMiddleware'
33
import AuditService, { Page } from '../../services/auditService'
4+
import { updateSessionData } from '../../utils/session'
45

56
export default function prisonerDetailsRoutes({ auditService }: { auditService: AuditService }): Router {
67
const router = Router()
@@ -27,6 +28,16 @@ export default function prisonerDetailsRoutes({ auditService }: { auditService:
2728
router.post(
2829
'/log/prisoner-details',
2930
asyncMiddleware(async (req: Request, res: Response) => {
31+
if (!req.session.applicationData) {
32+
res.status(400).send('Application data is missing')
33+
return
34+
}
35+
36+
updateSessionData(req, {
37+
prisonerName: req.body.prisonerName,
38+
date: req.body.date,
39+
})
40+
3041
res.redirect(`/log/swap-vos-pin-credit-details`)
3142
}),
3243
)

server/routes/applications/swapVosPinCreditDetailsRoutes.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Request, Response, Router } from 'express'
2+
import { APPLICATION_TYPES } from '../../constants/applicationTypes'
23
import asyncMiddleware from '../../middleware/asyncMiddleware'
34
import AuditService, { Page } from '../../services/auditService'
4-
import { APPLICATION_TYPES } from '../../constants/applicationTypes'
5+
import { updateSessionData } from '../../utils/session'
56

67
export default function swapVosPinCreditDetailsRoutes({ auditService }: { auditService: AuditService }): Router {
78
const router = Router()
@@ -27,5 +28,41 @@ export default function swapVosPinCreditDetailsRoutes({ auditService }: { auditS
2728
}),
2829
)
2930

31+
router.post(
32+
'/log/swap-vos-pin-credit-details',
33+
asyncMiddleware(async (req: Request, res: Response) => {
34+
updateSessionData(req, {
35+
additionalData: {
36+
...req.session.applicationData?.additionalData,
37+
swapVOsToPinCreditDetails: req.body.swapVosPinCreditDetails,
38+
},
39+
})
40+
41+
res.redirect(`/log/swap-vos-pin-credit-details/confirm`)
42+
}),
43+
)
44+
45+
router.get(
46+
'/log/swap-vos-pin-credit-details/confirm',
47+
asyncMiddleware(async (req: Request, res: Response) => {
48+
await auditService.logPageView(Page.CONFIRM_SWAP_VOS_PIN_CREDIT_DETAILS_PAGE, {
49+
who: res.locals.user.username,
50+
correlationId: req.id,
51+
})
52+
53+
const selectedAppType = APPLICATION_TYPES.find(type => type.value === req.session.applicationData?.type.value)
54+
55+
if (!selectedAppType) {
56+
return res.redirect('/log/application-type')
57+
}
58+
59+
return res.render('pages/log/confirm-swap-vos-pin-credit-details', {
60+
title: 'Check details',
61+
appTypeTitle: 'Swap VOs for PIN credit',
62+
session: req.session,
63+
})
64+
}),
65+
)
66+
3067
return router
3168
}

server/services/auditService.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum Page {
66
LOG_APPLICATION_TYPE_PAGE = 'LOG_APPLICATION_TYPE_PAGE',
77
LOG_PRISONER_DETAILS_PAGE = 'LOG_PRISONER_DETAILS_PAGE',
88
LOG_SWAP_VOS_PIN_CREDIT_DETAILS_PAGE = 'LOG_SWAP_VOS_PIN_CREDIT_DETAILS_PAGE',
9+
CONFIRM_SWAP_VOS_PIN_CREDIT_DETAILS_PAGE = 'CONFIRM_SWAP_VOS_PIN_CREDIT_DETAILS_PAGE',
910
}
1011

1112
export interface PageViewEventDetails {

server/utils/session.test.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Request } from 'express'
2+
import { SessionData } from 'express-session'
3+
import { updateSessionData } from './session'
4+
5+
describe(updateSessionData.name, () => {
6+
let req: Request
7+
8+
beforeEach(() => {
9+
req = {
10+
session: {} as SessionData,
11+
} as Request
12+
})
13+
14+
it('should initialize applicationData if it does not exist', () => {
15+
updateSessionData(req, { prisonerName: 'John Doe' })
16+
17+
expect(req.session.applicationData).toEqual({
18+
prisonerName: 'John Doe',
19+
})
20+
})
21+
22+
it('should update an existing field without removing other fields', () => {
23+
req.session.applicationData = {
24+
type: { name: 'Swap VOs', value: 'swap-vos' },
25+
prisonerName: 'Jane Doe',
26+
}
27+
28+
updateSessionData(req, { prisonerName: 'John Doe' })
29+
30+
expect(req.session.applicationData).toEqual({
31+
type: { name: 'Swap VOs', value: 'swap-vos' },
32+
prisonerName: 'John Doe',
33+
})
34+
})
35+
36+
it('should add new fields while keeping existing ones', () => {
37+
req.session.applicationData = {
38+
type: { name: 'Swap VOs', value: 'swap-vos' },
39+
}
40+
41+
updateSessionData(req, { prisonerName: 'John Doe', date: new Date('2024-02-05') })
42+
43+
expect(req.session.applicationData).toEqual({
44+
type: { name: 'Swap VOs', value: 'swap-vos' },
45+
prisonerName: 'John Doe',
46+
date: new Date('2024-02-05'),
47+
})
48+
})
49+
50+
it('should not overwrite nested objects but merge updates', () => {
51+
req.session.applicationData = {
52+
type: { name: 'Swap VOs', value: 'swap-vos' },
53+
additionalData: { swapVOsToPinCreditDetails: 'Old value' },
54+
}
55+
56+
updateSessionData(req, {
57+
additionalData: { swapVOsToPinCreditDetails: 'New value' },
58+
})
59+
60+
expect(req.session.applicationData).toEqual({
61+
type: { name: 'Swap VOs', value: 'swap-vos' },
62+
additionalData: { swapVOsToPinCreditDetails: 'New value' },
63+
})
64+
})
65+
66+
it('should handle undefined session gracefully', () => {
67+
req.session = undefined
68+
69+
expect(() => updateSessionData(req, { prisonerName: 'John Doe' })).toThrow()
70+
})
71+
})

server/utils/session.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Request } from 'express'
2+
import { SessionData } from 'express-session'
3+
4+
// eslint-disable-next-line import/prefer-default-export
5+
export const updateSessionData = (req: Request, updates: Partial<SessionData['applicationData']>) => {
6+
if (!req.session.applicationData) {
7+
req.session.applicationData = {} as SessionData['applicationData']
8+
}
9+
10+
req.session.applicationData = {
11+
...req.session.applicationData,
12+
...updates,
13+
}
14+
}

0 commit comments

Comments
 (0)