@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.
- package/package.json +3 -2
- package/src/client/javascripts/application.js +87 -0
- package/src/client/javascripts/file-upload.js +386 -0
- package/src/client/stylesheets/_code.scss +33 -0
- package/src/client/stylesheets/_govuk-frontend.scss +4 -0
- package/src/client/stylesheets/_prose.scss +56 -0
- package/src/client/stylesheets/_service-banner.scss +24 -0
- package/src/client/stylesheets/_summary-list.scss +28 -0
- package/src/client/stylesheets/_tag-env.scss +24 -0
- package/src/client/stylesheets/application.scss +14 -0
- package/src/common/cookies.js +58 -0
- package/src/common/cookies.test.js +23 -0
- package/src/common/types.js +5 -0
- package/src/config/index.ts +271 -0
- package/src/index.ts +31 -0
- package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
- package/src/server/common/helpers/logging/logger-options.ts +46 -0
- package/src/server/common/helpers/logging/logger.ts +7 -0
- package/src/server/common/helpers/logging/request-logger.ts +9 -0
- package/src/server/common/helpers/logging/request-tracing.js +10 -0
- package/src/server/common/helpers/redis-client.js +70 -0
- package/src/server/constants.js +1 -0
- package/src/server/forms/README.md +10 -0
- package/src/server/forms/components.json +1015 -0
- package/src/server/forms/report-a-terrorist.json +270 -0
- package/src/server/forms/runner-components-test.json +365 -0
- package/src/server/forms/test.json +581 -0
- package/src/server/index.test.ts +582 -0
- package/src/server/index.ts +140 -0
- package/src/server/plugins/blankie.test.ts +73 -0
- package/src/server/plugins/blankie.ts +48 -0
- package/src/server/plugins/crumb.ts +20 -0
- package/src/server/plugins/engine/README.md +87 -0
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
- package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
- package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
- package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
- package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
- package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
- package/src/server/plugins/engine/components/Details.test.ts +49 -0
- package/src/server/plugins/engine/components/Details.ts +30 -0
- package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
- package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
- package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
- package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
- package/src/server/plugins/engine/components/FormComponent.ts +249 -0
- package/src/server/plugins/engine/components/Html.test.ts +48 -0
- package/src/server/plugins/engine/components/Html.ts +29 -0
- package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
- package/src/server/plugins/engine/components/InsetText.ts +27 -0
- package/src/server/plugins/engine/components/List.test.ts +76 -0
- package/src/server/plugins/engine/components/List.ts +72 -0
- package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
- package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
- package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
- package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
- package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
- package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
- package/src/server/plugins/engine/components/NumberField.ts +163 -0
- package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
- package/src/server/plugins/engine/components/RadiosField.ts +24 -0
- package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
- package/src/server/plugins/engine/components/SelectField.ts +47 -0
- package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
- package/src/server/plugins/engine/components/TextField.test.ts +489 -0
- package/src/server/plugins/engine/components/TextField.ts +96 -0
- package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
- package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
- package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
- package/src/server/plugins/engine/components/YesNoField.ts +31 -0
- package/src/server/plugins/engine/components/constants.ts +1 -0
- package/src/server/plugins/engine/components/helpers.ts +330 -0
- package/src/server/plugins/engine/components/index.ts +24 -0
- package/src/server/plugins/engine/components/types.ts +117 -0
- package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
- package/src/server/plugins/engine/helpers.test.ts +791 -0
- package/src/server/plugins/engine/helpers.ts +379 -0
- package/src/server/plugins/engine/index.ts +7 -0
- package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
- package/src/server/plugins/engine/models/FormModel.ts +443 -0
- package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
- package/src/server/plugins/engine/models/Section.ts +0 -0
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
- package/src/server/plugins/engine/models/index.ts +2 -0
- package/src/server/plugins/engine/models/types.ts +114 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
- package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
- package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1108 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +446 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
- package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +561 -0
- package/src/server/plugins/engine/pageControllers/README.md +28 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +50 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +261 -0
- package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
- package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
- package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
- package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
- package/src/server/plugins/engine/pageControllers/index.ts +10 -0
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
- package/src/server/plugins/engine/plugin.ts +673 -0
- package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
- package/src/server/plugins/engine/services/formsService.js +46 -0
- package/src/server/plugins/engine/services/formsService.test.js +90 -0
- package/src/server/plugins/engine/services/index.js +3 -0
- package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
- package/src/server/plugins/engine/services/notifyService.ts +64 -0
- package/src/server/plugins/engine/services/uploadService.js +60 -0
- package/src/server/plugins/engine/types.ts +315 -0
- package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
- package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
- package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
- package/src/server/plugins/engine/views/components/details.html +6 -0
- package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
- package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
- package/src/server/plugins/engine/views/components/html.html +3 -0
- package/src/server/plugins/engine/views/components/insettext.html +7 -0
- package/src/server/plugins/engine/views/components/list.html +36 -0
- package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
- package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
- package/src/server/plugins/engine/views/components/numberfield.html +5 -0
- package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
- package/src/server/plugins/engine/views/components/selectfield.html +5 -0
- package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
- package/src/server/plugins/engine/views/components/textfield.html +5 -0
- package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
- package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
- package/src/server/plugins/engine/views/file-upload.html +45 -0
- package/src/server/plugins/engine/views/index.html +39 -0
- package/src/server/plugins/engine/views/item-delete.html +56 -0
- package/src/server/plugins/engine/views/partials/components.html +6 -0
- package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
- package/src/server/plugins/engine/views/partials/debug.html +44 -0
- package/src/server/plugins/engine/views/partials/form.html +15 -0
- package/src/server/plugins/engine/views/partials/heading.html +16 -0
- package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
- package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
- package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
- package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
- package/src/server/plugins/errorPages.ts +58 -0
- package/src/server/plugins/nunjucks/context.js +88 -0
- package/src/server/plugins/nunjucks/context.test.js +142 -0
- package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
- package/src/server/plugins/nunjucks/environment.js +116 -0
- package/src/server/plugins/nunjucks/filters/answer.js +27 -0
- package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
- package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
- package/src/server/plugins/nunjucks/filters/field.js +28 -0
- package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
- package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
- package/src/server/plugins/nunjucks/filters/href.js +30 -0
- package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
- package/src/server/plugins/nunjucks/filters/index.js +8 -0
- package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
- package/src/server/plugins/nunjucks/filters/page.js +24 -0
- package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
- package/src/server/plugins/nunjucks/index.js +3 -0
- package/src/server/plugins/nunjucks/plugin.js +40 -0
- package/src/server/plugins/nunjucks/render.js +42 -0
- package/src/server/plugins/nunjucks/types.js +40 -0
- package/src/server/plugins/pulse.ts +11 -0
- package/src/server/plugins/router.ts +201 -0
- package/src/server/plugins/session.ts +28 -0
- package/src/server/routes/health.js +13 -0
- package/src/server/routes/health.test.js +35 -0
- package/src/server/routes/index.test.ts +125 -0
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/public.ts +47 -0
- package/src/server/routes/types.ts +48 -0
- package/src/server/schemas/index.ts +34 -0
- package/src/server/secure-context.js +43 -0
- package/src/server/services/cacheService.test.ts +276 -0
- package/src/server/services/cacheService.ts +131 -0
- package/src/server/services/httpService.test.js +491 -0
- package/src/server/services/httpService.ts +50 -0
- package/src/server/services/index.ts +1 -0
- package/src/server/types.ts +54 -0
- package/src/server/utils/notify.test.ts +37 -0
- package/src/server/utils/notify.ts +50 -0
- package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
- package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
- package/src/server/utils/utils.js +24 -0
- package/src/server/utils/utils.test.js +54 -0
- package/src/server/views/404.html +16 -0
- package/src/server/views/500.html +19 -0
- package/src/server/views/components/debug/macro.njk +3 -0
- package/src/server/views/components/debug/template.njk +13 -0
- package/src/server/views/components/service-banner/macro.njk +3 -0
- package/src/server/views/components/service-banner/template.njk +20 -0
- package/src/server/views/components/service-banner/template.test.js +43 -0
- package/src/server/views/components/tag-env/macro.njk +3 -0
- package/src/server/views/components/tag-env/template.njk +30 -0
- package/src/server/views/components/tag-env/template.test.js +66 -0
- package/src/server/views/confirmation.html +19 -0
- package/src/server/views/help/accessibility-statement.html +58 -0
- package/src/server/views/help/cookie-preferences.html +57 -0
- package/src/server/views/help/cookies.html +71 -0
- package/src/server/views/help/get-support.html +37 -0
- package/src/server/views/help/privacy-notice.html +68 -0
- package/src/server/views/help/terms-and-conditions.html +83 -0
- package/src/server/views/layout.html +199 -0
- package/src/server/views/summary.html +50 -0
- package/src/typings/hapi/index.d.ts +95 -0
- package/src/typings/hapi-tracing/index.d.ts +6 -0
- package/src/typings/index.d.ts +3 -0
- package/src/typings/joi/index.d.ts +22 -0
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/dot-notation */
|
|
2
|
+
import { ComponentType, type ComponentDef } from '@defra/forms-model'
|
|
3
|
+
import { type ResponseToolkit } from '@hapi/hapi'
|
|
4
|
+
import { type ValidationErrorItem, type ValidationResult } from 'joi'
|
|
5
|
+
|
|
6
|
+
import { tempItemSchema } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
7
|
+
import { getError } from '~/src/server/plugins/engine/helpers.js'
|
|
8
|
+
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
9
|
+
import {
|
|
10
|
+
FileUploadPageController,
|
|
11
|
+
prepareStatus
|
|
12
|
+
} from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
|
|
13
|
+
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
|
|
14
|
+
import * as pageHelpers from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
15
|
+
import * as uploadService from '~/src/server/plugins/engine/services/uploadService.js'
|
|
16
|
+
import {
|
|
17
|
+
FileStatus,
|
|
18
|
+
UploadStatus,
|
|
19
|
+
type FeaturedFormPageViewModel,
|
|
20
|
+
type FormContext,
|
|
21
|
+
type FormContextRequest,
|
|
22
|
+
type FormParams,
|
|
23
|
+
type FormSubmissionState,
|
|
24
|
+
type UploadStatusFileResponse,
|
|
25
|
+
type UploadStatusResponse
|
|
26
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
27
|
+
import {
|
|
28
|
+
type FormRequest,
|
|
29
|
+
type FormRequestPayload
|
|
30
|
+
} from '~/src/server/routes/types.js'
|
|
31
|
+
import definition from '~/test/form/definitions/file-upload-basic.js'
|
|
32
|
+
|
|
33
|
+
type TestableFileUploadPageController = FileUploadPageController & {
|
|
34
|
+
initiateAndStoreNewUpload(
|
|
35
|
+
req: FormRequest,
|
|
36
|
+
state: FormSubmissionState
|
|
37
|
+
): Promise<FormSubmissionState>
|
|
38
|
+
mergeState(
|
|
39
|
+
req: FormRequest,
|
|
40
|
+
state: FormSubmissionState,
|
|
41
|
+
merge: object
|
|
42
|
+
): Promise<FormSubmissionState>
|
|
43
|
+
checkUploadStatus(
|
|
44
|
+
request: FormRequest,
|
|
45
|
+
state: FormSubmissionState,
|
|
46
|
+
depth?: number
|
|
47
|
+
): Promise<FormSubmissionState>
|
|
48
|
+
prepareStatus(status: UploadStatusFileResponse): UploadStatusFileResponse
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('FileUploadPageController', () => {
|
|
52
|
+
let model: FormModel
|
|
53
|
+
let controller: FileUploadPageController
|
|
54
|
+
let request: FormRequest
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
const { pages } = structuredClone(definition)
|
|
58
|
+
|
|
59
|
+
model = new FormModel(definition, {
|
|
60
|
+
basePath: 'test'
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
controller = new FileUploadPageController(model, pages[0])
|
|
64
|
+
request = {
|
|
65
|
+
logger: {
|
|
66
|
+
info: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
fatal: jest.fn(),
|
|
69
|
+
warn: jest.fn(),
|
|
70
|
+
debug: jest.fn(),
|
|
71
|
+
trace: jest.fn(),
|
|
72
|
+
level: 'info'
|
|
73
|
+
},
|
|
74
|
+
services: jest.fn().mockReturnValue({
|
|
75
|
+
cacheService: {
|
|
76
|
+
setFlash: jest.fn(),
|
|
77
|
+
setState: jest
|
|
78
|
+
.fn()
|
|
79
|
+
.mockImplementation((req, updated) => Promise.resolve(updated))
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
query: {}
|
|
83
|
+
} as unknown as FormRequest
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
jest.restoreAllMocks()
|
|
88
|
+
jest.clearAllMocks()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('Constructor', () => {
|
|
92
|
+
const textComponent: ComponentDef = {
|
|
93
|
+
name: 'fullName',
|
|
94
|
+
title: 'Full name',
|
|
95
|
+
type: ComponentType.TextField,
|
|
96
|
+
options: {},
|
|
97
|
+
schema: {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it('throws unless there is exactly 1 file upload component', () => {
|
|
101
|
+
const { pages } = structuredClone(definition)
|
|
102
|
+
|
|
103
|
+
// @ts-expect-error - Allow invalid component for test
|
|
104
|
+
pages[0].components = [textComponent]
|
|
105
|
+
|
|
106
|
+
expect(() => new FileUploadPageController(model, pages[0])).toThrow(
|
|
107
|
+
`Expected 1 FileUploadFieldComponent in FileUploadPageController '${pages[0].path}'`
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('throws unless file upload component is the first in the form', () => {
|
|
112
|
+
const { pages } = structuredClone(definition)
|
|
113
|
+
|
|
114
|
+
// @ts-expect-error - Allow invalid component for test
|
|
115
|
+
pages[0].components.unshift(textComponent)
|
|
116
|
+
|
|
117
|
+
expect(() => new FileUploadPageController(model, pages[0])).toThrow(
|
|
118
|
+
`Expected 'fileUpload' to be the first form component in FileUploadPageController '${pages[0].path}'`
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Form validation', () => {
|
|
124
|
+
it('includes title text and error', () => {
|
|
125
|
+
const result = controller.collection.validate()
|
|
126
|
+
|
|
127
|
+
expect(result.errors).toEqual([
|
|
128
|
+
{
|
|
129
|
+
path: ['fileUpload'],
|
|
130
|
+
href: '#fileUpload',
|
|
131
|
+
name: 'fileUpload',
|
|
132
|
+
text: 'Select upload something',
|
|
133
|
+
context: {
|
|
134
|
+
key: 'fileUpload',
|
|
135
|
+
label: 'Upload something',
|
|
136
|
+
title: 'Upload something'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('includes all field errors', () => {
|
|
143
|
+
const result = controller.collection.validate()
|
|
144
|
+
expect(result.errors).toHaveLength(1)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('checkUploadStatus', () => {
|
|
149
|
+
describe('error handling', () => {
|
|
150
|
+
it('throws error when getUploadStatus returns empty response', async () => {
|
|
151
|
+
const state = {
|
|
152
|
+
upload: {
|
|
153
|
+
[controller.path]: {
|
|
154
|
+
upload: {
|
|
155
|
+
uploadId: 'some-id',
|
|
156
|
+
uploadUrl: 'some-url',
|
|
157
|
+
statusUrl: 'some-status-url'
|
|
158
|
+
},
|
|
159
|
+
files: []
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} as unknown as FormSubmissionState
|
|
163
|
+
|
|
164
|
+
jest
|
|
165
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
166
|
+
.mockResolvedValue(undefined)
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
controller['checkUploadStatus'](request, state, 1)
|
|
170
|
+
).rejects.toThrow(
|
|
171
|
+
'Unexpected empty response from getUploadStatus for some-id'
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('handles pending upload with backoff and retries', async () => {
|
|
176
|
+
const state = {
|
|
177
|
+
upload: {
|
|
178
|
+
[controller.path]: {
|
|
179
|
+
upload: {
|
|
180
|
+
uploadId: 'some-id',
|
|
181
|
+
uploadUrl: 'some-url',
|
|
182
|
+
statusUrl: 'some-status-url'
|
|
183
|
+
},
|
|
184
|
+
files: []
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} as unknown as FormSubmissionState
|
|
188
|
+
|
|
189
|
+
const pendingStatus = {
|
|
190
|
+
uploadStatus: UploadStatus.pending,
|
|
191
|
+
form: { file: { fileStatus: FileStatus.complete } }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const getUploadStatusSpy = jest
|
|
195
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
196
|
+
.mockResolvedValueOnce(pendingStatus as UploadStatusResponse)
|
|
197
|
+
.mockResolvedValueOnce({
|
|
198
|
+
uploadStatus: UploadStatus.initiated
|
|
199
|
+
} as UploadStatusResponse)
|
|
200
|
+
|
|
201
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
202
|
+
|
|
203
|
+
expect(getUploadStatusSpy).toHaveBeenCalledTimes(2)
|
|
204
|
+
expect(request.logger.info).toHaveBeenCalled()
|
|
205
|
+
|
|
206
|
+
const logMsg = (request.logger.info as jest.Mock).mock.calls[0][0]
|
|
207
|
+
expect(logMsg).toEqual(expect.stringContaining('Waiting'))
|
|
208
|
+
expect(logMsg).toEqual(expect.stringContaining('some-id'))
|
|
209
|
+
}, 3000)
|
|
210
|
+
|
|
211
|
+
it('throws gateway timeout when maximum retry depth is exceeded, logs an error, and re-initiates a new upload', async () => {
|
|
212
|
+
const state = {
|
|
213
|
+
upload: {
|
|
214
|
+
[controller.path]: {
|
|
215
|
+
upload: {
|
|
216
|
+
uploadId: 'some-id',
|
|
217
|
+
uploadUrl: 'some-url',
|
|
218
|
+
statusUrl: 'some-status-url'
|
|
219
|
+
},
|
|
220
|
+
files: []
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} as unknown as FormSubmissionState
|
|
224
|
+
|
|
225
|
+
const pendingStatus = {
|
|
226
|
+
uploadStatus: UploadStatus.pending,
|
|
227
|
+
form: { file: { fileStatus: FileStatus.pending } }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
jest
|
|
231
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
232
|
+
.mockResolvedValue(pendingStatus as UploadStatusResponse)
|
|
233
|
+
|
|
234
|
+
const initiateSpy = jest
|
|
235
|
+
.spyOn(
|
|
236
|
+
controller as TestableFileUploadPageController,
|
|
237
|
+
'initiateAndStoreNewUpload'
|
|
238
|
+
)
|
|
239
|
+
.mockResolvedValue(state as never)
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
controller['checkUploadStatus'](request, state, 7)
|
|
243
|
+
).rejects.toThrow(
|
|
244
|
+
'Timed out waiting for some-id after cumulative retries exceeding 55 seconds'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
expect(request.logger.error).toHaveBeenCalledWith(
|
|
248
|
+
expect.stringContaining(
|
|
249
|
+
'Exceeded cumulative retry delay for some-id (depth: 7). Re-initiating a new upload.'
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
expect(initiateSpy).toHaveBeenCalledWith(request, state)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('throws error when initiateUpload returns undefined', async () => {
|
|
257
|
+
const state = {
|
|
258
|
+
upload: {
|
|
259
|
+
[controller.path]: {
|
|
260
|
+
upload: {},
|
|
261
|
+
files: []
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} as unknown as FormSubmissionState
|
|
265
|
+
|
|
266
|
+
jest.spyOn(uploadService, 'initiateUpload').mockResolvedValue(undefined)
|
|
267
|
+
|
|
268
|
+
await expect(
|
|
269
|
+
controller['checkUploadStatus'](request, state, 1)
|
|
270
|
+
).rejects.toThrow('Unexpected empty response from initiateUpload')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('handles pending file status with custom error message', async () => {
|
|
274
|
+
const state = {
|
|
275
|
+
upload: {
|
|
276
|
+
[controller.path]: {
|
|
277
|
+
upload: {
|
|
278
|
+
uploadId: 'some-id',
|
|
279
|
+
uploadUrl: 'some-url',
|
|
280
|
+
statusUrl: 'some-status-url'
|
|
281
|
+
},
|
|
282
|
+
files: []
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} as unknown as FormSubmissionState
|
|
286
|
+
|
|
287
|
+
const pendingStatus = {
|
|
288
|
+
uploadStatus: UploadStatus.ready,
|
|
289
|
+
form: {
|
|
290
|
+
file: {
|
|
291
|
+
fileStatus: FileStatus.pending,
|
|
292
|
+
errorMessage: 'Custom error message'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
jest
|
|
298
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
299
|
+
.mockResolvedValue(pendingStatus as UploadStatusResponse)
|
|
300
|
+
|
|
301
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
302
|
+
value: {
|
|
303
|
+
uploadId: 'some-id',
|
|
304
|
+
status: pendingStatus,
|
|
305
|
+
type: 'object.unknown',
|
|
306
|
+
path: ['fileUpload', 'errorMessage'],
|
|
307
|
+
context: { value: 'Custom error message' }
|
|
308
|
+
},
|
|
309
|
+
error: undefined
|
|
310
|
+
} as ValidationResult)
|
|
311
|
+
|
|
312
|
+
const testController = controller as TestableFileUploadPageController
|
|
313
|
+
const initiateSpy = jest.spyOn(
|
|
314
|
+
testController,
|
|
315
|
+
'initiateAndStoreNewUpload'
|
|
316
|
+
)
|
|
317
|
+
initiateSpy.mockResolvedValue(state as never)
|
|
318
|
+
|
|
319
|
+
const { cacheService } = request.services([])
|
|
320
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
321
|
+
|
|
322
|
+
expect(cacheService.setFlash).toHaveBeenCalledWith(request, {
|
|
323
|
+
errors: [
|
|
324
|
+
{
|
|
325
|
+
path: ['fileUpload'],
|
|
326
|
+
href: '#fileUpload',
|
|
327
|
+
name: 'fileUpload',
|
|
328
|
+
text: 'Custom error message'
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('state management', () => {
|
|
336
|
+
it('returns existing state when upload status is initiated', async () => {
|
|
337
|
+
const state = {
|
|
338
|
+
upload: {
|
|
339
|
+
[controller.path]: {
|
|
340
|
+
upload: {
|
|
341
|
+
uploadId: 'some-id',
|
|
342
|
+
uploadUrl: 'some-url',
|
|
343
|
+
statusUrl: 'some-status-url'
|
|
344
|
+
},
|
|
345
|
+
files: []
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} as unknown as FormSubmissionState
|
|
349
|
+
|
|
350
|
+
jest.spyOn(uploadService, 'getUploadStatus').mockResolvedValue({
|
|
351
|
+
uploadStatus: UploadStatus.initiated
|
|
352
|
+
} as UploadStatusResponse)
|
|
353
|
+
const result = await controller['checkUploadStatus'](request, state, 1)
|
|
354
|
+
expect(result).toBe(state)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('returns early when all files are updated', async () => {
|
|
358
|
+
const files = ['file1', 'file2']
|
|
359
|
+
const filesUpdated = [...files]
|
|
360
|
+
const state = {
|
|
361
|
+
upload: {
|
|
362
|
+
[controller.path]: {
|
|
363
|
+
upload: {
|
|
364
|
+
uploadId: 'some-id',
|
|
365
|
+
uploadUrl: 'some-url',
|
|
366
|
+
statusUrl: 'some-status-url'
|
|
367
|
+
},
|
|
368
|
+
files,
|
|
369
|
+
filesUpdated
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} as unknown as FormSubmissionState
|
|
373
|
+
|
|
374
|
+
const readyStatus = {
|
|
375
|
+
uploadStatus: UploadStatus.ready,
|
|
376
|
+
form: { file: { fileStatus: FileStatus.complete } }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
jest
|
|
380
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
381
|
+
.mockResolvedValue(readyStatus as UploadStatusResponse)
|
|
382
|
+
|
|
383
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
384
|
+
value: { status: readyStatus },
|
|
385
|
+
error: undefined
|
|
386
|
+
} as ValidationResult)
|
|
387
|
+
|
|
388
|
+
const testController = controller as TestableFileUploadPageController
|
|
389
|
+
const initiateSpy = jest.spyOn(
|
|
390
|
+
testController,
|
|
391
|
+
'initiateAndStoreNewUpload'
|
|
392
|
+
) as jest.SpyInstance<
|
|
393
|
+
Promise<FormSubmissionState>,
|
|
394
|
+
[FormRequest, FormSubmissionState]
|
|
395
|
+
>
|
|
396
|
+
|
|
397
|
+
initiateSpy.mockResolvedValue(state)
|
|
398
|
+
|
|
399
|
+
const result = await controller['checkUploadStatus'](request, state, 1)
|
|
400
|
+
|
|
401
|
+
expect(result).toBe(state)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('initiates new upload when no upload exists', async () => {
|
|
405
|
+
const state = {
|
|
406
|
+
upload: {
|
|
407
|
+
[controller.path]: {
|
|
408
|
+
upload: {},
|
|
409
|
+
files: []
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} as unknown as FormSubmissionState
|
|
413
|
+
|
|
414
|
+
const testController = controller as TestableFileUploadPageController
|
|
415
|
+
|
|
416
|
+
const initiateSpy = jest.spyOn(
|
|
417
|
+
testController,
|
|
418
|
+
'initiateAndStoreNewUpload'
|
|
419
|
+
) as jest.SpyInstance<
|
|
420
|
+
Promise<FormSubmissionState>,
|
|
421
|
+
[FormRequest, FormSubmissionState]
|
|
422
|
+
>
|
|
423
|
+
|
|
424
|
+
initiateSpy.mockImplementation(
|
|
425
|
+
(_req: FormRequest, s: FormSubmissionState) =>
|
|
426
|
+
Promise.resolve(Object.assign({}, s, { initiated: true }))
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
const result = await controller['checkUploadStatus'](request, state, 1)
|
|
430
|
+
|
|
431
|
+
expect(initiateSpy).toHaveBeenCalled()
|
|
432
|
+
expect(result.initiated).toBe(true)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('initiates new upload when file validation fails', async () => {
|
|
436
|
+
const state = {
|
|
437
|
+
upload: {
|
|
438
|
+
[controller.path]: {
|
|
439
|
+
upload: {
|
|
440
|
+
uploadId: 'some-id',
|
|
441
|
+
uploadUrl: 'some-url',
|
|
442
|
+
statusUrl: 'some-status-url'
|
|
443
|
+
},
|
|
444
|
+
files: []
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} as unknown as FormSubmissionState
|
|
448
|
+
|
|
449
|
+
jest.spyOn(uploadService, 'getUploadStatus').mockResolvedValue({
|
|
450
|
+
uploadStatus: UploadStatus.ready,
|
|
451
|
+
form: { file: { fileStatus: FileStatus.complete } }
|
|
452
|
+
} as UploadStatusResponse)
|
|
453
|
+
|
|
454
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
455
|
+
value: {},
|
|
456
|
+
error: new Error('Validation failed')
|
|
457
|
+
} as ValidationResult)
|
|
458
|
+
|
|
459
|
+
const testController = controller as TestableFileUploadPageController
|
|
460
|
+
|
|
461
|
+
const initiateSpy = jest.spyOn(
|
|
462
|
+
testController,
|
|
463
|
+
'initiateAndStoreNewUpload'
|
|
464
|
+
) as jest.SpyInstance<
|
|
465
|
+
Promise<FormSubmissionState>,
|
|
466
|
+
[FormRequest, FormSubmissionState]
|
|
467
|
+
>
|
|
468
|
+
|
|
469
|
+
initiateSpy.mockImplementation(
|
|
470
|
+
(
|
|
471
|
+
_req: FormRequest,
|
|
472
|
+
s: FormSubmissionState
|
|
473
|
+
): Promise<FormSubmissionState> =>
|
|
474
|
+
Promise.resolve(Object.assign({}, s, { newUpload: true }))
|
|
475
|
+
)
|
|
476
|
+
const result = await controller['checkUploadStatus'](request, state, 1)
|
|
477
|
+
|
|
478
|
+
expect(initiateSpy).toHaveBeenCalled()
|
|
479
|
+
expect(result.newUpload).toBe(true)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('merges state when file upload is complete', async () => {
|
|
483
|
+
const state = {
|
|
484
|
+
upload: {
|
|
485
|
+
[controller.path]: {
|
|
486
|
+
upload: {
|
|
487
|
+
uploadId: 'some-id',
|
|
488
|
+
uploadUrl: 'some-url',
|
|
489
|
+
statusUrl: 'some-status-url'
|
|
490
|
+
},
|
|
491
|
+
files: []
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} as unknown as FormSubmissionState
|
|
495
|
+
|
|
496
|
+
const completeStatus = {
|
|
497
|
+
uploadStatus: UploadStatus.ready,
|
|
498
|
+
form: { file: { fileStatus: FileStatus.complete } }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
jest
|
|
502
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
503
|
+
.mockResolvedValue(completeStatus as UploadStatusResponse)
|
|
504
|
+
|
|
505
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
506
|
+
value: {
|
|
507
|
+
status: completeStatus,
|
|
508
|
+
uploadId: 'some-id'
|
|
509
|
+
},
|
|
510
|
+
error: undefined
|
|
511
|
+
} as ValidationResult)
|
|
512
|
+
|
|
513
|
+
const testController = controller as TestableFileUploadPageController
|
|
514
|
+
|
|
515
|
+
const mergeStateSpy = jest.spyOn(
|
|
516
|
+
testController,
|
|
517
|
+
'mergeState'
|
|
518
|
+
) as jest.SpyInstance<
|
|
519
|
+
Promise<FormSubmissionState>,
|
|
520
|
+
[FormRequest, FormSubmissionState, object]
|
|
521
|
+
>
|
|
522
|
+
|
|
523
|
+
mergeStateSpy.mockImplementation(
|
|
524
|
+
(
|
|
525
|
+
_req: FormRequest,
|
|
526
|
+
s: FormSubmissionState,
|
|
527
|
+
_merge: object
|
|
528
|
+
): Promise<FormSubmissionState> =>
|
|
529
|
+
Promise.resolve(Object.assign({}, s, { merged: true }))
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
const initiateSpy = jest.spyOn(
|
|
533
|
+
testController,
|
|
534
|
+
'initiateAndStoreNewUpload'
|
|
535
|
+
) as jest.SpyInstance<
|
|
536
|
+
Promise<FormSubmissionState>,
|
|
537
|
+
[FormRequest, FormSubmissionState]
|
|
538
|
+
>
|
|
539
|
+
|
|
540
|
+
initiateSpy.mockImplementation(
|
|
541
|
+
(
|
|
542
|
+
_req: FormRequest,
|
|
543
|
+
s: FormSubmissionState
|
|
544
|
+
): Promise<FormSubmissionState> =>
|
|
545
|
+
Promise.resolve(Object.assign({}, s, { newUpload: true }))
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
const result = await controller['checkUploadStatus'](request, state, 1)
|
|
549
|
+
|
|
550
|
+
expect(mergeStateSpy).toHaveBeenCalled()
|
|
551
|
+
expect(result.newUpload).toBe(true)
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
describe('error messaging', () => {
|
|
556
|
+
describe('when file status is not complete', () => {
|
|
557
|
+
it('sets flash error with provided message', async () => {
|
|
558
|
+
const state = {
|
|
559
|
+
upload: {
|
|
560
|
+
[controller.path]: {
|
|
561
|
+
upload: {
|
|
562
|
+
uploadId: 'some-id',
|
|
563
|
+
uploadUrl: 'some-url',
|
|
564
|
+
statusUrl: 'some-status-url'
|
|
565
|
+
},
|
|
566
|
+
files: []
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} as unknown as FormSubmissionState
|
|
570
|
+
|
|
571
|
+
const errorStatus = {
|
|
572
|
+
uploadStatus: UploadStatus.ready,
|
|
573
|
+
form: {
|
|
574
|
+
file: {
|
|
575
|
+
fileStatus: FileStatus.rejected,
|
|
576
|
+
errorMessage: 'Test error'
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
jest
|
|
582
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
583
|
+
.mockResolvedValue(errorStatus as UploadStatusResponse)
|
|
584
|
+
|
|
585
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
586
|
+
value: {
|
|
587
|
+
status: errorStatus,
|
|
588
|
+
uploadId: 'some-id'
|
|
589
|
+
},
|
|
590
|
+
error: undefined
|
|
591
|
+
} as ValidationResult)
|
|
592
|
+
|
|
593
|
+
const testController = controller as TestableFileUploadPageController
|
|
594
|
+
|
|
595
|
+
const initiateSpy = jest.spyOn(
|
|
596
|
+
testController,
|
|
597
|
+
'initiateAndStoreNewUpload'
|
|
598
|
+
) as jest.SpyInstance<
|
|
599
|
+
Promise<FormSubmissionState>,
|
|
600
|
+
[FormRequest, FormSubmissionState]
|
|
601
|
+
>
|
|
602
|
+
|
|
603
|
+
initiateSpy.mockImplementation(
|
|
604
|
+
(
|
|
605
|
+
_req: FormRequest,
|
|
606
|
+
s: FormSubmissionState
|
|
607
|
+
): Promise<FormSubmissionState> =>
|
|
608
|
+
Promise.resolve(Object.assign({}, s, { newUpload: true }))
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const { cacheService } = request.services([])
|
|
612
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
613
|
+
|
|
614
|
+
expect(cacheService.setFlash).toHaveBeenCalledWith(request, {
|
|
615
|
+
errors: [
|
|
616
|
+
{
|
|
617
|
+
path: ['fileUpload'],
|
|
618
|
+
href: '#fileUpload',
|
|
619
|
+
name: 'fileUpload',
|
|
620
|
+
text: 'Test error'
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
describe('when file has error status', () => {
|
|
628
|
+
it('sets flash error with error message', async () => {
|
|
629
|
+
const state = {
|
|
630
|
+
upload: {
|
|
631
|
+
[controller.path]: {
|
|
632
|
+
upload: {
|
|
633
|
+
uploadId: 'some-id',
|
|
634
|
+
uploadUrl: 'some-url',
|
|
635
|
+
statusUrl: 'some-status-url'
|
|
636
|
+
},
|
|
637
|
+
files: []
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} as unknown as FormSubmissionState
|
|
641
|
+
|
|
642
|
+
const errorStatus = {
|
|
643
|
+
uploadStatus: UploadStatus.ready,
|
|
644
|
+
form: {
|
|
645
|
+
file: {
|
|
646
|
+
fileStatus: FileStatus.rejected,
|
|
647
|
+
errorMessage: 'Test error message'
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
jest
|
|
653
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
654
|
+
.mockResolvedValue(errorStatus as UploadStatusResponse)
|
|
655
|
+
|
|
656
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
657
|
+
value: { status: errorStatus },
|
|
658
|
+
error: undefined
|
|
659
|
+
} as ValidationResult)
|
|
660
|
+
|
|
661
|
+
const testController = controller as TestableFileUploadPageController
|
|
662
|
+
|
|
663
|
+
const initiateSpy = jest.spyOn(
|
|
664
|
+
testController,
|
|
665
|
+
'initiateAndStoreNewUpload'
|
|
666
|
+
) as jest.SpyInstance<
|
|
667
|
+
Promise<FormSubmissionState>,
|
|
668
|
+
[FormRequest, FormSubmissionState]
|
|
669
|
+
>
|
|
670
|
+
|
|
671
|
+
initiateSpy.mockResolvedValue(state)
|
|
672
|
+
|
|
673
|
+
const { cacheService } = request.services([])
|
|
674
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
675
|
+
|
|
676
|
+
expect(cacheService.setFlash).toHaveBeenCalledWith(request, {
|
|
677
|
+
errors: [
|
|
678
|
+
{
|
|
679
|
+
path: ['fileUpload'],
|
|
680
|
+
href: '#fileUpload',
|
|
681
|
+
name: 'fileUpload',
|
|
682
|
+
text: 'Test error message'
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
it('sets default error message when none provided', async () => {
|
|
689
|
+
const state = {
|
|
690
|
+
upload: {
|
|
691
|
+
[controller.path]: {
|
|
692
|
+
upload: {
|
|
693
|
+
uploadId: 'some-id',
|
|
694
|
+
uploadUrl: 'some-url',
|
|
695
|
+
statusUrl: 'some-status-url'
|
|
696
|
+
},
|
|
697
|
+
files: []
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
} as unknown as FormSubmissionState
|
|
701
|
+
|
|
702
|
+
const errorStatus = {
|
|
703
|
+
uploadStatus: UploadStatus.ready,
|
|
704
|
+
form: {
|
|
705
|
+
file: {
|
|
706
|
+
fileStatus: FileStatus.rejected
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
jest
|
|
712
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
713
|
+
.mockResolvedValue(errorStatus as UploadStatusResponse)
|
|
714
|
+
|
|
715
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
716
|
+
value: { status: errorStatus },
|
|
717
|
+
error: undefined
|
|
718
|
+
} as ValidationResult)
|
|
719
|
+
|
|
720
|
+
const testController = controller as TestableFileUploadPageController
|
|
721
|
+
|
|
722
|
+
const initiateSpy = jest.spyOn(
|
|
723
|
+
testController,
|
|
724
|
+
'initiateAndStoreNewUpload'
|
|
725
|
+
) as jest.SpyInstance<
|
|
726
|
+
Promise<FormSubmissionState>,
|
|
727
|
+
[FormRequest, FormSubmissionState]
|
|
728
|
+
>
|
|
729
|
+
|
|
730
|
+
initiateSpy.mockResolvedValue(state)
|
|
731
|
+
|
|
732
|
+
const { cacheService } = request.services([])
|
|
733
|
+
|
|
734
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
735
|
+
|
|
736
|
+
expect(cacheService.setFlash).toHaveBeenCalledWith(request, {
|
|
737
|
+
errors: [
|
|
738
|
+
{
|
|
739
|
+
path: ['fileUpload'],
|
|
740
|
+
href: '#fileUpload',
|
|
741
|
+
name: 'fileUpload',
|
|
742
|
+
text: 'Unknown error'
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
describe('file removal', () => {
|
|
751
|
+
it('returns early when no file is removed', async () => {
|
|
752
|
+
const files = [{ uploadId: 'file1' }, { uploadId: 'file2' }]
|
|
753
|
+
|
|
754
|
+
Object.defineProperty(request, 'params', {
|
|
755
|
+
value: { itemId: 'nonexistent-file' },
|
|
756
|
+
writable: true,
|
|
757
|
+
configurable: true
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
const state = {
|
|
761
|
+
upload: {
|
|
762
|
+
[controller.path]: {
|
|
763
|
+
upload: {
|
|
764
|
+
uploadId: 'upload-123',
|
|
765
|
+
uploadUrl: 'some-url',
|
|
766
|
+
statusUrl: 'some-status-url'
|
|
767
|
+
},
|
|
768
|
+
files
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} as unknown as FormSubmissionState
|
|
772
|
+
|
|
773
|
+
const testController = controller as TestableFileUploadPageController
|
|
774
|
+
const mergeStateSpy = jest.spyOn(testController, 'mergeState')
|
|
775
|
+
|
|
776
|
+
await controller['checkRemovedFiles'](
|
|
777
|
+
request as FormRequestPayload,
|
|
778
|
+
state
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
expect(mergeStateSpy).not.toHaveBeenCalled()
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('merges state when file is removed', async () => {
|
|
785
|
+
const files = [{ uploadId: 'file1' }, { uploadId: 'file2' }]
|
|
786
|
+
|
|
787
|
+
Object.defineProperty(request, 'params', {
|
|
788
|
+
value: { itemId: 'file1' },
|
|
789
|
+
writable: true,
|
|
790
|
+
configurable: true
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
const state = {
|
|
794
|
+
upload: {
|
|
795
|
+
[controller.path]: {
|
|
796
|
+
upload: {
|
|
797
|
+
uploadId: 'upload-123',
|
|
798
|
+
uploadUrl: 'some-url',
|
|
799
|
+
statusUrl: 'some-status-url'
|
|
800
|
+
},
|
|
801
|
+
files
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} as unknown as FormSubmissionState
|
|
805
|
+
|
|
806
|
+
const testController = controller as TestableFileUploadPageController
|
|
807
|
+
const mergeStateSpy = jest.spyOn(testController, 'mergeState')
|
|
808
|
+
|
|
809
|
+
await controller['checkRemovedFiles'](
|
|
810
|
+
request as FormRequestPayload,
|
|
811
|
+
state
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
expect(mergeStateSpy).toHaveBeenCalledWith(request, state, {
|
|
815
|
+
upload: {
|
|
816
|
+
[controller.path]: {
|
|
817
|
+
files: [{ uploadId: 'file2' }],
|
|
818
|
+
upload: {
|
|
819
|
+
uploadId: 'upload-123',
|
|
820
|
+
uploadUrl: 'some-url',
|
|
821
|
+
statusUrl: 'some-status-url'
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
describe('prepareStatus', () => {
|
|
831
|
+
describe('when file is pending', () => {
|
|
832
|
+
it('adds error message when no error message exists', () => {
|
|
833
|
+
const status = {
|
|
834
|
+
form: {
|
|
835
|
+
file: {
|
|
836
|
+
fileStatus: FileStatus.pending,
|
|
837
|
+
errorMessage: undefined
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
} as UploadStatusFileResponse
|
|
841
|
+
|
|
842
|
+
const result = prepareStatus(status)
|
|
843
|
+
|
|
844
|
+
expect(result.form.file.errorMessage).toBe(
|
|
845
|
+
'The selected file has not fully uploaded'
|
|
846
|
+
)
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
it('preserves existing error message', () => {
|
|
850
|
+
const existingError = 'Existing error message'
|
|
851
|
+
const status = {
|
|
852
|
+
form: {
|
|
853
|
+
file: {
|
|
854
|
+
fileStatus: FileStatus.pending,
|
|
855
|
+
errorMessage: existingError
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
} as UploadStatusFileResponse
|
|
859
|
+
|
|
860
|
+
const result = prepareStatus(status)
|
|
861
|
+
|
|
862
|
+
expect(result.form.file.errorMessage).toBe(existingError)
|
|
863
|
+
})
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
describe('when file is not pending', () => {
|
|
867
|
+
it('does not add error message', () => {
|
|
868
|
+
const status = {
|
|
869
|
+
form: {
|
|
870
|
+
file: {
|
|
871
|
+
fileStatus: FileStatus.complete,
|
|
872
|
+
errorMessage: undefined
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
} as UploadStatusFileResponse
|
|
876
|
+
|
|
877
|
+
const result = prepareStatus(status)
|
|
878
|
+
|
|
879
|
+
expect(result.form.file.errorMessage).toBeUndefined()
|
|
880
|
+
})
|
|
881
|
+
})
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
describe('getErrors', () => {
|
|
885
|
+
let controller: FileUploadPageController
|
|
886
|
+
|
|
887
|
+
beforeEach(() => {
|
|
888
|
+
const { pages } = structuredClone(definition)
|
|
889
|
+
const model = new FormModel(definition, { basePath: 'test' })
|
|
890
|
+
controller = new FileUploadPageController(model, pages[0])
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
describe('when no details provided', () => {
|
|
894
|
+
it('returns undefined', () => {
|
|
895
|
+
const errors = controller.getErrors()
|
|
896
|
+
expect(errors).toBeUndefined()
|
|
897
|
+
})
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
describe('error handling', () => {
|
|
901
|
+
it('handles non-upload errors using getError helper', () => {
|
|
902
|
+
const errorDetail = {
|
|
903
|
+
message: 'some error',
|
|
904
|
+
path: ['otherField'],
|
|
905
|
+
type: 'any.required'
|
|
906
|
+
}
|
|
907
|
+
const errors = controller.getErrors([errorDetail])
|
|
908
|
+
expect(errors).toEqual([getError(errorDetail)])
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('handles upload root errors using getError helper', () => {
|
|
912
|
+
const errorDetail = {
|
|
913
|
+
message: 'some error',
|
|
914
|
+
path: ['fileUpload'],
|
|
915
|
+
type: 'any.required'
|
|
916
|
+
}
|
|
917
|
+
const errors = controller.getErrors([errorDetail])
|
|
918
|
+
expect(errors).toEqual([getError(errorDetail)])
|
|
919
|
+
})
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
describe('object.unknown type errors', () => {
|
|
923
|
+
it('pushes an error with errorMessage', () => {
|
|
924
|
+
const errorDetail = {
|
|
925
|
+
message: 'some error',
|
|
926
|
+
path: ['fileUpload', 'errorMessage'],
|
|
927
|
+
type: 'object.unknown',
|
|
928
|
+
context: { value: 'some error text' }
|
|
929
|
+
}
|
|
930
|
+
const errors = controller.getErrors([errorDetail])
|
|
931
|
+
expect(errors).toEqual([
|
|
932
|
+
{
|
|
933
|
+
path: ['fileUpload', 'errorMessage'],
|
|
934
|
+
href: '#fileUpload',
|
|
935
|
+
name: 'fileUpload',
|
|
936
|
+
text: 'some error text'
|
|
937
|
+
}
|
|
938
|
+
])
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
it('handles non-string error message values with default text', () => {
|
|
942
|
+
const errorDetail = {
|
|
943
|
+
message: 'some error',
|
|
944
|
+
path: ['fileUpload', 'errorMessage'],
|
|
945
|
+
type: 'object.unknown',
|
|
946
|
+
context: { value: { some: 'object' } }
|
|
947
|
+
}
|
|
948
|
+
const errors = controller.getErrors([errorDetail])
|
|
949
|
+
expect(errors).toEqual([
|
|
950
|
+
{
|
|
951
|
+
path: ['fileUpload', 'errorMessage'],
|
|
952
|
+
href: '#fileUpload',
|
|
953
|
+
name: 'fileUpload',
|
|
954
|
+
text: 'Unknown error'
|
|
955
|
+
}
|
|
956
|
+
])
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
it('handles object.unknown error type with errorMessage path', () => {
|
|
960
|
+
const details = [
|
|
961
|
+
{
|
|
962
|
+
type: 'object.unknown',
|
|
963
|
+
path: ['fileUpload', 'errorMessage'],
|
|
964
|
+
context: { value: 'Custom error message' }
|
|
965
|
+
}
|
|
966
|
+
] as ValidationErrorItem[]
|
|
967
|
+
|
|
968
|
+
const errors = controller.getErrors(details)
|
|
969
|
+
|
|
970
|
+
expect(errors).toEqual([
|
|
971
|
+
{
|
|
972
|
+
path: ['fileUpload', 'errorMessage'],
|
|
973
|
+
href: '#fileUpload',
|
|
974
|
+
name: 'fileUpload',
|
|
975
|
+
text: 'Custom error message'
|
|
976
|
+
}
|
|
977
|
+
])
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
describe('initiateAndStoreNewUpload', () => {
|
|
983
|
+
it('throws error when initiateUpload returns undefined', async () => {
|
|
984
|
+
const state = {
|
|
985
|
+
upload: {
|
|
986
|
+
'/test/file-upload': {
|
|
987
|
+
upload: {},
|
|
988
|
+
files: []
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} as unknown as FormSubmissionState
|
|
992
|
+
|
|
993
|
+
jest.spyOn(uploadService, 'initiateUpload').mockResolvedValue(undefined)
|
|
994
|
+
|
|
995
|
+
await expect(
|
|
996
|
+
(
|
|
997
|
+
controller['initiateAndStoreNewUpload'] as (
|
|
998
|
+
req: FormRequest,
|
|
999
|
+
state: FormSubmissionState
|
|
1000
|
+
) => Promise<FormSubmissionState>
|
|
1001
|
+
)(request, state)
|
|
1002
|
+
).rejects.toThrow('Unexpected empty response from initiateUpload')
|
|
1003
|
+
})
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
describe('makeGetItemDeleteRouteHandler', () => {
|
|
1007
|
+
it('throws notFound error when file to delete does not exist', () => {
|
|
1008
|
+
const state = {
|
|
1009
|
+
upload: {
|
|
1010
|
+
[controller.path]: {
|
|
1011
|
+
files: [
|
|
1012
|
+
{
|
|
1013
|
+
uploadId: 'file-1',
|
|
1014
|
+
status: { form: { file: { filename: 'file-1.pdf' } } }
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
uploadId: 'file-2',
|
|
1018
|
+
status: { form: { file: { filename: 'file-2.pdf' } } }
|
|
1019
|
+
}
|
|
1020
|
+
]
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const request = {
|
|
1026
|
+
params: { itemId: 'I do not exist' }
|
|
1027
|
+
} as unknown as FormRequest
|
|
1028
|
+
|
|
1029
|
+
const context = { state } as unknown as FormContext
|
|
1030
|
+
const h = {} as unknown as Pick<ResponseToolkit, 'redirect' | 'view'>
|
|
1031
|
+
|
|
1032
|
+
const handler = controller.makeGetItemDeleteRouteHandler()
|
|
1033
|
+
|
|
1034
|
+
expect(() => handler(request, context, h)).toThrow(
|
|
1035
|
+
'File to delete not found'
|
|
1036
|
+
)
|
|
1037
|
+
})
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
describe('makePostItemDeleteRouteHandler', () => {
|
|
1041
|
+
it('proceeds without deleting when confirm is false', async () => {
|
|
1042
|
+
const request = {
|
|
1043
|
+
params: { itemId: 'file-1' }
|
|
1044
|
+
} as unknown as FormRequestPayload
|
|
1045
|
+
|
|
1046
|
+
const h = {
|
|
1047
|
+
redirect: jest.fn()
|
|
1048
|
+
} as unknown as Pick<ResponseToolkit, 'redirect' | 'view'>
|
|
1049
|
+
|
|
1050
|
+
const context = {
|
|
1051
|
+
state: {}
|
|
1052
|
+
} as unknown as FormContext
|
|
1053
|
+
|
|
1054
|
+
jest
|
|
1055
|
+
.spyOn(controller, 'getFormParams')
|
|
1056
|
+
.mockReturnValue({ confirm: false } as unknown as FormParams)
|
|
1057
|
+
|
|
1058
|
+
const proceedSpy = jest
|
|
1059
|
+
.spyOn(controller, 'proceed')
|
|
1060
|
+
.mockResolvedValue({ statusCode: 302 } as never)
|
|
1061
|
+
|
|
1062
|
+
const handler = controller.makePostItemDeleteRouteHandler()
|
|
1063
|
+
await handler(request, context, h)
|
|
1064
|
+
|
|
1065
|
+
expect(proceedSpy).toHaveBeenCalledWith(request, h)
|
|
1066
|
+
})
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
describe('getViewModel', () => {
|
|
1070
|
+
it('includes uploadId and proxyUrl in the view model', () => {
|
|
1071
|
+
const state = {
|
|
1072
|
+
upload: {
|
|
1073
|
+
[controller.path]: {
|
|
1074
|
+
upload: {
|
|
1075
|
+
uploadId: 'some-upload-id',
|
|
1076
|
+
uploadUrl: 'https://cdp-upload-and-scan.com/upload',
|
|
1077
|
+
statusUrl: 'https://cdp-upload-and-scan.com/status'
|
|
1078
|
+
},
|
|
1079
|
+
files: []
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
} as unknown as FormSubmissionState
|
|
1083
|
+
|
|
1084
|
+
const context = { state } as FormContext
|
|
1085
|
+
|
|
1086
|
+
jest
|
|
1087
|
+
.spyOn(QuestionPageController.prototype, 'getViewModel')
|
|
1088
|
+
.mockReturnValue({
|
|
1089
|
+
components: [{ model: { id: 'fileUpload' } }]
|
|
1090
|
+
} as unknown as FeaturedFormPageViewModel)
|
|
1091
|
+
|
|
1092
|
+
jest
|
|
1093
|
+
.spyOn(pageHelpers, 'getProxyUrlForLocalDevelopment')
|
|
1094
|
+
.mockReturnValue('http://uploader.127.0.0.1.sslip.io:7300')
|
|
1095
|
+
|
|
1096
|
+
const viewModel = controller.getViewModel(
|
|
1097
|
+
request as FormContextRequest,
|
|
1098
|
+
context
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
expect(viewModel.uploadId).toBe('some-upload-id')
|
|
1102
|
+
expect(viewModel.proxyUrl).toBe('http://uploader.127.0.0.1.sslip.io:7300')
|
|
1103
|
+
expect(viewModel.formAction).toBe(
|
|
1104
|
+
'https://cdp-upload-and-scan.com/upload'
|
|
1105
|
+
)
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
})
|