@defra/forms-engine-plugin 0.0.4 → 0.0.6

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 (260) hide show
  1. package/.server/server/index.js +0 -4
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/helpers.js +3 -0
  4. package/.server/server/plugins/engine/helpers.js.map +1 -1
  5. package/.server/server/plugins/engine/index.js +27 -1
  6. package/.server/server/plugins/engine/index.js.map +1 -1
  7. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
  8. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  9. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
  10. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  11. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
  12. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
  14. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/plugin.js +65 -6
  16. package/.server/server/plugins/engine/plugin.js.map +1 -1
  17. package/.server/server/plugins/engine/types.js.map +1 -1
  18. package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
  19. package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
  20. package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
  21. package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
  22. package/.server/server/services/cacheService.js +5 -2
  23. package/.server/server/services/cacheService.js.map +1 -1
  24. package/.server/typings/hapi/index.d.js.map +1 -1
  25. package/README.md +215 -4
  26. package/package.json +3 -3
  27. package/src/client/javascripts/application.js +87 -0
  28. package/src/client/javascripts/file-upload.js +386 -0
  29. package/src/client/stylesheets/_code.scss +33 -0
  30. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  31. package/src/client/stylesheets/_prose.scss +56 -0
  32. package/src/client/stylesheets/_service-banner.scss +24 -0
  33. package/src/client/stylesheets/_summary-list.scss +28 -0
  34. package/src/client/stylesheets/_tag-env.scss +24 -0
  35. package/src/client/stylesheets/application.scss +14 -0
  36. package/src/common/cookies.js +58 -0
  37. package/src/common/cookies.test.js +23 -0
  38. package/src/common/types.js +5 -0
  39. package/src/config/index.ts +271 -0
  40. package/src/index.ts +31 -0
  41. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  42. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  43. package/src/server/common/helpers/logging/logger.ts +7 -0
  44. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  45. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  46. package/src/server/common/helpers/redis-client.js +70 -0
  47. package/src/server/constants.js +1 -0
  48. package/src/server/forms/README.md +10 -0
  49. package/src/server/forms/components.json +1015 -0
  50. package/src/server/forms/report-a-terrorist.json +270 -0
  51. package/src/server/forms/runner-components-test.json +365 -0
  52. package/src/server/forms/test.json +581 -0
  53. package/src/server/index.test.ts +582 -0
  54. package/src/server/index.ts +135 -0
  55. package/src/server/plugins/blankie.test.ts +73 -0
  56. package/src/server/plugins/blankie.ts +48 -0
  57. package/src/server/plugins/crumb.ts +20 -0
  58. package/src/server/plugins/engine/README.md +87 -0
  59. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  60. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  61. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  62. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  63. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  64. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  65. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  66. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  67. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  68. package/src/server/plugins/engine/components/Details.ts +30 -0
  69. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  70. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  71. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  72. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  73. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  74. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  75. package/src/server/plugins/engine/components/Html.ts +29 -0
  76. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  77. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  78. package/src/server/plugins/engine/components/List.test.ts +76 -0
  79. package/src/server/plugins/engine/components/List.ts +72 -0
  80. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  81. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  82. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  83. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  84. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  85. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  86. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  87. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  88. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  89. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  90. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  91. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  92. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  93. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  94. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  95. package/src/server/plugins/engine/components/TextField.ts +96 -0
  96. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  97. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  98. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  99. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  100. package/src/server/plugins/engine/components/constants.ts +1 -0
  101. package/src/server/plugins/engine/components/helpers.ts +330 -0
  102. package/src/server/plugins/engine/components/index.ts +24 -0
  103. package/src/server/plugins/engine/components/types.ts +117 -0
  104. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  105. package/src/server/plugins/engine/helpers.test.ts +791 -0
  106. package/src/server/plugins/engine/helpers.ts +384 -0
  107. package/src/server/plugins/engine/index.ts +47 -0
  108. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  109. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  110. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  111. package/src/server/plugins/engine/models/Section.ts +0 -0
  112. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  113. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  114. package/src/server/plugins/engine/models/index.ts +2 -0
  115. package/src/server/plugins/engine/models/types.ts +114 -0
  116. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  117. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  118. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  119. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  120. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  121. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  122. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  123. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  124. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1116 -0
  125. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -0
  126. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  127. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  128. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  129. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +565 -0
  130. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  131. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  132. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  133. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  134. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +51 -0
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -0
  136. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  137. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  138. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  139. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  140. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  141. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  142. package/src/server/plugins/engine/plugin.ts +753 -0
  143. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  144. package/src/server/plugins/engine/services/formsService.js +46 -0
  145. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  146. package/src/server/plugins/engine/services/index.js +3 -0
  147. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  148. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  149. package/src/server/plugins/engine/services/uploadService.js +60 -0
  150. package/src/server/plugins/engine/types.ts +317 -0
  151. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  152. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  153. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  154. package/src/server/plugins/engine/views/components/debug/macro.njk +3 -0
  155. package/src/server/plugins/engine/views/components/debug/template.njk +13 -0
  156. package/src/server/plugins/engine/views/components/details.html +6 -0
  157. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  158. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  159. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  160. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  161. package/src/server/plugins/engine/views/components/html.html +3 -0
  162. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  163. package/src/server/plugins/engine/views/components/list.html +36 -0
  164. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  165. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  166. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  167. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  168. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  169. package/src/server/plugins/engine/views/components/service-banner/macro.njk +3 -0
  170. package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
  171. package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
  172. package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
  173. package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
  174. package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -0
  175. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  176. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  177. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  178. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  179. package/src/server/plugins/engine/views/confirmation.html +19 -0
  180. package/src/server/plugins/engine/views/file-upload.html +45 -0
  181. package/src/server/plugins/engine/views/index.html +39 -0
  182. package/src/server/plugins/engine/views/item-delete.html +56 -0
  183. package/src/server/plugins/engine/views/layout.html +199 -0
  184. package/src/server/plugins/engine/views/partials/components.html +6 -0
  185. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  186. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  187. package/src/server/plugins/engine/views/partials/form.html +15 -0
  188. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  189. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  190. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  191. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  192. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  193. package/src/server/plugins/engine/views/summary.html +50 -0
  194. package/src/server/plugins/errorPages.ts +58 -0
  195. package/src/server/plugins/nunjucks/context.js +88 -0
  196. package/src/server/plugins/nunjucks/context.test.js +142 -0
  197. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  198. package/src/server/plugins/nunjucks/environment.js +116 -0
  199. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  200. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  201. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  202. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  203. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  204. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  205. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  206. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  207. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  208. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  209. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  210. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  211. package/src/server/plugins/nunjucks/index.js +3 -0
  212. package/src/server/plugins/nunjucks/plugin.js +40 -0
  213. package/src/server/plugins/nunjucks/render.js +42 -0
  214. package/src/server/plugins/nunjucks/types.js +40 -0
  215. package/src/server/plugins/pulse.ts +11 -0
  216. package/src/server/plugins/router.ts +201 -0
  217. package/src/server/plugins/session.ts +28 -0
  218. package/src/server/routes/health.js +13 -0
  219. package/src/server/routes/health.test.js +35 -0
  220. package/src/server/routes/index.test.ts +125 -0
  221. package/src/server/routes/index.ts +2 -0
  222. package/src/server/routes/public.ts +47 -0
  223. package/src/server/routes/types.ts +48 -0
  224. package/src/server/schemas/index.ts +34 -0
  225. package/src/server/secure-context.js +43 -0
  226. package/src/server/services/cacheService.test.ts +277 -0
  227. package/src/server/services/cacheService.ts +138 -0
  228. package/src/server/services/httpService.test.js +491 -0
  229. package/src/server/services/httpService.ts +50 -0
  230. package/src/server/services/index.ts +1 -0
  231. package/src/server/types.ts +54 -0
  232. package/src/server/utils/notify.test.ts +37 -0
  233. package/src/server/utils/notify.ts +50 -0
  234. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  235. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  236. package/src/server/utils/utils.js +24 -0
  237. package/src/server/utils/utils.test.js +54 -0
  238. package/src/server/views/404.html +16 -0
  239. package/src/server/views/500.html +19 -0
  240. package/src/server/views/help/accessibility-statement.html +58 -0
  241. package/src/server/views/help/cookie-preferences.html +57 -0
  242. package/src/server/views/help/cookies.html +71 -0
  243. package/src/server/views/help/get-support.html +37 -0
  244. package/src/server/views/help/privacy-notice.html +68 -0
  245. package/src/server/views/help/terms-and-conditions.html +83 -0
  246. package/src/typings/hapi/index.d.ts +87 -0
  247. package/src/typings/hapi-tracing/index.d.ts +6 -0
  248. package/src/typings/index.d.ts +3 -0
  249. package/src/typings/joi/index.d.ts +22 -0
  250. package/.server/server/views/components/service-banner/template.test.js.map +0 -1
  251. package/.server/server/views/components/tag-env/template.test.js.map +0 -1
  252. /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
  253. /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
  254. /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
  255. /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
  256. /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
  257. /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
  258. /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
  259. /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
  260. /package/.server/server/{views → plugins/engine/views}/summary.html +0 -0
