@defra/lis-infra-ui-services 0.2.0 → 0.3.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.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "UI-Services shared definitions for the livestock taxonomy topology.",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -54,6 +54,7 @@
54
54
  "author": "Defra DDTS",
55
55
  "license": "OGL-UK-3.0",
56
56
  "dependencies": {
57
+ "@defra/lis-hubs-infra-registry": "^0.3.0",
57
58
  "@elastic/ecs-pino-format": "1.5.0",
58
59
  "@hapi/catbox-memory": "6.0.2",
59
60
  "@hapi/catbox-redis": "7.0.2",
package/src/base-path.js CHANGED
@@ -1,5 +1,7 @@
1
1
  /** @import { Request } from '@hapi/hapi' */
2
2
 
3
+ import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
4
+
3
5
  function normalizePath(path) {
4
6
  if (!path || path === '/') {
5
7
  return '/'
@@ -96,12 +98,14 @@ export function getAssetPaths({ basePath: _basePath = '', assetPath }) {
96
98
  }
97
99
 
98
100
  /**
99
- * @param {object} config
101
+ * @param {{ moduleId?: string, assetPath: string }} options
100
102
  * @returns {object}
101
103
  */
102
- export function createBasePathHelpersForConfig(config) {
104
+ export function createBasePathHelpersForConfig({ moduleId, assetPath }) {
105
+ const basePath = moduleId ? getBasePathForModule(moduleId) : ''
106
+
103
107
  function getBasePath() {
104
- return config.get('basePath')
108
+ return basePath
105
109
  }
106
110
 
107
111
  return {
@@ -134,7 +138,7 @@ export function createBasePathHelpersForConfig(config) {
134
138
  getAssetPaths() {
135
139
  return getAssetPaths({
136
140
  basePath: getBasePath(),
137
- assetPath: config.get('assetPath')
141
+ assetPath
138
142
  })
139
143
  }
140
144
  }
@@ -1,6 +1,6 @@
1
- import assert from 'node:assert/strict'
2
- import { test } from 'vitest'
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
3
2
 
3
+ import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
4
4
  import {
5
5
  buildAppPath,
6
6
  createBasePathHelpersForConfig,
@@ -10,111 +10,285 @@ import {
10
10
  isPrefixedRequest
11
11
  } from './base-path.js'
12
12
 
13
- test('isPrefixedRequest detects a forwarded prefix that matches the configured base path', () => {
14
- assert.equal(
15
- isPrefixedRequest({
16
- request: {
17
- path: '/about',
18
- headers: {
19
- 'x-forwarded-prefix': '/chicken/move'
20
- }
21
- },
22
- basePath: '/chicken/move'
23
- }),
24
- true
25
- )
26
- })
13
+ vi.mock('@defra/lis-hubs-infra-registry')
27
14
 
28
- test('isPrefixedRequest ignores a forwarded prefix that does not match the configured base path', () => {
29
- assert.equal(
30
- isPrefixedRequest({
31
- request: {
32
- path: '/about',
33
- headers: {
34
- 'x-forwarded-prefix': '/goat/move'
35
- }
36
- },
37
- basePath: '/chicken/move'
38
- }),
39
- false
40
- )
15
+ const mocks = {
16
+ getBasePathForModule: vi.mocked(getBasePathForModule)
17
+ }
18
+
19
+ describe('isPrefixedRequest()', () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks()
22
+ })
23
+
24
+ test('detects a forwarded prefix that matches the configured base path', () => {
25
+ // Arrange
26
+ const request = {
27
+ path: '/about',
28
+ headers: {
29
+ 'x-forwarded-prefix': '/chicken/move'
30
+ }
31
+ }
32
+
33
+ // Act
34
+ const result = isPrefixedRequest({ request, basePath: '/chicken/move' })
35
+
36
+ // Assert
37
+ expect(result).toBe(true)
38
+ })
39
+
40
+ test('ignores a forwarded prefix that does not match the configured base path', () => {
41
+ // Arrange
42
+ const request = {
43
+ path: '/about',
44
+ headers: {
45
+ 'x-forwarded-prefix': '/goat/move'
46
+ }
47
+ }
48
+
49
+ // Act
50
+ const result = isPrefixedRequest({ request, basePath: '/chicken/move' })
51
+
52
+ // Assert
53
+ expect(result).toBe(false)
54
+ })
41
55
  })
42
56
 
43
- test('getRequestBasePath preserves direct requests to the configured base path', () => {
44
- assert.equal(
45
- getRequestBasePath({
46
- request: {
47
- path: '/chicken/move/about',
48
- headers: {}
49
- },
50
- basePath: '/chicken/move'
51
- }),
52
- '/chicken/move'
53
- )
57
+ describe('getRequestBasePath()', () => {
58
+ beforeEach(() => {
59
+ vi.clearAllMocks()
60
+ })
61
+
62
+ test('preserves direct requests to the configured base path', () => {
63
+ // Arrange
64
+ const request = {
65
+ path: '/chicken/move/about',
66
+ headers: {}
67
+ }
68
+
69
+ // Act
70
+ const result = getRequestBasePath({ request, basePath: '/chicken/move' })
71
+
72
+ // Assert
73
+ expect(result).toBe('/chicken/move')
74
+ })
54
75
  })
55
76
 
56
- test('buildAppPath uses the forwarded prefix for outbound paths when present', () => {
57
- assert.equal(
58
- buildAppPath({
59
- request: {
60
- path: '/about',
61
- headers: {
62
- 'x-forwarded-prefix': '/chicken/move'
63
- }
64
- },
77
+ describe('buildAppPath()', () => {
78
+ beforeEach(() => {
79
+ vi.clearAllMocks()
80
+ })
81
+
82
+ test('uses the forwarded prefix for outbound paths when present', () => {
83
+ // Arrange
84
+ const request = {
85
+ path: '/about',
86
+ headers: {
87
+ 'x-forwarded-prefix': '/chicken/move'
88
+ }
89
+ }
90
+
91
+ // Act
92
+ const result = buildAppPath({
93
+ request,
65
94
  routePath: '/more-info',
66
95
  basePath: '/chicken/move'
67
- }),
68
- '/chicken/move/more-info'
69
- )
70
- })
96
+ })
97
+
98
+ // Assert
99
+ expect(result).toBe('/chicken/move/more-info')
100
+ })
71
101
 
72
- test('getRouteVariants only returns the internal route path', () => {
73
- assert.deepEqual(
74
- getRouteVariants({
75
- routePath: '/about',
102
+ test('returns just the base path for the root route', () => {
103
+ // Arrange
104
+ const request = {
105
+ path: '/chicken/move',
106
+ headers: {}
107
+ }
108
+
109
+ // Act
110
+ const result = buildAppPath({
111
+ request,
112
+ routePath: '/',
76
113
  basePath: '/chicken/move'
77
- }),
78
- ['/about']
79
- )
114
+ })
115
+
116
+ // Assert
117
+ expect(result).toBe('/chicken/move')
118
+ })
119
+
120
+ test('falls back to / for the root route with no base path', () => {
121
+ // Arrange
122
+ const request = {
123
+ path: '/',
124
+ headers: {}
125
+ }
126
+
127
+ // Act
128
+ const result = buildAppPath({ request, routePath: '/' })
129
+
130
+ // Assert
131
+ expect(result).toBe('/')
132
+ })
80
133
  })
81
134
 
82
- test('getAssetPaths only returns the internal asset path', () => {
83
- assert.deepEqual(
84
- getAssetPaths({
85
- basePath: '/chicken/move',
86
- assetPath: '/public'
87
- }),
88
- ['/public']
89
- )
135
+ describe('getRouteVariants()', () => {
136
+ test('only returns the internal route path', () => {
137
+ // Arrange
138
+ const routePath = '/about'
139
+
140
+ // Act
141
+ const result = getRouteVariants({ routePath, basePath: '/chicken/move' })
142
+
143
+ // Assert
144
+ expect(result).toEqual(['/about'])
145
+ })
90
146
  })
91
147
 
92
- test('createBasePathHelpersForConfig binds config for request and asset helpers', () => {
93
- const calls = []
94
- const config = {
95
- get(key) {
96
- calls.push(key)
148
+ describe('getAssetPaths()', () => {
149
+ test('only returns the internal asset path', () => {
150
+ // Arrange
151
+ const assetPath = '/public'
97
152
 
98
- if (key === 'basePath') {
99
- return '/chicken/move'
100
- }
153
+ // Act
154
+ const result = getAssetPaths({ basePath: '/chicken/move', assetPath })
101
155
 
102
- if (key === 'assetPath') {
103
- return '/public'
104
- }
156
+ // Assert
157
+ expect(result).toEqual(['/public'])
158
+ })
159
+ })
160
+
161
+ describe('createBasePathHelpersForConfig()', () => {
162
+ beforeEach(() => {
163
+ vi.clearAllMocks()
164
+ })
165
+
166
+ test('resolves the base path from the module registry when moduleId is given', () => {
167
+ // Arrange
168
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
169
+
170
+ // Act
171
+ const helpers = createBasePathHelpersForConfig({
172
+ moduleId: 'cattle-register',
173
+ assetPath: '/public'
174
+ })
175
+
176
+ // Assert
177
+ expect(mocks.getBasePathForModule).toHaveBeenCalledWith('cattle-register')
178
+ expect(helpers.getBasePath()).toBe('/cattle/register')
179
+ })
180
+
181
+ test('defaults to an empty base path when no moduleId is given', () => {
182
+ // Arrange
183
+ // Act
184
+ const helpers = createBasePathHelpersForConfig({ assetPath: '/public' })
185
+
186
+ // Assert
187
+ expect(helpers.getBasePath()).toBe('')
188
+ expect(mocks.getBasePathForModule).not.toHaveBeenCalled()
189
+ })
190
+
191
+ test('binds the resolved base path to isPrefixedRequest', () => {
192
+ // Arrange
193
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
194
+ const helpers = createBasePathHelpersForConfig({
195
+ moduleId: 'cattle-register',
196
+ assetPath: '/public'
197
+ })
198
+ const request = {
199
+ path: '/cattle/register/about',
200
+ headers: {}
105
201
  }
106
- }
107
- const helpers = createBasePathHelpersForConfig(config)
108
202
 
109
- assert.equal(
110
- helpers.getRequestBasePath({
111
- path: '/about',
112
- headers: {
113
- 'x-forwarded-prefix': '/chicken/move'
114
- }
115
- }),
116
- '/chicken/move'
117
- )
118
- assert.deepEqual(helpers.getAssetPaths(), ['/public'])
119
- assert.deepEqual(calls, ['basePath', 'basePath', 'assetPath'])
203
+ // Act
204
+ const result = helpers.isPrefixedRequest(request)
205
+
206
+ // Assert
207
+ expect(result).toBe(true)
208
+ })
209
+
210
+ test('binds the resolved base path to getRequestBasePath', () => {
211
+ // Arrange
212
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
213
+ const helpers = createBasePathHelpersForConfig({
214
+ moduleId: 'cattle-register',
215
+ assetPath: '/public'
216
+ })
217
+ const request = {
218
+ path: '/cattle/register/about',
219
+ headers: {}
220
+ }
221
+
222
+ // Act
223
+ const result = helpers.getRequestBasePath(request)
224
+
225
+ // Assert
226
+ expect(result).toBe('/cattle/register')
227
+ })
228
+
229
+ test('binds the resolved base path to buildAppPath', () => {
230
+ // Arrange
231
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
232
+ const helpers = createBasePathHelpersForConfig({
233
+ moduleId: 'cattle-register',
234
+ assetPath: '/public'
235
+ })
236
+ const request = {
237
+ path: '/cattle/register/about',
238
+ headers: {}
239
+ }
240
+
241
+ // Act
242
+ const result = helpers.buildAppPath(request, '/more-info')
243
+
244
+ // Assert
245
+ expect(result).toBe('/cattle/register/more-info')
246
+ })
247
+
248
+ test('binds the resolved base path to getRouteVariants', () => {
249
+ // Arrange
250
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
251
+ const helpers = createBasePathHelpersForConfig({
252
+ moduleId: 'cattle-register',
253
+ assetPath: '/public'
254
+ })
255
+
256
+ // Act
257
+ const result = helpers.getRouteVariants('/about')
258
+
259
+ // Assert
260
+ expect(result).toEqual(['/about'])
261
+ })
262
+
263
+ test('defaults buildAppPath and getRouteVariants to the root route path', () => {
264
+ // Arrange
265
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
266
+ const helpers = createBasePathHelpersForConfig({
267
+ moduleId: 'cattle-register',
268
+ assetPath: '/public'
269
+ })
270
+ const request = {
271
+ path: '/cattle/register',
272
+ headers: {}
273
+ }
274
+
275
+ // Act
276
+ const appPath = helpers.buildAppPath(request)
277
+ const routeVariants = helpers.getRouteVariants()
278
+
279
+ // Assert
280
+ expect(appPath).toBe('/cattle/register')
281
+ expect(routeVariants).toEqual(['/'])
282
+ })
283
+
284
+ test('binds the configured asset path to getAssetPaths', () => {
285
+ // Arrange
286
+ const helpers = createBasePathHelpersForConfig({ assetPath: '/public' })
287
+
288
+ // Act
289
+ const result = helpers.getAssetPaths()
290
+
291
+ // Assert
292
+ expect(result).toEqual(['/public'])
293
+ })
120
294
  })
@@ -5,6 +5,8 @@ import hapiVision from '@hapi/vision'
5
5
  import { createRequire } from 'node:module'
6
6
  import { fileURLToPath } from 'node:url'
7
7
 
8
+ import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
9
+
8
10
  import { createNunjucksContextBuilder } from './context.js'
9
11
  import { buildPrimaryNavigation } from './build-navigation.js'
10
12
  import * as filters from './filters.js'
@@ -155,10 +157,16 @@ function buildNunjucksEnvironment({
155
157
  }
156
158
 
157
159
  /**
158
- * @param {{ config: object, logger: object, getRequestBasePath: Function }} options
160
+ * @param {{ config: object, logger: object, getRequestBasePath: Function, moduleId?: string }} options
159
161
  * @returns {object}
160
162
  */
161
- export function createNunjucksConfig({ config, logger, getRequestBasePath }) {
163
+ export function createNunjucksConfig({
164
+ config,
165
+ logger,
166
+ getRequestBasePath,
167
+ moduleId
168
+ }) {
169
+ const basePath = moduleId ? getBasePathForModule(moduleId) : ''
162
170
  const projectRoot = config.get('root')
163
171
  const infrastructureRoot = path.resolve(
164
172
  path.dirname(fileURLToPath(import.meta.url)),
@@ -178,7 +186,7 @@ export function createNunjucksConfig({ config, logger, getRequestBasePath }) {
178
186
  const buildNavigation = (request) =>
179
187
  buildPrimaryNavigation({
180
188
  request,
181
- basePath: config.get('basePath'),
189
+ basePath,
182
190
  hubOrigin: request?.app?.hubOrigin ?? ''
183
191
  })
184
192
 
@@ -0,0 +1,84 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2
+
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import path from 'node:path'
6
+ import nunjucks from 'nunjucks'
7
+ import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
8
+ import { createNunjucksConfig } from './plugin.js'
9
+
10
+ vi.mock('nunjucks')
11
+ vi.mock('@defra/lis-hubs-infra-registry')
12
+
13
+ const mocks = {
14
+ nunjucksConfigure: vi.mocked(nunjucks.configure),
15
+ getBasePathForModule: vi.mocked(getBasePathForModule)
16
+ }
17
+
18
+ function createProjectRoot() {
19
+ const root = mkdtempSync(path.join(tmpdir(), 'ui-services-nunjucks-'))
20
+
21
+ writeFileSync(
22
+ path.join(root, 'package.json'),
23
+ JSON.stringify({ dependencies: {} })
24
+ )
25
+ mkdirSync(path.join(root, 'node_modules/govuk-frontend'), {
26
+ recursive: true
27
+ })
28
+ writeFileSync(
29
+ path.join(root, 'node_modules/govuk-frontend/package.json'),
30
+ '{}'
31
+ )
32
+
33
+ return root
34
+ }
35
+
36
+ describe('createNunjucksConfig()', () => {
37
+ let projectRoot
38
+
39
+ beforeEach(() => {
40
+ vi.clearAllMocks()
41
+ projectRoot = createProjectRoot()
42
+ mocks.nunjucksConfigure.mockReturnValue({
43
+ addGlobal: vi.fn(),
44
+ addFilter: vi.fn()
45
+ })
46
+ })
47
+
48
+ afterEach(() => {
49
+ rmSync(projectRoot, { recursive: true, force: true })
50
+ })
51
+
52
+ test('resolves the base path from the module registry when moduleId is given', () => {
53
+ // Arrange
54
+ mocks.getBasePathForModule.mockReturnValue('/cattle/register')
55
+ const config = { get: vi.fn((key) => (key === 'root' ? projectRoot : false)) }
56
+
57
+ // Act
58
+ createNunjucksConfig({
59
+ config,
60
+ logger: {},
61
+ getRequestBasePath: vi.fn(),
62
+ moduleId: 'cattle-register'
63
+ })
64
+
65
+ // Assert
66
+ expect(mocks.getBasePathForModule).toHaveBeenCalledWith('cattle-register')
67
+ })
68
+
69
+ test('defaults to an empty base path when no moduleId is given', () => {
70
+ // Arrange
71
+ const config = { get: vi.fn((key) => (key === 'root' ? projectRoot : false)) }
72
+
73
+ // Act
74
+ const nunjucksConfig = createNunjucksConfig({
75
+ config,
76
+ logger: {},
77
+ getRequestBasePath: vi.fn()
78
+ })
79
+
80
+ // Assert
81
+ expect(mocks.getBasePathForModule).not.toHaveBeenCalled()
82
+ expect(nunjucksConfig.plugin).toBeDefined()
83
+ })
84
+ })