@defra/forms-engine-plugin 0.0.4 → 0.0.5

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 (224) hide show
  1. package/package.json +3 -2
  2. package/src/client/javascripts/application.js +87 -0
  3. package/src/client/javascripts/file-upload.js +386 -0
  4. package/src/client/stylesheets/_code.scss +33 -0
  5. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  6. package/src/client/stylesheets/_prose.scss +56 -0
  7. package/src/client/stylesheets/_service-banner.scss +24 -0
  8. package/src/client/stylesheets/_summary-list.scss +28 -0
  9. package/src/client/stylesheets/_tag-env.scss +24 -0
  10. package/src/client/stylesheets/application.scss +14 -0
  11. package/src/common/cookies.js +58 -0
  12. package/src/common/cookies.test.js +23 -0
  13. package/src/common/types.js +5 -0
  14. package/src/config/index.ts +271 -0
  15. package/src/index.ts +31 -0
  16. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  17. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  18. package/src/server/common/helpers/logging/logger.ts +7 -0
  19. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  20. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  21. package/src/server/common/helpers/redis-client.js +70 -0
  22. package/src/server/constants.js +1 -0
  23. package/src/server/forms/README.md +10 -0
  24. package/src/server/forms/components.json +1015 -0
  25. package/src/server/forms/report-a-terrorist.json +270 -0
  26. package/src/server/forms/runner-components-test.json +365 -0
  27. package/src/server/forms/test.json +581 -0
  28. package/src/server/index.test.ts +582 -0
  29. package/src/server/index.ts +140 -0
  30. package/src/server/plugins/blankie.test.ts +73 -0
  31. package/src/server/plugins/blankie.ts +48 -0
  32. package/src/server/plugins/crumb.ts +20 -0
  33. package/src/server/plugins/engine/README.md +87 -0
  34. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  35. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  36. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  37. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  38. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  39. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  40. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  41. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  42. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  43. package/src/server/plugins/engine/components/Details.ts +30 -0
  44. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  45. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  46. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  47. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  48. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  49. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  50. package/src/server/plugins/engine/components/Html.ts +29 -0
  51. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  52. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  53. package/src/server/plugins/engine/components/List.test.ts +76 -0
  54. package/src/server/plugins/engine/components/List.ts +72 -0
  55. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  56. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  57. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  58. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  59. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  60. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  61. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  62. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  63. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  64. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  65. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  66. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  67. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  68. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  69. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  70. package/src/server/plugins/engine/components/TextField.ts +96 -0
  71. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  72. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  73. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  74. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  75. package/src/server/plugins/engine/components/constants.ts +1 -0
  76. package/src/server/plugins/engine/components/helpers.ts +330 -0
  77. package/src/server/plugins/engine/components/index.ts +24 -0
  78. package/src/server/plugins/engine/components/types.ts +117 -0
  79. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  80. package/src/server/plugins/engine/helpers.test.ts +791 -0
  81. package/src/server/plugins/engine/helpers.ts +379 -0
  82. package/src/server/plugins/engine/index.ts +7 -0
  83. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  84. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  85. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  86. package/src/server/plugins/engine/models/Section.ts +0 -0
  87. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  88. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  89. package/src/server/plugins/engine/models/index.ts +2 -0
  90. package/src/server/plugins/engine/models/types.ts +114 -0
  91. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  92. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  93. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  94. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  95. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  96. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  97. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  98. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  99. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1108 -0
  100. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +446 -0
  101. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  102. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  103. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  104. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +561 -0
  105. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  106. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  107. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  108. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  109. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +50 -0
  110. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +261 -0
  111. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  112. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  113. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  114. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  115. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  116. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  117. package/src/server/plugins/engine/plugin.ts +673 -0
  118. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  119. package/src/server/plugins/engine/services/formsService.js +46 -0
  120. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  121. package/src/server/plugins/engine/services/index.js +3 -0
  122. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  123. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  124. package/src/server/plugins/engine/services/uploadService.js +60 -0
  125. package/src/server/plugins/engine/types.ts +315 -0
  126. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  127. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  128. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  129. package/src/server/plugins/engine/views/components/details.html +6 -0
  130. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  131. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  132. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  133. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  134. package/src/server/plugins/engine/views/components/html.html +3 -0
  135. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  136. package/src/server/plugins/engine/views/components/list.html +36 -0
  137. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  138. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  139. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  140. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  141. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  142. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  143. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  144. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  145. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  146. package/src/server/plugins/engine/views/file-upload.html +45 -0
  147. package/src/server/plugins/engine/views/index.html +39 -0
  148. package/src/server/plugins/engine/views/item-delete.html +56 -0
  149. package/src/server/plugins/engine/views/partials/components.html +6 -0
  150. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  151. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  152. package/src/server/plugins/engine/views/partials/form.html +15 -0
  153. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  154. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  155. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  156. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  157. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  158. package/src/server/plugins/errorPages.ts +58 -0
  159. package/src/server/plugins/nunjucks/context.js +88 -0
  160. package/src/server/plugins/nunjucks/context.test.js +142 -0
  161. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  162. package/src/server/plugins/nunjucks/environment.js +116 -0
  163. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  164. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  165. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  166. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  167. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  168. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  169. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  170. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  171. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  172. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  173. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  174. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  175. package/src/server/plugins/nunjucks/index.js +3 -0
  176. package/src/server/plugins/nunjucks/plugin.js +40 -0
  177. package/src/server/plugins/nunjucks/render.js +42 -0
  178. package/src/server/plugins/nunjucks/types.js +40 -0
  179. package/src/server/plugins/pulse.ts +11 -0
  180. package/src/server/plugins/router.ts +201 -0
  181. package/src/server/plugins/session.ts +28 -0
  182. package/src/server/routes/health.js +13 -0
  183. package/src/server/routes/health.test.js +35 -0
  184. package/src/server/routes/index.test.ts +125 -0
  185. package/src/server/routes/index.ts +2 -0
  186. package/src/server/routes/public.ts +47 -0
  187. package/src/server/routes/types.ts +48 -0
  188. package/src/server/schemas/index.ts +34 -0
  189. package/src/server/secure-context.js +43 -0
  190. package/src/server/services/cacheService.test.ts +276 -0
  191. package/src/server/services/cacheService.ts +131 -0
  192. package/src/server/services/httpService.test.js +491 -0
  193. package/src/server/services/httpService.ts +50 -0
  194. package/src/server/services/index.ts +1 -0
  195. package/src/server/types.ts +54 -0
  196. package/src/server/utils/notify.test.ts +37 -0
  197. package/src/server/utils/notify.ts +50 -0
  198. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  199. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  200. package/src/server/utils/utils.js +24 -0
  201. package/src/server/utils/utils.test.js +54 -0
  202. package/src/server/views/404.html +16 -0
  203. package/src/server/views/500.html +19 -0
  204. package/src/server/views/components/debug/macro.njk +3 -0
  205. package/src/server/views/components/debug/template.njk +13 -0
  206. package/src/server/views/components/service-banner/macro.njk +3 -0
  207. package/src/server/views/components/service-banner/template.njk +20 -0
  208. package/src/server/views/components/service-banner/template.test.js +43 -0
  209. package/src/server/views/components/tag-env/macro.njk +3 -0
  210. package/src/server/views/components/tag-env/template.njk +30 -0
  211. package/src/server/views/components/tag-env/template.test.js +66 -0
  212. package/src/server/views/confirmation.html +19 -0
  213. package/src/server/views/help/accessibility-statement.html +58 -0
  214. package/src/server/views/help/cookie-preferences.html +57 -0
  215. package/src/server/views/help/cookies.html +71 -0
  216. package/src/server/views/help/get-support.html +37 -0
  217. package/src/server/views/help/privacy-notice.html +68 -0
  218. package/src/server/views/help/terms-and-conditions.html +83 -0
  219. package/src/server/views/layout.html +199 -0
  220. package/src/server/views/summary.html +50 -0
  221. package/src/typings/hapi/index.d.ts +95 -0
  222. package/src/typings/hapi-tracing/index.d.ts +6 -0
  223. package/src/typings/index.d.ts +3 -0
  224. package/src/typings/joi/index.d.ts +22 -0