@@ -0,0 +1,277 @@
1
+ import { type Request, type Server } from '@hapi/hapi'
2
+
3
+ import { config } from '~/src/config/index.js'
4
+ import { type FormRequest } from '~/src/server/routes/types.js'
5
+ import { CacheService, merge } from '~/src/server/services/cacheService.js'
6
+
7
+ describe('CacheService', () => {
8
+ let mockServer: Partial<Server>
9
+ let mockCache: {
10
+ get: jest.Mock
11
+ set: jest.Mock
12
+ drop: jest.Mock
13
+ }
14
+ let cacheService: CacheService
15
+
16
+ beforeEach(() => {
17
+ mockCache = {
18
+ get: jest.fn(),
19
+ set: jest.fn(),
20
+ drop: jest.fn()
21
+ }
22
+
23
+ mockServer = {
24
+ log: jest.fn(),
25
+ cache: (() => ({
26
+ ...mockCache,
27
+ provision: jest.fn()
28
+ })) as unknown as Server['cache'],
29
+ logger: {
30
+ info: jest.fn()
31
+ } as unknown as Server['logger']
32
+ }
33
+
34
+ cacheService = new CacheService(mockServer as Server)
35
+ })
36
+
37
+ describe('getState', () => {
38
+ describe('when cache exists', () => {
39
+ it('should return cached state', async () => {
40
+ const mockRequest = {
41
+ yar: { id: 'some-session' },
42
+ params: { state: 'form1', slug: 'page1' }
43
+ } as unknown as FormRequest
44
+ const mockState = { someData: 'test' }
45
+
46
+ mockCache.get.mockResolvedValue(mockState)
47
+
48
+ const result = await cacheService.getState(mockRequest)
49
+
50
+ expect(result).toEqual(mockState)
51
+ expect(mockCache.get).toHaveBeenCalledWith({
52
+ segment: 'cache',
53
+ id: 'some-session:form1:page1:'
54
+ })
55
+ })
56
+ })
57
+
58
+ describe('when cache does not exist', () => {
59
+ it('should return empty object', async () => {
60
+ const mockRequest = {
61
+ yar: { id: 'some-session' },
62
+ params: { state: 'form1', slug: 'page1' }
63
+ } as unknown as FormRequest
64
+
65
+ mockCache.get.mockResolvedValue(null)
66
+
67
+ const result = await cacheService.getState(mockRequest)
68
+
69
+ expect(result).toEqual({})
70
+ })
71
+ })
72
+ })
73
+
74
+ describe('setState', () => {
75
+ it('should set state with correct TTL', async () => {
76
+ const mockRequest = {
77
+ yar: { id: 'some-session' },
78
+ params: { state: 'form1', slug: 'page1' }
79
+ } as unknown as FormRequest
80
+ const mockState = { someData: 'test' }
81
+ const mockTTL = 3600000
82
+
83
+ jest.spyOn(config, 'get').mockReturnValue(mockTTL)
84
+
85
+ await cacheService.setState(mockRequest, mockState)
86
+
87
+ expect(mockCache.set).toHaveBeenCalledWith(
88
+ {
89
+ segment: 'cache',
90
+ id: 'some-session:form1:page1:'
91
+ },
92
+ mockState,
93
+ mockTTL
94
+ )
95
+ })
96
+ })
97
+
98
+ describe('Key', () => {
99
+ describe('when session ID is missing', () => {
100
+ it('should throw error', () => {
101
+ const mockRequest = {
102
+ yar: { id: null },
103
+ params: {}
104
+ } as unknown as Request
105
+
106
+ expect(() => cacheService.Key(mockRequest)).toThrow(
107
+ 'No session ID found'
108
+ )
109
+ })
110
+ })
111
+ })
112
+
113
+ describe('merge', () => {
114
+ describe('when merging objects', () => {
115
+ it('should merge them correctly', () => {
116
+ const state = { field1: 'some-data-1', field2: 'some-data-2' }
117
+ const update = { field2: 'updated', field3: 'some-data-3' }
118
+
119
+ const result = merge(state, update)
120
+
121
+ expect(result).toEqual({
122
+ field1: 'some-data-1',
123
+ field2: 'updated',
124
+ field3: 'some-data-3'
125
+ })
126
+ })
127
+ })
128
+
129
+ describe('when merging arrays', () => {
130
+ it('should overwrite instead of merge', () => {
131
+ const state = { items: ['some-item-1', 'some-item-2'] }
132
+ const update = { items: ['some-item-3'] }
133
+
134
+ const result = merge(state, update)
135
+
136
+ expect(result.items).toEqual(['some-item-3'])
137
+ })
138
+ })
139
+ })
140
+
141
+ describe('getFlash', () => {
142
+ describe('when messages exist', () => {
143
+ it('should return first message', () => {
144
+ const mockRequest = {
145
+ yar: {
146
+ id: 'some-session',
147
+ flash: jest.fn().mockReturnValue([{ errors: [{ type: 'error' }] }])
148
+ },
149
+ params: { state: 'form1', slug: 'page1' }
150
+ } as unknown as FormRequest
151
+
152
+ const result = cacheService.getFlash(mockRequest)
153
+
154
+ expect(result).toEqual({ errors: [{ type: 'error' }] })
155
+ })
156
+ })
157
+
158
+ describe('when no messages exist', () => {
159
+ it('should return undefined', () => {
160
+ const mockRequest = {
161
+ yar: {
162
+ id: 'some-session',
163
+ flash: jest.fn().mockReturnValue([])
164
+ },
165
+ params: { state: 'form1', slug: 'page1' }
166
+ } as unknown as FormRequest
167
+
168
+ const result = cacheService.getFlash(mockRequest)
169
+
170
+ expect(result).toBeUndefined()
171
+ })
172
+ })
173
+ })
174
+
175
+ describe('setFlash', () => {
176
+ it('should set flash message', () => {
177
+ const mockRequest = {
178
+ yar: {
179
+ id: 'some-session',
180
+ flash: jest.fn()
181
+ },
182
+ params: { state: 'form1', slug: 'page1' }
183
+ } as unknown as FormRequest
184
+ const message = {
185
+ errors: [
186
+ {
187
+ type: 'error',
188
+ href: '#error',
189
+ name: 'error',
190
+ text: 'Error message',
191
+ path: ['path-abc']
192
+ }
193
+ ]
194
+ }
195
+
196
+ cacheService.setFlash(mockRequest, message)
197
+
198
+ expect(mockRequest.yar.flash).toHaveBeenCalledWith(
199
+ 'some-session:form1:page1:',
200
+ message
201
+ )
202
+ })
203
+ })
204
+
205
+ describe('clearState', () => {
206
+ it('should clear state from cache', async () => {
207
+ const mockRequest = {
208
+ yar: { id: 'some-session' },
209
+ params: { state: 'form1', slug: 'page1' }
210
+ } as unknown as FormRequest
211
+
212
+ await cacheService.clearState(mockRequest)
213
+
214
+ expect(mockCache.drop).toHaveBeenCalledWith({
215
+ segment: 'cache',
216
+ id: 'some-session:form1:page1:'
217
+ })
218
+ })
219
+ })
220
+
221
+ describe('getConfirmationState', () => {
222
+ it('should return confirmation state from cache', async () => {
223
+ const mockRequest = {
224
+ yar: { id: 'some-session' },
225
+ params: { state: 'form1', slug: 'page1' }
226
+ } as unknown as FormRequest
227
+ const mockState = { confirmed: true as const }
228
+
229
+ mockCache.get.mockResolvedValue(mockState)
230
+
231
+ const result = await cacheService.getConfirmationState(mockRequest)
232
+
233
+ expect(result).toEqual(mockState)
234
+ expect(mockCache.get).toHaveBeenCalledWith({
235
+ segment: 'cache',
236
+ id: 'some-session:form1:page1::confirmation'
237
+ })
238
+ })
239
+
240
+ it('should return empty object when no confirmation state exists', async () => {
241
+ const mockRequest = {
242
+ yar: { id: 'some-session' },
243
+ params: { state: 'form1', slug: 'page1' }
244
+ } as unknown as FormRequest
245
+
246
+ mockCache.get.mockResolvedValue(null)
247
+
248
+ const result = await cacheService.getConfirmationState(mockRequest)
249
+
250
+ expect(result).toEqual({})
251
+ })
252
+ })
253
+
254
+ describe('setConfirmationState', () => {
255
+ it('should set confirmation state in cache', async () => {
256
+ const mockRequest = {
257
+ yar: { id: 'some-session' },
258
+ params: { state: 'form1', slug: 'page1' }
259
+ } as unknown as FormRequest
260
+ const mockState = { confirmed: true as const }
261
+ const mockTTL = 3600000
262
+
263
+ jest.spyOn(config, 'get').mockReturnValue(mockTTL)
264
+
265
+ await cacheService.setConfirmationState(mockRequest, mockState)
266
+
267
+ expect(mockCache.set).toHaveBeenCalledWith(
268
+ {
269
+ segment: 'cache',
270
+ id: 'some-session:form1:page1::confirmation'
271
+ },
272
+ mockState,
273
+ mockTTL
274
+ )
275
+ })
276
+ })
277
+ })
@@ -0,0 +1,138 @@
1
+ import { type Request, type Server } from '@hapi/hapi'
2
+ import * as Hoek from '@hapi/hoek'
3
+
4
+ import { config } from '~/src/config/index.js'
5
+ import { type createServer } from '~/src/server/index.js'
6
+ import {
7
+ type FormPayload,
8
+ type FormState,
9
+ type FormSubmissionError,
10
+ type FormSubmissionState
11
+ } from '~/src/server/plugins/engine/types.js'
12
+ import {
13
+ type FormRequest,
14
+ type FormRequestPayload
15
+ } from '~/src/server/routes/types.js'
16
+
17
+ const partition = 'cache'
18
+
19
+ enum ADDITIONAL_IDENTIFIER {
20
+ Confirmation = ':confirmation'
21
+ }
22
+
23
+ export class CacheService {
24
+ /**
25
+ * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
26
+ */
27
+ cache
28
+ logger: Server['logger']
29
+
30
+ constructor(server: Server, cacheName?: string) {
31
+ if (!cacheName) {
32
+ server.log(
33
+ 'warn',
34
+ 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'
35
+ )
36
+ }
37
+
38
+ this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })
39
+ this.logger = server.logger
40
+ }
41
+
42
+ async getState(
43
+ request: Request | FormRequest | FormRequestPayload
44
+ ): Promise<FormSubmissionState> {
45
+ const cached = await this.cache.get(this.Key(request))
46
+
47
+ return cached || {}
48
+ }
49
+
50
+ async setState(
51
+ request: FormRequest | FormRequestPayload,
52
+ state: FormSubmissionState
53
+ ) {
54
+ const key = this.Key(request)
55
+ const ttl = config.get('sessionTimeout')
56
+
57
+ await this.cache.set(key, state, ttl)
58
+ return this.getState(request)
59
+ }
60
+
61
+ async getConfirmationState(
62
+ request: FormRequest | FormRequestPayload
63
+ ): Promise<{ confirmed?: true }> {
64
+ const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
65
+ const value = await this.cache.get(key)
66
+
67
+ return value || {}
68
+ }
69
+
70
+ async setConfirmationState(
71
+ request: FormRequest | FormRequestPayload,
72
+ confirmationState: { confirmed?: true }
73
+ ) {
74
+ const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
75
+ const ttl = config.get('confirmationSessionTimeout')
76
+
77
+ return this.cache.set(key, confirmationState, ttl)
78
+ }
79
+
80
+ async clearState(request: FormRequest | FormRequestPayload) {
81
+ if (request.yar.id) {
82
+ await this.cache.drop(this.Key(request))
83
+ }
84
+ }
85
+
86
+ getFlash(
87
+ request: FormRequest | FormRequestPayload
88
+ ): { errors: FormSubmissionError[] } | undefined {
89
+ const key = this.Key(request)
90
+ const messages = request.yar.flash(key.id)
91
+
92
+ if (Array.isArray(messages) && messages.length) {
93
+ return messages.at(0) as { errors: FormSubmissionError[] }
94
+ }
95
+ }
96
+
97
+ setFlash(
98
+ request: FormRequest | FormRequestPayload,
99
+ message: { errors: FormSubmissionError[] }
100
+ ) {
101
+ const key = this.Key(request)
102
+
103
+ request.yar.flash(key.id, message)
104
+ }
105
+
106
+ /**
107
+ * The key used to store user session data against.
108
+ * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
109
+ * @param request - hapi request object
110
+ * @param additionalIdentifier - appended to the id
111
+ */
112
+ Key(
113
+ request: Request | FormRequest | FormRequestPayload,
114
+ additionalIdentifier?: ADDITIONAL_IDENTIFIER
115
+ ) {
116
+ if (!request.yar.id) {
117
+ throw Error('No session ID found')
118
+ }
119
+ return {
120
+ segment: partition,
121
+ id: `${request.yar.id}:${request.params.state ?? ''}:${request.params.slug ?? ''}:${additionalIdentifier ?? ''}`
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * State merge helper
128
+ * 1. Merges objects (form fields)
129
+ * 2. Overwrites arrays
130
+ */
131
+ export function merge<StateType extends FormState | FormPayload>(
132
+ state: StateType,
133
+ update: object
134
+ ): StateType {
135
+ return Hoek.merge(state, update, {
136
+ mergeArrays: false
137
+ })
138
+ }