@defra/forms-engine-plugin 4.0.0 → 4.0.2

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.
Files changed (108) hide show
  1. package/.public/stylesheets/application.min.css +2 -2
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/shared.scss +15 -0
  4. package/.server/config/index.d.ts +1 -0
  5. package/.server/config/index.js +7 -0
  6. package/.server/config/index.js.map +1 -1
  7. package/.server/index.js +6 -2
  8. package/.server/index.js.map +1 -1
  9. package/.server/server/constants.d.ts +2 -0
  10. package/.server/server/constants.js +2 -0
  11. package/.server/server/constants.js.map +1 -1
  12. package/.server/server/forms/components.json +7 -0
  13. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
  14. package/.server/server/plugins/engine/components/UkAddressField.d.ts +15 -9
  15. package/.server/server/plugins/engine/components/UkAddressField.js +67 -6
  16. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  17. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  18. package/.server/server/plugins/engine/configureEnginePlugin.js +6 -3
  19. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  20. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
  21. package/.server/server/plugins/engine/models/FormModel.js +3 -1
  22. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  23. package/.server/server/plugins/engine/options.js +2 -1
  24. package/.server/server/plugins/engine/options.js.map +1 -1
  25. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -0
  26. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +46 -3
  27. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  28. package/.server/server/plugins/engine/plugin.js +13 -1
  29. package/.server/server/plugins/engine/plugin.js.map +1 -1
  30. package/.server/server/plugins/engine/routes/index.js +41 -3
  31. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  32. package/.server/server/plugins/engine/types.d.ts +19 -1
  33. package/.server/server/plugins/engine/types.js.map +1 -1
  34. package/.server/server/plugins/engine/validationHelpers.d.ts +15 -0
  35. package/.server/server/plugins/engine/validationHelpers.js +29 -0
  36. package/.server/server/plugins/engine/validationHelpers.js.map +1 -0
  37. package/.server/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  38. package/.server/server/plugins/engine/vision.js +3 -1
  39. package/.server/server/plugins/engine/vision.js.map +1 -1
  40. package/.server/server/plugins/postcode-lookup/index.d.ts +8 -0
  41. package/.server/server/plugins/postcode-lookup/index.js +21 -0
  42. package/.server/server/plugins/postcode-lookup/index.js.map +1 -0
  43. package/.server/server/plugins/postcode-lookup/models/index.d.ts +255 -0
  44. package/.server/server/plugins/postcode-lookup/models/index.js +517 -0
  45. package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -0
  46. package/.server/server/plugins/postcode-lookup/routes/index.d.ts +19 -0
  47. package/.server/server/plugins/postcode-lookup/routes/index.js +267 -0
  48. package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -0
  49. package/.server/server/plugins/postcode-lookup/service.d.ts +26 -0
  50. package/.server/server/plugins/postcode-lookup/service.js +148 -0
  51. package/.server/server/plugins/postcode-lookup/service.js.map +1 -0
  52. package/.server/server/plugins/postcode-lookup/service.test.js +144 -0
  53. package/.server/server/plugins/postcode-lookup/service.test.js.map +1 -0
  54. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.d.ts +282 -0
  55. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js +370 -0
  56. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js.map +1 -0
  57. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.d.ts +131 -0
  58. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js +195 -0
  59. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js.map +1 -0
  60. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.d.ts +51 -0
  61. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js +52 -0
  62. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js.map +1 -0
  63. package/.server/server/plugins/postcode-lookup/types.d.ts +204 -0
  64. package/.server/server/plugins/postcode-lookup/types.js +144 -0
  65. package/.server/server/plugins/postcode-lookup/types.js.map +1 -0
  66. package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  67. package/.server/server/routes/types.d.ts +6 -1
  68. package/.server/server/routes/types.js +6 -0
  69. package/.server/server/routes/types.js.map +1 -1
  70. package/.server/server/schemas/index.js +1 -1
  71. package/.server/server/schemas/index.js.map +1 -1
  72. package/.server/server/types.d.ts +1 -0
  73. package/.server/server/types.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/client/stylesheets/shared.scss +15 -0
  76. package/src/config/index.ts +9 -1
  77. package/src/index.ts +5 -4
  78. package/src/server/constants.js +2 -0
  79. package/src/server/forms/components.json +7 -0
  80. package/src/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
  81. package/src/server/plugins/engine/components/UkAddressField.test.ts +50 -27
  82. package/src/server/plugins/engine/components/UkAddressField.ts +91 -8
  83. package/src/server/plugins/engine/configureEnginePlugin.ts +5 -3
  84. package/src/server/plugins/engine/models/FormModel.ts +10 -2
  85. package/src/server/plugins/engine/options.js +2 -1
  86. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1 -0
  87. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +69 -1
  88. package/src/server/plugins/engine/plugin.ts +13 -1
  89. package/src/server/plugins/engine/routes/index.test.ts +1 -0
  90. package/src/server/plugins/engine/routes/index.ts +71 -3
  91. package/src/server/plugins/engine/types.ts +21 -1
  92. package/src/server/plugins/engine/validationHelpers.ts +48 -0
  93. package/src/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  94. package/src/server/plugins/engine/vision.ts +6 -0
  95. package/src/server/plugins/postcode-lookup/index.js +21 -0
  96. package/src/server/plugins/postcode-lookup/models/index.js +549 -0
  97. package/src/server/plugins/postcode-lookup/routes/index.js +258 -0
  98. package/src/server/plugins/postcode-lookup/service.js +188 -0
  99. package/src/server/plugins/postcode-lookup/service.test.js +177 -0
  100. package/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js +382 -0
  101. package/src/server/plugins/postcode-lookup/test/__stubs__/query.js +200 -0
  102. package/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js +53 -0
  103. package/src/server/plugins/postcode-lookup/types.js +143 -0
  104. package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  105. package/src/server/postcode-lookup.test.ts +64 -0
  106. package/src/server/routes/types.ts +7 -1
  107. package/src/server/schemas/index.ts +5 -7
  108. package/src/server/types.ts +1 -0
