@defra/lis-infra-ui-services 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/lis-infra-ui-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "UI-Services shared definitions for the livestock taxonomy topology.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./nunjucks-context": "./src/nunjucks-context.js",
|
|
31
31
|
"./proxy/setup-proxy": "./src/proxy/setup-proxy.js",
|
|
32
32
|
"./redis-client": "./src/redis-client.js",
|
|
33
|
+
"./referrer-link": "./src/referrer-link.js",
|
|
33
34
|
"./session-cache": "./src/session-cache.js",
|
|
34
35
|
"./session-cache/cache-engine": "./src/session-cache/cache-engine.js",
|
|
35
36
|
"./static-files": "./src/static-files.js",
|
package/src/duration.js
CHANGED
|
@@ -17,6 +17,7 @@ export const milliseconds = {
|
|
|
17
17
|
tenSeconds: TEN * MILLIS_IN_SECOND,
|
|
18
18
|
thirtySeconds: THIRTY * MILLIS_IN_SECOND,
|
|
19
19
|
fiveMinutes: FIVE * SECONDS_IN_MINUTE * MILLIS_IN_SECOND,
|
|
20
|
+
oneHour: MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MILLIS_IN_SECOND,
|
|
20
21
|
fourHours: FOUR * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MILLIS_IN_SECOND,
|
|
21
22
|
oneDay: HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MILLIS_IN_SECOND,
|
|
22
23
|
oneWeek:
|
|
@@ -55,6 +55,28 @@ describe('searchForm()', () => {
|
|
|
55
55
|
expect(html).not.toContain('aria-describedby')
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
+
test('carries hidden fields inside the form so they survive a submit', () => {
|
|
59
|
+
// Act
|
|
60
|
+
const html = render({
|
|
61
|
+
label: 'Search',
|
|
62
|
+
hiddenFields: [{ name: 'referrer', value: 'abc-123' }]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(html).toContain(
|
|
67
|
+
'<input type="hidden" name="referrer" value="abc-123">'
|
|
68
|
+
)
|
|
69
|
+
expect(html.indexOf('<form')).toBeLessThan(html.indexOf('type="hidden"'))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('renders no hidden fields when none are given', () => {
|
|
73
|
+
// Act
|
|
74
|
+
const html = render({ label: 'Search' })
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(html).not.toContain('type="hidden"')
|
|
78
|
+
})
|
|
79
|
+
|
|
58
80
|
test('defaults the input name and id to "search"', () => {
|
|
59
81
|
// Act
|
|
60
82
|
const html = render({ label: 'Search' })
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
{%- endif -%}
|
|
13
13
|
|
|
14
14
|
<form method="GET" class="lis-search-form">
|
|
15
|
+
{%- for field in params.hiddenFields -%}
|
|
16
|
+
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
|
|
17
|
+
{%- endfor -%}
|
|
15
18
|
<div class="lis-search-form__wrapper">
|
|
16
19
|
<input
|
|
17
20
|
class="govuk-input lis-search-form__input"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** @import { Request, Server } from '@hapi/hapi' */
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
import { milliseconds } from './duration.js'
|
|
5
|
+
|
|
6
|
+
const idPattern =
|
|
7
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The browser sends the hub's public URL as the referrer, and the hub proxy
|
|
11
|
+
* passes the host it was called on as x-forwarded-host, so the two are
|
|
12
|
+
* compared to keep a link built from the referrer on the current domain.
|
|
13
|
+
*
|
|
14
|
+
* @param {Request} request
|
|
15
|
+
* @returns {string | undefined} the referrer's path and query, when it is on the same host as the request
|
|
16
|
+
*/
|
|
17
|
+
export function getSameHostReferrerPath(request) {
|
|
18
|
+
const { referer } = request.headers
|
|
19
|
+
if (!referer) {
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let refererUrl
|
|
24
|
+
try {
|
|
25
|
+
refererUrl = new URL(referer)
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const requestHost = (
|
|
31
|
+
request.headers['x-forwarded-host'] ??
|
|
32
|
+
request.headers.host ??
|
|
33
|
+
''
|
|
34
|
+
)
|
|
35
|
+
.split(',')[0]
|
|
36
|
+
.trim()
|
|
37
|
+
|
|
38
|
+
// A path starting with // would make the link protocol-relative, i.e.
|
|
39
|
+
// another site.
|
|
40
|
+
if (refererUrl.host !== requestHost || refererUrl.pathname.startsWith('//')) {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `${refererUrl.pathname}${refererUrl.search}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} href
|
|
49
|
+
* @param {string} queryParam
|
|
50
|
+
* @param {string | null} id
|
|
51
|
+
* @returns {string} href with the referrer id appended, or unchanged when there is no id
|
|
52
|
+
*/
|
|
53
|
+
function appendReferrerId(href, queryParam, id) {
|
|
54
|
+
if (!id) {
|
|
55
|
+
return href
|
|
56
|
+
}
|
|
57
|
+
const separator = href.includes('?') ? '&' : '?'
|
|
58
|
+
return `${href}${separator}${queryParam}=${id}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Request} request
|
|
63
|
+
* @returns {string | null} the signed-in user's subject, which keys what is remembered for them
|
|
64
|
+
*/
|
|
65
|
+
function userKey(request) {
|
|
66
|
+
const sub = request.auth?.credentials?.user?.sub
|
|
67
|
+
return typeof sub === 'string' && sub.length > 0 ? sub : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remembers where a user came from so pages reached from a list can link
|
|
72
|
+
* back to it, including its query, after moving between related pages.
|
|
73
|
+
*
|
|
74
|
+
* On a request whose same-host referrer the caller allows, the referrer path
|
|
75
|
+
* is stored in the server cache under a new id, keyed with the user, and the
|
|
76
|
+
* page links to its related pages carrying `?<queryParam>=<id>`. A request
|
|
77
|
+
* carrying an id resolves it from the cache, for the same user only.
|
|
78
|
+
*
|
|
79
|
+
* @typedef {object} ReferrerLink
|
|
80
|
+
* @property {string | null} id the id to carry on links to related pages, or null when there is nothing to carry
|
|
81
|
+
* @property {string} href the back link: the remembered referrer, or the fallback
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Server} server
|
|
86
|
+
* @param {object} options
|
|
87
|
+
* @param {string} options.cacheName a cache provisioned on the server
|
|
88
|
+
* @param {(path: string) => boolean} options.allow whether a same-host referrer path is one worth remembering
|
|
89
|
+
* @param {string} options.fallbackHref the back link when nothing is remembered
|
|
90
|
+
* @param {(request: Request) => boolean} [options.when] whether the request gets a back link at all; defaults to always
|
|
91
|
+
* @param {string} [options.queryParam] query parameter carrying the id; defaults to "referrer"
|
|
92
|
+
* @param {string} [options.segment] cache segment; defaults to "referrer-link"
|
|
93
|
+
* @param {number} [options.expiresIn] how long a remembered referrer lasts, in milliseconds; defaults to one hour
|
|
94
|
+
* @returns {{ resolve: (request: Request) => Promise<ReferrerLink | null>, link: (href: string, id: string | null) => string }}
|
|
95
|
+
*/
|
|
96
|
+
export function createReferrerLink(
|
|
97
|
+
server,
|
|
98
|
+
{
|
|
99
|
+
cacheName,
|
|
100
|
+
allow,
|
|
101
|
+
fallbackHref,
|
|
102
|
+
when = () => true,
|
|
103
|
+
queryParam = 'referrer',
|
|
104
|
+
segment = 'referrer-link',
|
|
105
|
+
expiresIn = milliseconds.oneHour
|
|
106
|
+
}
|
|
107
|
+
) {
|
|
108
|
+
if (typeof cacheName !== 'string' || cacheName.length === 0) {
|
|
109
|
+
throw new TypeError('createReferrerLink() requires a cacheName')
|
|
110
|
+
}
|
|
111
|
+
if (typeof allow !== 'function') {
|
|
112
|
+
throw new TypeError('createReferrerLink() requires an allow function')
|
|
113
|
+
}
|
|
114
|
+
if (typeof fallbackHref !== 'string' || fallbackHref.length === 0) {
|
|
115
|
+
throw new TypeError('createReferrerLink() requires a fallbackHref')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const store = server.cache({ cache: cacheName, segment, expiresIn })
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A route pre method: resolves the back link for the request. Null when
|
|
122
|
+
* `when` rules the request out, so the page shows no back link.
|
|
123
|
+
* @param {Request} request
|
|
124
|
+
* @returns {Promise<ReferrerLink | null>}
|
|
125
|
+
*/
|
|
126
|
+
async function resolve(request) {
|
|
127
|
+
if (!when(request)) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const user = userKey(request)
|
|
132
|
+
const requestedId = request.query?.[queryParam]
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
user &&
|
|
136
|
+
typeof requestedId === 'string' &&
|
|
137
|
+
idPattern.test(requestedId)
|
|
138
|
+
) {
|
|
139
|
+
const remembered = await store.get(`${user}:${requestedId}`)
|
|
140
|
+
if (typeof remembered === 'string') {
|
|
141
|
+
return { id: requestedId, href: remembered }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const referrerPath = getSameHostReferrerPath(request)
|
|
146
|
+
if (user && referrerPath && allow(referrerPath)) {
|
|
147
|
+
const id = randomUUID()
|
|
148
|
+
await store.set(`${user}:${id}`, referrerPath)
|
|
149
|
+
return { id, href: referrerPath }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { id: null, href: fallbackHref }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
resolve,
|
|
157
|
+
link: (href, id) => appendReferrerId(href, queryParam, id)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createReferrerLink, getSameHostReferrerPath } from './referrer-link.js'
|
|
4
|
+
|
|
5
|
+
const uuidPattern =
|
|
6
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
7
|
+
|
|
8
|
+
const mocks = {
|
|
9
|
+
get: vi.fn(),
|
|
10
|
+
set: vi.fn(),
|
|
11
|
+
cache: vi.fn()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const server = { cache: mocks.cache }
|
|
15
|
+
|
|
16
|
+
function createRequest({ query = {}, headers = {}, sub = 'user-1' } = {}) {
|
|
17
|
+
return {
|
|
18
|
+
query,
|
|
19
|
+
headers: { 'x-forwarded-host': 'back-office.lis.defra', ...headers },
|
|
20
|
+
auth: { credentials: { user: sub ? { sub } : undefined } }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('getSameHostReferrerPath()', () => {
|
|
25
|
+
test('it returns the path and query of a referrer on the forwarded host', () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const request = {
|
|
28
|
+
headers: {
|
|
29
|
+
referer: 'https://back-office.lis.defra/holdings?search=oak&page=2',
|
|
30
|
+
'x-forwarded-host': 'back-office.lis.defra',
|
|
31
|
+
host: 'localhost:3221'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const result = getSameHostReferrerPath(request)
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(result).toBe('/holdings?search=oak&page=2')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('it ignores a referrer on another host', () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const request = {
|
|
45
|
+
headers: {
|
|
46
|
+
referer: 'https://evil.example/holdings',
|
|
47
|
+
'x-forwarded-host': 'back-office.lis.defra'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
const result = getSameHostReferrerPath(request)
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
expect(result).toBeUndefined()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('it ignores a referrer path that would point at another site', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const request = {
|
|
61
|
+
headers: {
|
|
62
|
+
referer: 'https://back-office.lis.defra//evil.example',
|
|
63
|
+
'x-forwarded-host': 'back-office.lis.defra'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
const result = getSameHostReferrerPath(request)
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(result).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('it returns undefined when there is no referrer or it is not a URL', () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const noReferrer = { headers: { host: 'back-office.lis.defra' } }
|
|
77
|
+
const badReferrer = {
|
|
78
|
+
headers: { referer: 'not a url', host: 'back-office.lis.defra' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const first = getSameHostReferrerPath(noReferrer)
|
|
83
|
+
const second = getSameHostReferrerPath(badReferrer)
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
expect(first).toBeUndefined()
|
|
87
|
+
expect(second).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('createReferrerLink()', () => {
|
|
92
|
+
beforeAll(() => {
|
|
93
|
+
mocks.cache.mockReturnValue({ get: mocks.get, set: mocks.set })
|
|
94
|
+
mocks.get.mockResolvedValue(null)
|
|
95
|
+
mocks.set.mockResolvedValue(undefined)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
vi.clearAllMocks()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
function createSubject(overrides = {}) {
|
|
103
|
+
return createReferrerLink(server, {
|
|
104
|
+
cacheName: 'test-cache',
|
|
105
|
+
allow: (path) => path === '/holdings' || path.startsWith('/holdings?'),
|
|
106
|
+
fallbackHref: '/holdings',
|
|
107
|
+
...overrides
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test('it provisions a cache segment with the given name and expiry', () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const expiresIn = 1234
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
createSubject({ segment: 'list-referrer', expiresIn })
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
expect(mocks.cache).toHaveBeenCalledWith({
|
|
120
|
+
cache: 'test-cache',
|
|
121
|
+
segment: 'list-referrer',
|
|
122
|
+
expiresIn
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('it throws when the cache name, allow rule or fallback is missing', () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const incomplete = [
|
|
129
|
+
{ allow: () => true, fallbackHref: '/x' },
|
|
130
|
+
{ cacheName: 'c', fallbackHref: '/x' },
|
|
131
|
+
{ cacheName: 'c', allow: () => true }
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
const errors = incomplete.map((options) => {
|
|
136
|
+
try {
|
|
137
|
+
createReferrerLink(server, options)
|
|
138
|
+
return undefined
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return e
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
expect(errors.map((e) => e?.message)).toEqual([
|
|
146
|
+
'createReferrerLink() requires a cacheName',
|
|
147
|
+
'createReferrerLink() requires an allow function',
|
|
148
|
+
'createReferrerLink() requires a fallbackHref'
|
|
149
|
+
])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('it remembers an allowed same-host referrer under a new id for the user', async () => {
|
|
153
|
+
// Arrange
|
|
154
|
+
const subject = createSubject()
|
|
155
|
+
const request = createRequest({
|
|
156
|
+
headers: {
|
|
157
|
+
referer: 'https://back-office.lis.defra/holdings?search=oak&page=2'
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Act
|
|
162
|
+
const result = await subject.resolve(request)
|
|
163
|
+
|
|
164
|
+
// Assert
|
|
165
|
+
expect(result.href).toBe('/holdings?search=oak&page=2')
|
|
166
|
+
expect(result.id).toMatch(uuidPattern)
|
|
167
|
+
expect(mocks.set).toHaveBeenCalledWith(
|
|
168
|
+
`user-1:${result.id}`,
|
|
169
|
+
'/holdings?search=oak&page=2'
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('it resolves a carried id from the cache for the same user', async () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const subject = createSubject()
|
|
176
|
+
const id = '0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f'
|
|
177
|
+
mocks.get.mockResolvedValueOnce('/holdings?search=oak')
|
|
178
|
+
const request = createRequest({ query: { referrer: id } })
|
|
179
|
+
|
|
180
|
+
// Act
|
|
181
|
+
const result = await subject.resolve(request)
|
|
182
|
+
|
|
183
|
+
// Assert
|
|
184
|
+
expect(mocks.get).toHaveBeenCalledWith(`user-1:${id}`)
|
|
185
|
+
expect(result).toEqual({ id, href: '/holdings?search=oak' })
|
|
186
|
+
expect(mocks.set).not.toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('it falls back when a carried id is unknown, expired or not a uuid', async () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const subject = createSubject()
|
|
192
|
+
const unknown = createRequest({
|
|
193
|
+
query: { referrer: '0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f' }
|
|
194
|
+
})
|
|
195
|
+
const malformed = createRequest({ query: { referrer: '../etc' } })
|
|
196
|
+
|
|
197
|
+
// Act
|
|
198
|
+
const first = await subject.resolve(unknown)
|
|
199
|
+
const second = await subject.resolve(malformed)
|
|
200
|
+
|
|
201
|
+
// Assert
|
|
202
|
+
expect(first).toEqual({ id: null, href: '/holdings' })
|
|
203
|
+
expect(second).toEqual({ id: null, href: '/holdings' })
|
|
204
|
+
expect(mocks.get).toHaveBeenCalledTimes(1)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('it falls back when the referrer is not allowed, off-host or missing', async () => {
|
|
208
|
+
// Arrange
|
|
209
|
+
const subject = createSubject()
|
|
210
|
+
const notAllowed = createRequest({
|
|
211
|
+
headers: { referer: 'https://back-office.lis.defra/cattle/holdings/1' }
|
|
212
|
+
})
|
|
213
|
+
const offHost = createRequest({
|
|
214
|
+
headers: { referer: 'https://evil.example/holdings' }
|
|
215
|
+
})
|
|
216
|
+
const missing = createRequest()
|
|
217
|
+
|
|
218
|
+
// Act
|
|
219
|
+
const results = await Promise.all(
|
|
220
|
+
[notAllowed, offHost, missing].map((request) => subject.resolve(request))
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Assert
|
|
224
|
+
expect(results).toEqual([
|
|
225
|
+
{ id: null, href: '/holdings' },
|
|
226
|
+
{ id: null, href: '/holdings' },
|
|
227
|
+
{ id: null, href: '/holdings' }
|
|
228
|
+
])
|
|
229
|
+
expect(mocks.set).not.toHaveBeenCalled()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('it never remembers or resolves anything for a request without a user', async () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
const subject = createSubject()
|
|
235
|
+
const request = createRequest({
|
|
236
|
+
sub: null,
|
|
237
|
+
query: { referrer: '0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f' },
|
|
238
|
+
headers: { referer: 'https://back-office.lis.defra/holdings' }
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Act
|
|
242
|
+
const result = await subject.resolve(request)
|
|
243
|
+
|
|
244
|
+
// Assert
|
|
245
|
+
expect(result).toEqual({ id: null, href: '/holdings' })
|
|
246
|
+
expect(mocks.get).not.toHaveBeenCalled()
|
|
247
|
+
expect(mocks.set).not.toHaveBeenCalled()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('it returns null when the request is ruled out by "when"', async () => {
|
|
251
|
+
// Arrange
|
|
252
|
+
const subject = createSubject({ when: () => false })
|
|
253
|
+
const request = createRequest({
|
|
254
|
+
headers: { referer: 'https://back-office.lis.defra/holdings' }
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Act
|
|
258
|
+
const result = await subject.resolve(request)
|
|
259
|
+
|
|
260
|
+
// Assert
|
|
261
|
+
expect(result).toBeNull()
|
|
262
|
+
expect(mocks.set).not.toHaveBeenCalled()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('it appends the id to a link, after any existing query', () => {
|
|
266
|
+
// Arrange
|
|
267
|
+
const subject = createSubject({ queryParam: 'from' })
|
|
268
|
+
const id = '0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f'
|
|
269
|
+
|
|
270
|
+
// Act
|
|
271
|
+
const plain = subject.link('/cattle/holdings/22/001/0001', id)
|
|
272
|
+
const withQuery = subject.link('/cattle/holdings/x/animals?page=2', id)
|
|
273
|
+
const noId = subject.link('/cattle/holdings/22/001/0001', null)
|
|
274
|
+
|
|
275
|
+
// Assert
|
|
276
|
+
expect(plain).toBe(`/cattle/holdings/22/001/0001?from=${id}`)
|
|
277
|
+
expect(withQuery).toBe(`/cattle/holdings/x/animals?page=2&from=${id}`)
|
|
278
|
+
expect(noId).toBe('/cattle/holdings/22/001/0001')
|
|
279
|
+
})
|
|
280
|
+
})
|