@@ -0,0 +1,491 @@
1
+ import { getTraceId } from '@defra/hapi-tracing'
2
+ import Boom from '@hapi/boom'
3
+ import Wreck from '@hapi/wreck'
4
+ import { StatusCodes } from 'http-status-codes'
5
+
6
+ import {
7
+ get,
8
+ getJson,
9
+ post,
10
+ postJson,
11
+ put
12
+ } from '~/src/server/services/httpService.js'
13
+
14
+ jest.mock('@defra/hapi-tracing')
15
+
16
+ describe('HTTP service', () => {
17
+ /** @type {RequestOptions} */
18
+ let authOptions
19
+ /** @type {RequestOptions} */
20
+ let blankOptions
21
+ /** @type {RequestOptions} */
22
+ let timeoutOptions
23
+
24
+ beforeEach(() => {
25
+ authOptions = {
26
+ headers: { Authorization: 'Bearer ey56yDSASDFfbgcbc' }
27
+ }
28
+ blankOptions = {}
29
+ timeoutOptions = {
30
+ timeout: 5000
31
+ }
32
+ })
33
+
34
+ describe('GET', () => {
35
+ beforeEach(() => {
36
+ jest.spyOn(Wreck, 'get').mockResolvedValue({
37
+ res: /** @type {IncomingMessage} */ ({
38
+ statusCode: StatusCodes.OK
39
+ }),
40
+ payload: undefined
41
+ })
42
+ })
43
+
44
+ it('passes headers', async () => {
45
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
46
+ await expect(get('/test', blankOptions)).resolves.toEqual({
47
+ res: { statusCode: StatusCodes.OK }
48
+ })
49
+
50
+ expect(Wreck.get).toHaveBeenCalledWith('/test', {
51
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
52
+ })
53
+ })
54
+
55
+ it('passes additional headers', async () => {
56
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
57
+ await expect(get('/test', authOptions)).resolves.toEqual({
58
+ res: { statusCode: StatusCodes.OK }
59
+ })
60
+
61
+ expect(Wreck.get).toHaveBeenCalledWith('/test', {
62
+ headers: {
63
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
64
+ 'x-cdp-request-id': 'my-trace-id'
65
+ }
66
+ })
67
+ })
68
+
69
+ it('passes non headers options', async () => {
70
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
71
+ await expect(get('/test', timeoutOptions)).resolves.toEqual({
72
+ res: { statusCode: StatusCodes.OK }
73
+ })
74
+
75
+ expect(Wreck.get).toHaveBeenCalledWith('/test', {
76
+ headers: {
77
+ 'x-cdp-request-id': 'my-trace-id'
78
+ },
79
+ timeout: 5000
80
+ })
81
+ })
82
+
83
+ it('sends request', async () => {
84
+ await expect(get('/test', blankOptions)).resolves.toEqual({
85
+ res: { statusCode: StatusCodes.OK }
86
+ })
87
+
88
+ expect(Wreck.get).toHaveBeenCalledWith('/test', {})
89
+ })
90
+
91
+ it('sends request as JSON', async () => {
92
+ await expect(getJson('/test')).resolves.toEqual({
93
+ res: { statusCode: StatusCodes.OK }
94
+ })
95
+
96
+ expect(Wreck.get).toHaveBeenCalledWith('/test', { json: true })
97
+ })
98
+ })
99
+
100
+ describe('GET (with error)', () => {
101
+ const error = Boom.notFound()
102
+
103
+ beforeEach(() => {
104
+ jest.spyOn(Wreck, 'get').mockResolvedValue({
105
+ res: /** @type {IncomingMessage} */ ({
106
+ statusCode: StatusCodes.NOT_FOUND
107
+ }),
108
+ payload: error
109
+ })
110
+ })
111
+
112
+ it('passes headers', async () => {
113
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
114
+ await expect(get('/error', blankOptions)).resolves.toEqual({
115
+ res: { statusCode: StatusCodes.NOT_FOUND },
116
+ error
117
+ })
118
+
119
+ expect(Wreck.get).toHaveBeenCalledWith('/error', {
120
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
121
+ })
122
+ })
123
+
124
+ it('passes additional headers', async () => {
125
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
126
+ await expect(get('/error', authOptions)).resolves.toEqual({
127
+ res: { statusCode: StatusCodes.NOT_FOUND },
128
+ error
129
+ })
130
+
131
+ expect(Wreck.get).toHaveBeenCalledWith('/error', {
132
+ headers: {
133
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
134
+ 'x-cdp-request-id': 'my-trace-id'
135
+ }
136
+ })
137
+ })
138
+
139
+ it('passes non headers options', async () => {
140
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
141
+ await expect(get('/error', timeoutOptions)).resolves.toEqual({
142
+ res: { statusCode: StatusCodes.NOT_FOUND },
143
+ error
144
+ })
145
+
146
+ expect(Wreck.get).toHaveBeenCalledWith('/error', {
147
+ headers: {
148
+ 'x-cdp-request-id': 'my-trace-id'
149
+ },
150
+ timeout: 5000
151
+ })
152
+ })
153
+
154
+ it('sends request (with error)', async () => {
155
+ await expect(get('/error', blankOptions)).resolves.toEqual({
156
+ res: { statusCode: StatusCodes.NOT_FOUND },
157
+ error
158
+ })
159
+
160
+ expect(Wreck.get).toHaveBeenCalledWith('/error', {})
161
+ })
162
+
163
+ it('sends request as JSON (with error)', async () => {
164
+ await expect(getJson('/error')).resolves.toEqual({
165
+ res: { statusCode: StatusCodes.NOT_FOUND },
166
+ error
167
+ })
168
+
169
+ expect(Wreck.get).toHaveBeenCalledWith('/error', { json: true })
170
+ })
171
+
172
+ it('sends request (unknown error)', async () => {
173
+ jest.spyOn(Wreck, 'get').mockResolvedValue({
174
+ res: /** @type {IncomingMessage} */ ({
175
+ statusCode: StatusCodes.NOT_FOUND
176
+ }),
177
+ payload: undefined
178
+ })
179
+
180
+ await expect(get('/error', blankOptions)).resolves.toEqual({
181
+ res: { statusCode: StatusCodes.NOT_FOUND },
182
+ error: new Error('Unknown error')
183
+ })
184
+
185
+ expect(Wreck.get).toHaveBeenCalledWith('/error', {})
186
+ })
187
+ })
188
+
189
+ describe('POST', () => {
190
+ beforeEach(() => {
191
+ jest.spyOn(Wreck, 'post').mockResolvedValue({
192
+ res: /** @type {IncomingMessage} */ ({
193
+ statusCode: StatusCodes.OK
194
+ }),
195
+ payload: { reference: '1234' }
196
+ })
197
+ })
198
+
199
+ it('passes headers', async () => {
200
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
201
+ await expect(post('/test', blankOptions)).resolves.toEqual({
202
+ res: { statusCode: StatusCodes.OK },
203
+ payload: { reference: '1234' }
204
+ })
205
+
206
+ expect(Wreck.post).toHaveBeenCalledWith('/test', {
207
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
208
+ })
209
+ })
210
+
211
+ it('passes additonal headers', async () => {
212
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
213
+ await expect(post('/test', authOptions)).resolves.toEqual({
214
+ res: { statusCode: StatusCodes.OK },
215
+ payload: { reference: '1234' }
216
+ })
217
+
218
+ expect(Wreck.post).toHaveBeenCalledWith('/test', {
219
+ headers: {
220
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
221
+ 'x-cdp-request-id': 'my-trace-id'
222
+ }
223
+ })
224
+ })
225
+
226
+ it('passes non headers options', async () => {
227
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
228
+ await expect(post('/test', timeoutOptions)).resolves.toEqual({
229
+ res: { statusCode: StatusCodes.OK },
230
+ payload: { reference: '1234' }
231
+ })
232
+
233
+ expect(Wreck.post).toHaveBeenCalledWith('/test', {
234
+ headers: {
235
+ 'x-cdp-request-id': 'my-trace-id'
236
+ },
237
+ timeout: 5000
238
+ })
239
+ })
240
+
241
+ it('sends request', async () => {
242
+ await expect(post('/test', blankOptions)).resolves.toEqual({
243
+ res: { statusCode: StatusCodes.OK },
244
+ payload: { reference: '1234' }
245
+ })
246
+
247
+ expect(Wreck.post).toHaveBeenCalledWith('/test', {})
248
+ })
249
+
250
+ it('sends request as JSON', async () => {
251
+ await expect(postJson('/test', blankOptions)).resolves.toEqual({
252
+ res: { statusCode: StatusCodes.OK },
253
+ payload: { reference: '1234' }
254
+ })
255
+
256
+ expect(Wreck.post).toHaveBeenCalledWith('/test', { json: true })
257
+ })
258
+ })
259
+
260
+ describe('POST (with error)', () => {
261
+ const error = Boom.notFound()
262
+
263
+ beforeEach(() => {
264
+ jest.spyOn(Wreck, 'post').mockResolvedValue({
265
+ res: /** @type {IncomingMessage} */ ({
266
+ statusCode: StatusCodes.NOT_FOUND
267
+ }),
268
+ payload: error
269
+ })
270
+ })
271
+
272
+ it('passes headers', async () => {
273
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
274
+ await expect(post('/error', blankOptions)).resolves.toEqual({
275
+ res: { statusCode: StatusCodes.NOT_FOUND },
276
+ error
277
+ })
278
+
279
+ expect(Wreck.post).toHaveBeenCalledWith('/error', {
280
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
281
+ })
282
+ })
283
+
284
+ it('passes additional headers', async () => {
285
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
286
+ await expect(post('/error', authOptions)).resolves.toEqual({
287
+ res: { statusCode: StatusCodes.NOT_FOUND },
288
+ error
289
+ })
290
+
291
+ expect(Wreck.post).toHaveBeenCalledWith('/error', {
292
+ headers: {
293
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
294
+ 'x-cdp-request-id': 'my-trace-id'
295
+ }
296
+ })
297
+ })
298
+
299
+ it('passes non headers options', async () => {
300
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
301
+ await expect(post('/error', timeoutOptions)).resolves.toEqual({
302
+ res: { statusCode: StatusCodes.NOT_FOUND },
303
+ error
304
+ })
305
+
306
+ expect(Wreck.post).toHaveBeenCalledWith('/error', {
307
+ headers: {
308
+ 'x-cdp-request-id': 'my-trace-id'
309
+ },
310
+ timeout: 5000
311
+ })
312
+ })
313
+
314
+ it('sends request (with error)', async () => {
315
+ await expect(post('/error', blankOptions)).resolves.toEqual({
316
+ res: { statusCode: StatusCodes.NOT_FOUND },
317
+ error
318
+ })
319
+
320
+ expect(Wreck.post).toHaveBeenCalledWith('/error', {})
321
+ })
322
+
323
+ it('sends request as JSON (with error)', async () => {
324
+ await expect(postJson('/error', blankOptions)).resolves.toEqual({
325
+ res: { statusCode: StatusCodes.NOT_FOUND },
326
+ error
327
+ })
328
+
329
+ expect(Wreck.post).toHaveBeenCalledWith('/error', { json: true })
330
+ })
331
+
332
+ it('sends request (unknown error)', async () => {
333
+ jest.spyOn(Wreck, 'post').mockResolvedValue({
334
+ res: /** @type {IncomingMessage} */ ({
335
+ statusCode: StatusCodes.NOT_FOUND
336
+ }),
337
+ payload: undefined
338
+ })
339
+
340
+ await expect(post('/error', blankOptions)).resolves.toEqual({
341
+ res: { statusCode: StatusCodes.NOT_FOUND },
342
+ error: new Error('Unknown error')
343
+ })
344
+
345
+ expect(Wreck.post).toHaveBeenCalledWith('/error', {})
346
+ })
347
+ })
348
+
349
+ describe('PUT', () => {
350
+ beforeEach(() => {
351
+ jest.spyOn(Wreck, 'put').mockResolvedValue({
352
+ res: /** @type {IncomingMessage} */ ({
353
+ statusCode: StatusCodes.OK
354
+ }),
355
+ payload: undefined
356
+ })
357
+ })
358
+
359
+ it('passes headers', async () => {
360
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
361
+ await expect(put('/test', blankOptions)).resolves.toEqual({
362
+ res: { statusCode: StatusCodes.OK }
363
+ })
364
+
365
+ expect(Wreck.put).toHaveBeenCalledWith('/test', {
366
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
367
+ })
368
+ })
369
+
370
+ it('passes additional headers', async () => {
371
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
372
+ await expect(put('/test', authOptions)).resolves.toEqual({
373
+ res: { statusCode: StatusCodes.OK }
374
+ })
375
+
376
+ expect(Wreck.put).toHaveBeenCalledWith('/test', {
377
+ headers: {
378
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
379
+ 'x-cdp-request-id': 'my-trace-id'
380
+ }
381
+ })
382
+ })
383
+
384
+ it('passes non headers options', async () => {
385
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
386
+ await expect(put('/test', timeoutOptions)).resolves.toEqual({
387
+ res: { statusCode: StatusCodes.OK }
388
+ })
389
+
390
+ expect(Wreck.put).toHaveBeenCalledWith('/test', {
391
+ headers: {
392
+ 'x-cdp-request-id': 'my-trace-id'
393
+ },
394
+ timeout: 5000
395
+ })
396
+ })
397
+
398
+ it('sends request', async () => {
399
+ await expect(put('/test', blankOptions)).resolves.toEqual({
400
+ res: { statusCode: StatusCodes.OK }
401
+ })
402
+
403
+ expect(Wreck.put).toHaveBeenCalledWith('/test', {})
404
+ })
405
+ })
406
+
407
+ describe('PUT (with error)', () => {
408
+ const error = Boom.notFound()
409
+
410
+ beforeEach(() => {
411
+ jest.spyOn(Wreck, 'put').mockResolvedValue({
412
+ res: /** @type {IncomingMessage} */ ({
413
+ statusCode: StatusCodes.NOT_FOUND
414
+ }),
415
+ payload: error
416
+ })
417
+ })
418
+
419
+ it('passes headers', async () => {
420
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
421
+ await expect(put('/error', blankOptions)).resolves.toEqual({
422
+ res: { statusCode: StatusCodes.NOT_FOUND },
423
+ error
424
+ })
425
+
426
+ expect(Wreck.put).toHaveBeenCalledWith('/error', {
427
+ headers: { 'x-cdp-request-id': 'my-trace-id' }
428
+ })
429
+ })
430
+
431
+ it('passes additional headers', async () => {
432
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
433
+ await expect(put('/error', authOptions)).resolves.toEqual({
434
+ res: { statusCode: StatusCodes.NOT_FOUND },
435
+ error
436
+ })
437
+
438
+ expect(Wreck.put).toHaveBeenCalledWith('/error', {
439
+ headers: {
440
+ Authorization: 'Bearer ey56yDSASDFfbgcbc',
441
+ 'x-cdp-request-id': 'my-trace-id'
442
+ }
443
+ })
444
+ })
445
+
446
+ it('passes non headers options', async () => {
447
+ jest.mocked(getTraceId).mockReturnValue('my-trace-id')
448
+ await expect(put('/error', timeoutOptions)).resolves.toEqual({
449
+ res: { statusCode: StatusCodes.NOT_FOUND },
450
+ error
451
+ })
452
+
453
+ expect(Wreck.put).toHaveBeenCalledWith('/error', {
454
+ headers: {
455
+ 'x-cdp-request-id': 'my-trace-id'
456
+ },
457
+ timeout: 5000
458
+ })
459
+ })
460
+
461
+ it('sends request (with error)', async () => {
462
+ await expect(put('/error', blankOptions)).resolves.toEqual({
463
+ res: { statusCode: StatusCodes.NOT_FOUND },
464
+ error
465
+ })
466
+
467
+ expect(Wreck.put).toHaveBeenCalledWith('/error', {})
468
+ })
469
+
470
+ it('sends request (unknown error)', async () => {
471
+ jest.spyOn(Wreck, 'put').mockResolvedValue({
472
+ res: /** @type {IncomingMessage} */ ({
473
+ statusCode: StatusCodes.NOT_FOUND
474
+ }),
475
+ payload: undefined
476
+ })
477
+
478
+ await expect(put('/error', blankOptions)).resolves.toEqual({
479
+ res: { statusCode: StatusCodes.NOT_FOUND },
480
+ error: new Error('Unknown error')
481
+ })
482
+
483
+ expect(Wreck.put).toHaveBeenCalledWith('/error', {})
484
+ })
485
+ })
486
+ })
487
+
488
+ /**
489
+ * @import { IncomingMessage } from 'node:http'
490
+ * @import { RequestOptions } from '~/src/server/services/httpService.js'
491
+ */
@@ -0,0 +1,50 @@
1
+ import Wreck from '@hapi/wreck'
2
+
3
+ import { applyTraceHeaders } from '~/src/server/utils/utils.js'
4
+
5
+ export type Method = keyof Pick<typeof Wreck, 'get' | 'post' | 'put' | 'delete'>
6
+ export type RequestOptions = Parameters<typeof Wreck.defaults>[0]
7
+
8
+ export const request = async <BodyType = Buffer>(
9
+ method: Method,
10
+ url: string,
11
+ options?: RequestOptions
12
+ ) => {
13
+ const headers = applyTraceHeaders(options?.headers)
14
+
15
+ const mergedOptions = { ...options, headers }
16
+
17
+ const { res, payload } = await Wreck[method]<BodyType>(url, mergedOptions)
18
+
19
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode > 299) {
20
+ return { res, error: payload || new Error('Unknown error') }
21
+ }
22
+
23
+ return { res, payload }
24
+ }
25
+
26
+ export const get = <BodyType>(url: string, options?: RequestOptions) => {
27
+ return request<BodyType>('get', url, options)
28
+ }
29
+
30
+ export const getJson = <BodyType extends object>(url: string) => {
31
+ return get<BodyType>(url, { json: true })
32
+ }
33
+
34
+ export const post = <BodyType>(url: string, options: RequestOptions) => {
35
+ return request<BodyType>('post', url, options)
36
+ }
37
+
38
+ export const postJson = <BodyType extends object>(
39
+ url: string,
40
+ options: RequestOptions
41
+ ) => {
42
+ return post<BodyType>(url, {
43
+ ...options,
44
+ json: true
45
+ })
46
+ }
47
+
48
+ export const put = <BodyType>(url: string, options: RequestOptions) => {
49
+ return request<BodyType>('put', url, options)
50
+ }
@@ -0,0 +1 @@
1
+ export { CacheService } from '~/src/server/services/cacheService.js'
@@ -0,0 +1,54 @@
1
+ import {
2
+ type FormDefinition,
3
+ type FormMetadata,
4
+ type SubmitPayload,
5
+ type SubmitResponsePayload
6
+ } from '@defra/forms-model'
7
+
8
+ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
9
+ import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
10
+ import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
11
+ import {
12
+ type FormRequestPayload,
13
+ type FormStatus
14
+ } from '~/src/server/routes/types.js'
15
+
16
+ export interface FormsService {
17
+ getFormMetadata: (slug: string) => Promise<FormMetadata>
18
+ getFormDefinition: (
19
+ id: string,
20
+ state: FormStatus
21
+ ) => Promise<FormDefinition | undefined>
22
+ }
23
+
24
+ export interface FormSubmissionService {
25
+ persistFiles: (
26
+ files: { fileId: string; initiatedRetrievalKey: string }[],
27
+ persistedRetrievalKey: string
28
+ ) => Promise<object>
29
+ submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>
30
+ }
31
+
32
+ export interface Services {
33
+ formsService: FormsService
34
+ formSubmissionService: FormSubmissionService
35
+ outputService: OutputService
36
+ }
37
+
38
+ export interface RouteConfig {
39
+ formFileName?: string
40
+ formFilePath?: string
41
+ enforceCsrf?: boolean
42
+ services?: Services
43
+ controllers?: Record<string, typeof PageController>
44
+ }
45
+
46
+ export interface OutputService {
47
+ submit: (
48
+ request: FormRequestPayload,
49
+ model: FormModel,
50
+ emailAddress: string,
51
+ items: DetailItem[],
52
+ submitResponse: SubmitResponsePayload
53
+ ) => Promise<void>
54
+ }
@@ -0,0 +1,37 @@
1
+ import { postJson } from '~/src/server/services/httpService.js'
2
+ import { sendNotification } from '~/src/server/utils/notify.js'
3
+
4
+ jest.mock('~/src/server/services/httpService')
5
+
6
+ describe('Utils: Notify', () => {
7
+ const templateId = 'example-template-id'
8
+ const emailAddress = 'enrique.chase@defra.gov.uk'
9
+ const personalisation = {
10
+ subject: 'Hello',
11
+ body: 'World'
12
+ }
13
+
14
+ describe('sendNotification', () => {
15
+ it('calls postJson with personalised email payload', async () => {
16
+ await sendNotification({
17
+ templateId,
18
+ emailAddress,
19
+ personalisation
20
+ })
21
+
22
+ expect(postJson).toHaveBeenCalledWith(
23
+ 'https://api.notifications.service.gov.uk/v2/notifications/email',
24
+ {
25
+ payload: {
26
+ template_id: templateId,
27
+ email_address: emailAddress,
28
+ personalisation
29
+ },
30
+ headers: {
31
+ Authorization: expect.stringMatching(/^Bearer /)
32
+ }
33
+ }
34
+ )
35
+ })
36
+ })
37
+ })
@@ -0,0 +1,50 @@
1
+ import { token } from '@hapi/jwt'
2
+
3
+ import { config } from '~/src/config/index.js'
4
+ import { postJson } from '~/src/server/services/httpService.js'
5
+
6
+ const notifyAPIKey = config.get('notifyAPIKey')
7
+
8
+ // Extract the two uuids from the notifyApiKey
9
+ // See https://github.com/alphagov/notifications-node-client/blob/main/client/api_client.js#L17
10
+ // Needed until `https://github.com/alphagov/notifications-node-client/pull/200` is published
11
+ const apiKeyId: string = notifyAPIKey.substring(
12
+ notifyAPIKey.length - 36,
13
+ notifyAPIKey.length
14
+ )
15
+ const serviceId: string = notifyAPIKey.substring(
16
+ notifyAPIKey.length - 73,
17
+ notifyAPIKey.length - 37
18
+ )
19
+
20
+ export interface SendNotificationArgs {
21
+ templateId: string
22
+ emailAddress: string
23
+ personalisation: { subject: string; body: string }
24
+ }
25
+
26
+ function createToken(iss: string, secret: string) {
27
+ const iat = Math.round(Date.now() / 1000)
28
+
29
+ return token.generate({ iss, iat }, secret, {
30
+ header: { typ: 'JWT', alg: 'HS256' }
31
+ })
32
+ }
33
+
34
+ export async function sendNotification(args: SendNotificationArgs) {
35
+ const { templateId, emailAddress, personalisation } = args
36
+
37
+ return postJson(
38
+ 'https://api.notifications.service.gov.uk/v2/notifications/email',
39
+ {
40
+ payload: {
41
+ template_id: templateId,
42
+ email_address: emailAddress,
43
+ personalisation
44
+ },
45
+ headers: {
46
+ Authorization: 'Bearer ' + createToken(serviceId, apiKeyId)
47
+ }
48
+ }
49
+ )
50
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Get base64 certs from all environment variables starting with TRUSTSTORE_
3
+ * @param {NodeJS.ProcessEnv} envs - environment variables
4
+ */
5
+ export const getTrustStoreCerts = (envs) =>
6
+ Object.entries(envs)
7
+ .filter(
8
+ /** @type { (env: [string, string?]) => env is [string, string] } */
9
+ (([key, value]) => key.startsWith('TRUSTSTORE_') && !!value)
10
+ )
11
+ .map(([, value]) => Buffer.from(value, 'base64').toString().trim())