@@ -0,0 +1,258 @@
1
+ import Boom from '@hapi/boom'
2
+ import { StatusCodes } from 'http-status-codes'
3
+ import Joi from 'joi'
4
+
5
+ import { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'
6
+ import {
7
+ JOURNEY_BASE_URL,
8
+ detailsPayloadSchema,
9
+ detailsViewModel,
10
+ manualPayloadSchema,
11
+ manualViewModel,
12
+ selectPayloadSchema,
13
+ selectViewModel,
14
+ stepSchema,
15
+ steps
16
+ } from '~/src/server/plugins/postcode-lookup/models/index.js'
17
+ import * as service from '~/src/server/plugins/postcode-lookup/service.js'
18
+
19
+ const viewName = 'postcode-lookup-details'
20
+
21
+ /**
22
+ * Get the session state associated with this journey
23
+ * @param {PostcodeLookupRequest} request
24
+ */
25
+ function getSessionState(request) {
26
+ /**
27
+ * @type {PostcodeLookupSessionData | undefined}
28
+ */
29
+ const state = request.yar.get(JOURNEY_BASE_URL)
30
+
31
+ if (!state) {
32
+ throw Boom.internal(`No postcode lookup data found for ${JOURNEY_BASE_URL}`)
33
+ }
34
+
35
+ return state
36
+ }
37
+
38
+ /**
39
+ * Flash form component state
40
+ * @param {PostcodeLookupRequest} request - the request
41
+ * @param {string} componentName - the component name
42
+ * @param {Address | PostcodeLookupManualPayload} address - the address from ordnance survey or manually entered
43
+ */
44
+ function flashComponentState(request, componentName, address) {
45
+ const addressState = {
46
+ addressLine1: address.addressLine1,
47
+ addressLine2: address.addressLine2,
48
+ town: address.town,
49
+ county: address.county,
50
+ postcode: address.postcode,
51
+ uprn: 'uprn' in address && address.uprn ? address.uprn : undefined
52
+ }
53
+
54
+ /**
55
+ * @type {ExternalStateAppendage}
56
+ */
57
+ const appendage = {
58
+ component: componentName,
59
+ data: addressState
60
+ }
61
+
62
+ request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)
63
+ }
64
+
65
+ /**
66
+ * Initialises and dispatches the request to the postcode lookup journey
67
+ * @param {FormRequestPayload} request - the source page
68
+ * @param {FormResponseToolkit} h - the source page
69
+ * @param {PostcodeLookupDispatchData} initial - the source data
70
+ */
71
+ export function dispatch(request, h, initial) {
72
+ /**
73
+ * @type {PostcodeLookupSessionData}
74
+ */
75
+ const data = {
76
+ initial,
77
+ details: { postcodeQuery: '', buildingNameQuery: '' }
78
+ }
79
+
80
+ request.yar.set(JOURNEY_BASE_URL, data)
81
+
82
+ const query = initial.step ? `?step=${initial.step}` : ''
83
+
84
+ return h.redirect(`${JOURNEY_BASE_URL}${query}`).code(StatusCodes.SEE_OTHER)
85
+ }
86
+
87
+ /**
88
+ * Gets the postcode lookup routes
89
+ * @param {PostcodeLookupConfiguration} options - ordnance survey api key
90
+ */
91
+ export function getRoutes(options) {
92
+ return [getRoute(), postRoute(options)]
93
+ }
94
+
95
+ /**
96
+ * @returns {ServerRoute<PostcodeLookupGetRequestRefs>}
97
+ */
98
+ function getRoute() {
99
+ return {
100
+ method: 'GET',
101
+ path: JOURNEY_BASE_URL,
102
+ handler(request, h) {
103
+ const { query } = request
104
+ const { step } = query
105
+ const session = getSessionState(request)
106
+
107
+ const model =
108
+ step === steps.manual
109
+ ? manualViewModel(session)
110
+ : detailsViewModel(session)
111
+
112
+ return h.view(viewName, model)
113
+ },
114
+ options: {
115
+ validate: {
116
+ query: Joi.object()
117
+ .keys({
118
+ step: Joi.string().allow(steps.details, steps.manual).optional()
119
+ })
120
+ .optional()
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {PostcodeLookupConfiguration} options
128
+ * @returns {ServerRoute<PostcodeLookupPostRequestRefs>}
129
+ */
130
+ function postRoute(options) {
131
+ return {
132
+ method: 'POST',
133
+ path: JOURNEY_BASE_URL,
134
+ async handler(request, h) {
135
+ const { payload } = request
136
+ const { step } = payload
137
+
138
+ switch (step) {
139
+ case steps.details: {
140
+ return detailsPostHandler(request, h, options)
141
+ }
142
+ case steps.select: {
143
+ return selectPostHandler(request, h, options)
144
+ }
145
+ case steps.manual: {
146
+ return manualPostHandler(request, h)
147
+ }
148
+ default:
149
+ throw Boom.badRequest(`Invalid step ${step}`)
150
+ }
151
+ },
152
+ options: {
153
+ validate: {
154
+ payload: Joi.object()
155
+ .keys({
156
+ step: stepSchema
157
+ })
158
+ .unknown(true)
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Post handler for the details step
166
+ * @param {PostcodeLookupPostRequest} request
167
+ * @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
168
+ * @param {PostcodeLookupConfiguration} options
169
+ */
170
+ async function detailsPostHandler(request, h, options) {
171
+ const { payload } = request
172
+ const session = getSessionState(request)
173
+ const { ordnanceSurveyApiKey: apiKey } = options
174
+ const { value: details, error } = detailsPayloadSchema.validate(payload)
175
+
176
+ let model
177
+
178
+ if (error) {
179
+ model = detailsViewModel(session, details, error)
180
+
181
+ return h.view(viewName, model)
182
+ }
183
+
184
+ const { postcodeQuery, buildingNameQuery } = details
185
+ session.details = { postcodeQuery, buildingNameQuery }
186
+
187
+ // Store the updated session
188
+ request.yar.set(JOURNEY_BASE_URL, session)
189
+
190
+ model = await selectViewModel({ session, apiKey })
191
+
192
+ return h.view(viewName, model)
193
+ }
194
+
195
+ /**
196
+ * Post handler for the select step
197
+ * @param {PostcodeLookupPostRequest} request
198
+ * @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
199
+ * @param {PostcodeLookupConfiguration} options
200
+ */
201
+ async function selectPostHandler(request, h, options) {
202
+ const { payload } = request
203
+ const session = getSessionState(request)
204
+ const { ordnanceSurveyApiKey: apiKey } = options
205
+ const { value: select, error } = selectPayloadSchema.validate(payload)
206
+
207
+ if (error) {
208
+ const model = await selectViewModel({ session, apiKey }, select, error)
209
+
210
+ return h.view(viewName, model)
211
+ }
212
+
213
+ const addresses = await service.searchByUPRN(select.uprn, apiKey)
214
+ const property = addresses.at(0)
215
+
216
+ if (!property) {
217
+ throw Boom.internal(`UPRN ${property} not found`)
218
+ }
219
+
220
+ const { componentName, sourceUrl } = session.initial
221
+ flashComponentState(request, componentName, property)
222
+
223
+ // Redirect back to the source form page
224
+ return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER)
225
+ }
226
+
227
+ /**
228
+ * Post handler for the manual step
229
+ * @param {PostcodeLookupPostRequest} request
230
+ * @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
231
+ */
232
+ function manualPostHandler(request, h) {
233
+ const { payload } = request
234
+ const session = getSessionState(request)
235
+
236
+ const { value: manual, error } = manualPayloadSchema.validate(payload, {
237
+ abortEarly: false
238
+ })
239
+
240
+ if (error) {
241
+ const model = manualViewModel(session, manual, error)
242
+
243
+ return h.view(viewName, model)
244
+ }
245
+
246
+ const { componentName, sourceUrl } = session.initial
247
+ flashComponentState(request, componentName, manual)
248
+
249
+ // Redirect back to the source form page
250
+ return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER)
251
+ }
252
+
253
+ /**
254
+ * @import { ResponseToolkit, ServerRoute } from '@hapi/hapi'
255
+ * @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js'
256
+ * @import { FormRequestPayload, FormResponseToolkit } from '~/src/server/routes/types.js'
257
+ * @import { ExternalStateAppendage } from '~/src/server/plugins/engine/types.js'
258
+ */
@@ -0,0 +1,188 @@
1
+ import { getErrorMessage } from '@defra/forms-model'
2
+ import Boom from '@hapi/boom'
3
+
4
+ import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
5
+ import { getJson } from '~/src/server/services/httpService.js'
6
+
7
+ const logger = createLogger()
8
+
9
+ /**
10
+ * Returns an empty result set
11
+ */
12
+ function empty() {
13
+ return []
14
+ }
15
+
16
+ /**
17
+ * Logs OS places errors
18
+ * @param {unknown} err - the error
19
+ * @param {string} endpoint - the OS api endpoint
20
+ */
21
+ function logErrorAndReturnEmpty(err, endpoint) {
22
+ const msg = `${getErrorMessage(err)} ${(Boom.isBoom(err) && err.data?.payload?.error?.message) ?? ''}`
23
+
24
+ logger.error(err, `Exception occured calling OS places ${endpoint} - ${msg}}`)
25
+
26
+ return empty()
27
+ }
28
+
29
+ /**
30
+ * Fetch data from OS API
31
+ * @param {string} url - the url to get address json data from
32
+ * @param {string} endpoint - the url endpoint description for logging
33
+ */
34
+ async function getAddressData(url, endpoint) {
35
+ const getJsonByType =
36
+ /** @type {typeof getJson<DeliveryPointAddressResult>} */ (getJson)
37
+
38
+ try {
39
+ const response = await getJsonByType(url)
40
+
41
+ if (response.error) {
42
+ return logErrorAndReturnEmpty(response.error, endpoint)
43
+ }
44
+
45
+ const results = response.payload.results
46
+
47
+ if (!Array.isArray(results)) {
48
+ return empty()
49
+ }
50
+
51
+ return results.map((result) => formatAddress(result.DPA))
52
+ } catch (err) {
53
+ return logErrorAndReturnEmpty(err, endpoint)
54
+ }
55
+ }
56
+
57
+ /**
58
+ * OS places search
59
+ * @param {string} query - the search term
60
+ * @param {string} apiKey - the OS api key
61
+ */
62
+ export async function searchByQuery(query, apiKey) {
63
+ const endpoint = 'find'
64
+ const url = `https://api.os.uk/search/places/v1/${endpoint}?query=${encodeURIComponent(query)}&key=${apiKey}`
65
+
66
+ return getAddressData(url, endpoint)
67
+ }
68
+
69
+ /**
70
+ * OS postcode search
71
+ * @param {string} postcode - the postcode
72
+ * @param {string} apiKey - the OS api key
73
+ */
74
+ export async function searchByPostcode(postcode, apiKey) {
75
+ const endpoint = 'postcode'
76
+ const url = `https://api.os.uk/search/places/v1/${endpoint}?postcode=${encodeURIComponent(postcode.replaceAll(/\s/g, ''))}&key=${apiKey}`
77
+
78
+ return getAddressData(url, endpoint)
79
+ }
80
+
81
+ /**
82
+ * OS UPRN search
83
+ * @param {string} uprn - the unique property reference number
84
+ * @param {string} apiKey - the OS api key
85
+ */
86
+ export async function searchByUPRN(uprn, apiKey) {
87
+ const endpoint = 'uprn'
88
+ const url = `https://api.os.uk/search/places/v1/${endpoint}?uprn=${uprn}&key=${apiKey}`
89
+
90
+ return getAddressData(url, endpoint)
91
+ }
92
+
93
+ /**
94
+ * OS postcode and building name search
95
+ * @param {string} postcodeQuery - the postcode query
96
+ * @param {string} buildingNameQuery - the building name query
97
+ * @param {string} apiKey - the OS api key
98
+ */
99
+ export async function search(postcodeQuery, buildingNameQuery, apiKey) {
100
+ let addresses = await searchByPostcode(postcodeQuery, apiKey)
101
+
102
+ if (buildingNameQuery) {
103
+ addresses = addresses.filter((item) =>
104
+ item.address.includes(buildingNameQuery.toUpperCase())
105
+ )
106
+ }
107
+
108
+ return addresses
109
+ }
110
+
111
+ /**
112
+ * Converts a delivery point address to an address
113
+ * Taken from http://github.com/dwp/find-an-address-plugin/blob/main/utils/getData.js
114
+ * @param {DeliveryPointAddress} dpa
115
+ */
116
+ function formatAddress(dpa) {
117
+ const addressLine1 = formatAddressLine1(dpa)
118
+ const addressLine2 = formatAddressLine2(dpa)
119
+ const town = titleCase(dpa.POST_TOWN || '')
120
+ const postcode = dpa.POSTCODE || ''
121
+ const lines = [addressLine1, addressLine2, town]
122
+ const formatted = `${lines.filter((i) => !!i).join(', ')}, ${postcode}`
123
+
124
+ /**
125
+ * @type {Address}
126
+ */
127
+ const address = {
128
+ uprn: dpa.UPRN,
129
+ address: dpa.ADDRESS,
130
+ addressLine1,
131
+ addressLine2,
132
+ town,
133
+ county: '',
134
+ postcode,
135
+ formatted
136
+ }
137
+
138
+ return address
139
+ }
140
+
141
+ /**
142
+ * @param {DeliveryPointAddress} dpa
143
+ */
144
+ function formatAddressLine1(dpa) {
145
+ return titleCase(
146
+ dpa.ORGANISATION_NAME ||
147
+ dpa.SUB_BUILDING_NAME ||
148
+ dpa.BUILDING_NAME ||
149
+ dpa.BUILDING_NUMBER
150
+ ? [
151
+ dpa.ORGANISATION_NAME || '',
152
+ dpa.SUB_BUILDING_NAME || '',
153
+ dpa.BUILDING_NAME || '',
154
+ dpa.BUILDING_NUMBER || ''
155
+ ]
156
+ .filter((item) => !!item)
157
+ .join(' ')
158
+ : ''
159
+ )
160
+ }
161
+
162
+ /**
163
+ * @param {DeliveryPointAddress} dpa
164
+ */
165
+ function formatAddressLine2(dpa) {
166
+ return titleCase(
167
+ dpa.THOROUGHFARE_NAME || dpa.DEPENDENT_LOCALITY
168
+ ? [dpa.THOROUGHFARE_NAME || '', dpa.DEPENDENT_LOCALITY || '']
169
+ .filter((item) => !!item)
170
+ .join(', ')
171
+ : ''
172
+ )
173
+ }
174
+
175
+ /**
176
+ * Title case address
177
+ * @param {string} address
178
+ */
179
+ function titleCase(address) {
180
+ return address
181
+ .split(' ')
182
+ .map((item) => item.charAt(0).toUpperCase() + item.slice(1).toLowerCase())
183
+ .join(' ')
184
+ }
185
+
186
+ /**
187
+ * @import { Address, DeliveryPointAddress, DeliveryPointAddressResult } from '~/src/server/plugins/postcode-lookup/types.js'
188
+ */
@@ -0,0 +1,177 @@
1
+ import Boom from '@hapi/boom'
2
+
3
+ import * as service from '~/src/server/plugins/postcode-lookup/service.js'
4
+ import { result as postcodeResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js'
5
+ import { result as queryResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/query.js'
6
+ import { result as uprnResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js'
7
+ import { getJson } from '~/src/server/services/httpService.js'
8
+
9
+ jest.mock('~/src/server/services/httpService.ts')
10
+
11
+ describe('Postcode lookup service', () => {
12
+ describe('searchByPostcode', () => {
13
+ it('should return formatted addresses', async () => {
14
+ jest.mocked(getJson).mockResolvedValueOnce({
15
+ res: /** @type {IncomingMessage} */ ({
16
+ statusCode: 200,
17
+ headers: {}
18
+ }),
19
+ payload: postcodeResult,
20
+ error: undefined
21
+ })
22
+
23
+ const results = await service.searchByPostcode('NW1 6XE', 'apikey')
24
+
25
+ expect(results).toHaveLength(10)
26
+ expect(results.at(0)).toEqual({
27
+ address: "EMILIA'S CRAFTED PASTA, 215, BAKER STREET, LONDON, NW1 6XE",
28
+ addressLine1: "Emilia's Crafted Pasta 215",
29
+ addressLine2: 'Baker Street',
30
+ county: '',
31
+ formatted: "Emilia's Crafted Pasta 215, Baker Street, London, NW1 6XE",
32
+ postcode: 'NW1 6XE',
33
+ town: 'London',
34
+ uprn: '10033619968'
35
+ })
36
+ })
37
+
38
+ it('should return an empty response when an error is encountered', async () => {
39
+ jest.mocked(getJson).mockResolvedValueOnce({
40
+ res: /** @type {IncomingMessage} */ ({
41
+ statusCode: 300,
42
+ headers: {}
43
+ }),
44
+ payload: undefined,
45
+ error: new Error('Unknown error')
46
+ })
47
+
48
+ const results = await service.searchByPostcode('NW1 6XE', 'apikey')
49
+
50
+ expect(results).toHaveLength(0)
51
+ expect(results).toEqual([])
52
+ })
53
+
54
+ it('should return an empty response when a non 200 response is encountered', async () => {
55
+ jest
56
+ .mocked(getJson)
57
+ .mockRejectedValueOnce(
58
+ Boom.badRequest(
59
+ 'OS API error',
60
+ new Error('Invalid postcode segments')
61
+ )
62
+ )
63
+
64
+ const results = await service.searchByPostcode(
65
+ 'invalid postcode',
66
+ 'apikey'
67
+ )
68
+
69
+ expect(results).toHaveLength(0)
70
+ expect(results).toEqual([])
71
+ })
72
+
73
+ it('should return an empty response when no results are returned', async () => {
74
+ jest.mocked(getJson).mockResolvedValueOnce({
75
+ res: /** @type {IncomingMessage} */ ({
76
+ statusCode: 200,
77
+ headers: {}
78
+ }),
79
+ payload: { results: undefined },
80
+ error: undefined
81
+ })
82
+
83
+ const results = await service.searchByPostcode('NW1 6XE', 'apikey')
84
+
85
+ expect(results).toHaveLength(0)
86
+ expect(results).toEqual([])
87
+ })
88
+ })
89
+
90
+ describe('searchByUPRN', () => {
91
+ it('should return formatted addresses', async () => {
92
+ jest.mocked(getJson).mockResolvedValueOnce({
93
+ res: /** @type {IncomingMessage} */ ({
94
+ statusCode: 200,
95
+ headers: {}
96
+ }),
97
+ payload: uprnResult,
98
+ error: undefined
99
+ })
100
+
101
+ const results = await service.searchByUPRN('100023071949', 'apikey')
102
+
103
+ expect(results).toHaveLength(1)
104
+ expect(results.at(0)).toEqual({
105
+ address: 'SHERLOCK HOLMES MUSEUM, 221B, BAKER STREET, LONDON, NW1 6XE',
106
+ addressLine1: 'Sherlock Holmes Museum 221b',
107
+ addressLine2: 'Baker Street',
108
+ county: '',
109
+ formatted: 'Sherlock Holmes Museum 221b, Baker Street, London, NW1 6XE',
110
+ postcode: 'NW1 6XE',
111
+ town: 'London',
112
+ uprn: '100023071949'
113
+ })
114
+ })
115
+ })
116
+
117
+ describe('searchByQuery', () => {
118
+ it('should return formatted addresses', async () => {
119
+ jest.mocked(getJson).mockResolvedValueOnce({
120
+ res: /** @type {IncomingMessage} */ ({
121
+ statusCode: 200,
122
+ headers: {}
123
+ }),
124
+ payload: queryResult,
125
+ error: undefined
126
+ })
127
+
128
+ const results = await service.searchByQuery(
129
+ 'Prime minister downing',
130
+ 'apikey'
131
+ )
132
+
133
+ expect(results).toHaveLength(5)
134
+ expect(results.at(0)).toEqual({
135
+ address: 'BAKER STREET COTTAGE, BAKER STREET, FROME, BA11 3BL',
136
+ addressLine1: 'Baker Street Cottage',
137
+ addressLine2: 'Baker Street',
138
+ town: 'Frome',
139
+ county: '',
140
+ formatted: 'Baker Street Cottage, Baker Street, Frome, BA11 3BL',
141
+ postcode: 'BA11 3BL',
142
+ uprn: '250034655'
143
+ })
144
+ })
145
+ })
146
+
147
+ describe('search', () => {
148
+ it('should return formatted addresses', async () => {
149
+ jest.mocked(getJson).mockResolvedValueOnce({
150
+ res: /** @type {IncomingMessage} */ ({
151
+ statusCode: 200,
152
+ headers: {}
153
+ }),
154
+ payload: postcodeResult,
155
+ error: undefined
156
+ })
157
+
158
+ const results = await service.search('NW1 6XE', 'Emilia', 'apikey')
159
+
160
+ expect(results).toHaveLength(1)
161
+ expect(results.at(0)).toEqual({
162
+ address: "EMILIA'S CRAFTED PASTA, 215, BAKER STREET, LONDON, NW1 6XE",
163
+ addressLine1: "Emilia's Crafted Pasta 215",
164
+ addressLine2: 'Baker Street',
165
+ county: '',
166
+ formatted: "Emilia's Crafted Pasta 215, Baker Street, London, NW1 6XE",
167
+ postcode: 'NW1 6XE',
168
+ town: 'London',
169
+ uprn: '10033619968'
170
+ })
171
+ })
172
+ })
173
+ })
174
+
175
+ /**
176
+ * @import { IncomingMessage } from 'node:http'
177
+ */