@planning-inspectorate/dynamic-forms 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/package.json +1 -1
- package/src/components/manage-list/README.md +21 -2
- package/src/components/manage-list/index.njk +3 -3
- package/src/components/manage-list/question.js +22 -10
- package/src/components/manage-list/utils.js +14 -0
- package/src/components/manage-list/utils.test.js +47 -0
- package/src/components/utils/question-has-answer.js +45 -11
- package/src/components/utils/question-utils.js +3 -37
- package/src/controller.js +4 -0
- package/src/controller.test.js +5 -1
- package/src/journey/journey.js +1 -0
- package/src/lib/session-answer-store.js +12 -3
- package/src/lib/session-answer-store.test.js +42 -0
- package/src/lib/utils.js +11 -0
- package/src/lib/utils.test.js +19 -0
- package/src/questions/question.js +9 -9
- package/src/questions/question.test.js +2 -0
- package/src/section-types.d.ts +2 -0
- package/src/section.js +5 -1
- package/src/section.test.js +7 -3
- package/test/check-your-answers.test.js +19 -1
- package/test/questions.test.js +3 -3
- package/src/components/macros/answer-list.njk +0 -29
- package/src/middleware/check-not-submitted.js +0 -13
- package/src/middleware/check-not-submitted.test.js +0 -118
- package/src/middleware/dynamic-req-files-to-req-body-files.js +0 -46
- package/src/middleware/dynamic-req-files-to-req-body-files.test.js +0 -221
- package/src/validator/required-file-upload-validator.js +0 -38
- package/src/validator/required-file-upload-validator.test.js +0 -141
- package/src/validator/schemas/required-file-upload-schema.js +0 -35
package/src/section.test.js
CHANGED
|
@@ -715,12 +715,14 @@ describe('./src/dynamic-forms/section.js', () => {
|
|
|
715
715
|
const section = new Section('s1', 'S').addQuestion(q1).addQuestion(manageListQ, manageListSection);
|
|
716
716
|
return { section, q1, manageListQ, q3, q4 };
|
|
717
717
|
};
|
|
718
|
+
const response = { answers: {} };
|
|
718
719
|
it(`should get first question within a manage list section`, () => {
|
|
719
720
|
const { section, manageListQ, q3 } = newSectionWithManageList();
|
|
720
721
|
const next = section.getNextQuestion({
|
|
721
722
|
questionFieldName: manageListQ.fieldName,
|
|
722
|
-
response
|
|
723
|
+
response,
|
|
723
724
|
manageListQuestion: manageListQ,
|
|
725
|
+
routeParams: {},
|
|
724
726
|
takeNextQuestion: true,
|
|
725
727
|
reverse: false
|
|
726
728
|
});
|
|
@@ -730,8 +732,9 @@ describe('./src/dynamic-forms/section.js', () => {
|
|
|
730
732
|
const { section, manageListQ, q3, q4 } = newSectionWithManageList();
|
|
731
733
|
const next = section.getNextQuestion({
|
|
732
734
|
questionFieldName: q3.fieldName,
|
|
733
|
-
response
|
|
735
|
+
response,
|
|
734
736
|
manageListQuestion: manageListQ,
|
|
737
|
+
routeParams: {},
|
|
735
738
|
takeNextQuestion: false,
|
|
736
739
|
reverse: false
|
|
737
740
|
});
|
|
@@ -741,8 +744,9 @@ describe('./src/dynamic-forms/section.js', () => {
|
|
|
741
744
|
const { section, manageListQ, q4 } = newSectionWithManageList();
|
|
742
745
|
const next = section.getNextQuestion({
|
|
743
746
|
questionFieldName: q4.fieldName,
|
|
744
|
-
response
|
|
747
|
+
response,
|
|
745
748
|
manageListQuestion: manageListQ,
|
|
749
|
+
routeParams: {},
|
|
746
750
|
takeNextQuestion: false,
|
|
747
751
|
reverse: false
|
|
748
752
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'assert';
|
|
3
|
-
import { questionsInOrder } from './questions.js';
|
|
3
|
+
import { manageListQuestions, questionsInOrder } from './questions.js';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { escapeForRegExp, snapshotsDir } from './utils/utils.js';
|
|
6
6
|
import { createAppWithQuestions, mockAnswer, mockAnswerBody } from './questions.test.js';
|
|
7
|
+
import { COMPONENT_TYPES } from '#src/index.js';
|
|
7
8
|
|
|
8
9
|
describe('check-your-answers', () => {
|
|
9
10
|
it(`should render all question rows with no answers`, async (ctx) => {
|
|
@@ -39,6 +40,23 @@ describe('check-your-answers', () => {
|
|
|
39
40
|
|
|
40
41
|
// 'answer' each question
|
|
41
42
|
for (const q of questionsInOrder) {
|
|
43
|
+
// Save happens on subquestion for manage list questions
|
|
44
|
+
if (q.type === COMPONENT_TYPES.MANAGE_LIST) {
|
|
45
|
+
// Get the list of subquestions for this manage list question
|
|
46
|
+
const manageListQuestion = manageListQuestions.find(
|
|
47
|
+
(question) => question.manageListQuestion.fieldName === q.fieldName
|
|
48
|
+
);
|
|
49
|
+
const subQuestionAnswers = mockAnswerBody(q)[manageListQuestion.manageListQuestion.fieldName];
|
|
50
|
+
// Post to them individually
|
|
51
|
+
for (const answer of subQuestionAnswers) {
|
|
52
|
+
const id = answer.id;
|
|
53
|
+
for (const subQuestion of manageListQuestion.questions) {
|
|
54
|
+
const key = subQuestion.fieldName;
|
|
55
|
+
const body = { [key]: answer[key] };
|
|
56
|
+
await testServer.post(`/questions/${q.url}/add/${id}/${subQuestion.url}`, body, { redirect: 'manual' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
await testServer.post(`/questions/${q.url}`, mockAnswerBody(q), { redirect: 'manual' });
|
|
43
61
|
}
|
|
44
62
|
|
package/test/questions.test.js
CHANGED
|
@@ -257,9 +257,9 @@ export function mockAnswerBody(q) {
|
|
|
257
257
|
// manage list questions not available here so just hard coded
|
|
258
258
|
return {
|
|
259
259
|
[q.fieldName]: [
|
|
260
|
-
{ travelCompanionName: 'Companion 1', travelCompanionEmail: '
|
|
261
|
-
{ travelCompanionName: 'Companion 2', travelCompanionEmail: '
|
|
262
|
-
{ travelCompanionName: 'Companion 3', travelCompanionEmail: '
|
|
260
|
+
{ id: '1234', travelCompanionName: 'Companion 1', travelCompanionEmail: 'companion-1@example.com' },
|
|
261
|
+
{ id: '4567', travelCompanionName: 'Companion 2', travelCompanionEmail: 'companion-2@example.com' },
|
|
262
|
+
{ id: '8901', travelCompanionName: 'Companion 3', travelCompanionEmail: 'companion-3@example.com' }
|
|
263
263
|
]
|
|
264
264
|
};
|
|
265
265
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
|
|
2
|
-
|
|
3
|
-
{% macro answerList(answers) %}
|
|
4
|
-
{% set summaryRows = [] %}
|
|
5
|
-
|
|
6
|
-
{% for item in answers %}
|
|
7
|
-
{% set summaryRows = (summaryRows.push({
|
|
8
|
-
key: {
|
|
9
|
-
text: item.label
|
|
10
|
-
},
|
|
11
|
-
value: {
|
|
12
|
-
html: item.answer | escape | nl2br | safe
|
|
13
|
-
},
|
|
14
|
-
actions: {
|
|
15
|
-
items: [
|
|
16
|
-
{
|
|
17
|
-
href: item.removeLink,
|
|
18
|
-
text: "Remove",
|
|
19
|
-
visuallyHiddenText: item.label + ", " + item.answer
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}), summaryRows) %}
|
|
24
|
-
{% endfor %}
|
|
25
|
-
|
|
26
|
-
{{ govukSummaryList({
|
|
27
|
-
rows: summaryRows
|
|
28
|
-
}) }}
|
|
29
|
-
{% endmacro %}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {string} alreadySubmittedUrl
|
|
3
|
-
* @returns {import('express').Handler}
|
|
4
|
-
*/
|
|
5
|
-
export default (alreadySubmittedUrl) => (req, res, next) => {
|
|
6
|
-
const journeyResponse = res?.locals?.journeyResponse;
|
|
7
|
-
|
|
8
|
-
if (journeyResponse?.answers?.submitted === 'yes' || journeyResponse?.answers?.submitted === true) {
|
|
9
|
-
return res.redirect(alreadySubmittedUrl);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return next();
|
|
13
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, mock } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import checkNotSubmitted from './check-not-submitted.js';
|
|
4
|
-
|
|
5
|
-
const testUrl = '/test';
|
|
6
|
-
|
|
7
|
-
describe('dynamic-forms/middleware/check-not-submitted', () => {
|
|
8
|
-
const mockReq = () => ({
|
|
9
|
-
cookies: {},
|
|
10
|
-
log: {},
|
|
11
|
-
params: {}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
[
|
|
15
|
-
{
|
|
16
|
-
description: 'should do nothing if submitted is undefined',
|
|
17
|
-
given: () => {
|
|
18
|
-
return {
|
|
19
|
-
req: mockReq(),
|
|
20
|
-
res: {}
|
|
21
|
-
};
|
|
22
|
-
},
|
|
23
|
-
expected: (req, res, next) => {
|
|
24
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
description: 'should do nothing if submitted is false',
|
|
29
|
-
given: () => {
|
|
30
|
-
return {
|
|
31
|
-
req: mockReq(),
|
|
32
|
-
res: {
|
|
33
|
-
locals: {
|
|
34
|
-
journeyResponse: {
|
|
35
|
-
answers: { submitted: false }
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
expected: (req, res, next) => {
|
|
42
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
description: 'should do nothing if submitted is no',
|
|
47
|
-
given: () => {
|
|
48
|
-
return {
|
|
49
|
-
req: mockReq(),
|
|
50
|
-
res: {
|
|
51
|
-
locals: {
|
|
52
|
-
journeyResponse: {
|
|
53
|
-
answers: {
|
|
54
|
-
submitted: 'no'
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
},
|
|
61
|
-
expected: (req, res, next) => {
|
|
62
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
description: 'should redirect if submitted is true',
|
|
67
|
-
given: () => {
|
|
68
|
-
return {
|
|
69
|
-
req: mockReq(),
|
|
70
|
-
res: {
|
|
71
|
-
locals: {
|
|
72
|
-
journeyResponse: {
|
|
73
|
-
answers: {
|
|
74
|
-
submitted: true
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
redirect: mock.fn()
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
expected: (req, res) => {
|
|
83
|
-
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
description: 'should redirect if submitted is yes',
|
|
88
|
-
given: () => {
|
|
89
|
-
return {
|
|
90
|
-
req: mockReq(),
|
|
91
|
-
res: {
|
|
92
|
-
locals: {
|
|
93
|
-
journeyResponse: {
|
|
94
|
-
answers: {
|
|
95
|
-
submitted: 'yes'
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
redirect: mock.fn()
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
expected: (req, res) => {
|
|
104
|
-
assert.strictEqual(res.redirect.mock.callCount(), 1);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
].forEach(({ description, given, expected }) => {
|
|
108
|
-
it(description, () => {
|
|
109
|
-
const next = mock.fn();
|
|
110
|
-
const req = given().req;
|
|
111
|
-
const res = given().res;
|
|
112
|
-
|
|
113
|
-
checkNotSubmitted(testUrl)(req, res, next);
|
|
114
|
-
|
|
115
|
-
expected(req, res, next);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Middleware for converting `req.files` to `req.body.files`.
|
|
3
|
-
*
|
|
4
|
-
* The requirement for this is due to `express-validator` only considering a limited subset of
|
|
5
|
-
* keys on the `req` object as being eligible for validation.
|
|
6
|
-
*
|
|
7
|
-
* This middleware is designed for use with component-based dynamic forms
|
|
8
|
-
*
|
|
9
|
-
* This has not been tested with
|
|
10
|
-
* pages that contain two or more multiple file uploads.
|
|
11
|
-
*
|
|
12
|
-
* @returns {function(*, *, *): (*)}
|
|
13
|
-
*/
|
|
14
|
-
export default () => (req, res, next) => {
|
|
15
|
-
if (!req.body || !req.files || !req.params) {
|
|
16
|
-
return next();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const { journey } = res.locals;
|
|
20
|
-
|
|
21
|
-
const questionObj = journey.getQuestionByParams(req.params);
|
|
22
|
-
const filesPropertyPath = questionObj.fieldName;
|
|
23
|
-
|
|
24
|
-
if (typeof req.files[filesPropertyPath] === 'undefined') {
|
|
25
|
-
req.body.files = [];
|
|
26
|
-
return next();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
req.body.files = {
|
|
31
|
-
...req.body.files,
|
|
32
|
-
// Ensure we are always working with an array. Single files would otherwise be an object.
|
|
33
|
-
[filesPropertyPath]: Array.isArray(req.files[filesPropertyPath])
|
|
34
|
-
? req.files[filesPropertyPath]
|
|
35
|
-
: [req.files[filesPropertyPath]]
|
|
36
|
-
};
|
|
37
|
-
} catch (err) {
|
|
38
|
-
// this feels like it should never happen, as nothing in the try block throws. This is a precaution.
|
|
39
|
-
/* istanbul ignore next */
|
|
40
|
-
req.log.debug({ err, filesPropertyPath }, 'Error extracting req.files[filesPropertyPath]');
|
|
41
|
-
/* istanbul ignore next */
|
|
42
|
-
req.body.files = [];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return next();
|
|
46
|
-
};
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import dynamicReqFilesToReqBodyFilesMiddleware from './dynamic-req-files-to-req-body-files.js';
|
|
4
|
-
|
|
5
|
-
describe('dynamic-forms/middleware/dynamic-req-files-to-req-body-files', () => {
|
|
6
|
-
let counter = 0;
|
|
7
|
-
let mockReq;
|
|
8
|
-
let mockRes;
|
|
9
|
-
let filesPropertyPath = `example-files-property-path-${counter}`;
|
|
10
|
-
|
|
11
|
-
const mockgetQuestionByParams = mock.fn(() => {
|
|
12
|
-
return { fieldName: filesPropertyPath };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
mockReq = () => ({
|
|
17
|
-
cookies: {},
|
|
18
|
-
log: {},
|
|
19
|
-
params: {}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
mockRes = () => ({
|
|
23
|
-
clearCookie: mock.fn(),
|
|
24
|
-
cookie: mock.fn(),
|
|
25
|
-
locals: {
|
|
26
|
-
journeyResponse: mock.fn(),
|
|
27
|
-
journey: { getQuestionByParams: mockgetQuestionByParams }
|
|
28
|
-
},
|
|
29
|
-
redirect: mock.fn(),
|
|
30
|
-
render: mock.fn(),
|
|
31
|
-
sendStatus: mock.fn(),
|
|
32
|
-
status: mock.fn()
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
counter++;
|
|
36
|
-
filesPropertyPath = `example-files-property-path-${counter}`;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
[
|
|
40
|
-
{
|
|
41
|
-
description: 'should return early if `req.body` is not set',
|
|
42
|
-
given: () => mockReq(),
|
|
43
|
-
expected: (req, res, next) => {
|
|
44
|
-
assert.strictEqual(req.body, undefined);
|
|
45
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
description: 'should return early if `req.files` is not set',
|
|
50
|
-
given: () => ({
|
|
51
|
-
...mockReq(),
|
|
52
|
-
body: {}
|
|
53
|
-
}),
|
|
54
|
-
expected: (req, res, next) => {
|
|
55
|
-
assert.strictEqual(req.body.files, undefined);
|
|
56
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
description: 'should return early if `req.params` is not set',
|
|
61
|
-
given: () => ({
|
|
62
|
-
cookies: {},
|
|
63
|
-
log: {},
|
|
64
|
-
body: {},
|
|
65
|
-
files: {}
|
|
66
|
-
}),
|
|
67
|
-
expected: (req, res, next) => {
|
|
68
|
-
assert.strictEqual(req.body.files, undefined);
|
|
69
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
description:
|
|
74
|
-
'should set `req.body.files = []` if unable to find the given file property path `req.files` - unset',
|
|
75
|
-
given: () => ({
|
|
76
|
-
...mockReq(),
|
|
77
|
-
body: {},
|
|
78
|
-
files: {},
|
|
79
|
-
params: {
|
|
80
|
-
section: 'mock-section',
|
|
81
|
-
question: 'mock-question'
|
|
82
|
-
}
|
|
83
|
-
}),
|
|
84
|
-
expected: (req, res, next) => {
|
|
85
|
-
assert.deepStrictEqual(req.body.files, []);
|
|
86
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
description:
|
|
91
|
-
'should set `req.body.files = []` if unable to find the given file property path `req.files` - wrong path',
|
|
92
|
-
given: () => ({
|
|
93
|
-
...mockReq(),
|
|
94
|
-
body: {},
|
|
95
|
-
files: {
|
|
96
|
-
'some-different-path': {
|
|
97
|
-
x: 'y'
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
params: {
|
|
101
|
-
section: 'mock-section',
|
|
102
|
-
question: 'mock-question'
|
|
103
|
-
}
|
|
104
|
-
}),
|
|
105
|
-
expected: (req, res, next) => {
|
|
106
|
-
assert.deepStrictEqual(req.body.files, []);
|
|
107
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
description: 'should set `req.body.files = [req.files]` when given a single file',
|
|
112
|
-
given: () => ({
|
|
113
|
-
...mockReq(),
|
|
114
|
-
body: {},
|
|
115
|
-
params: {
|
|
116
|
-
section: 'mock-section',
|
|
117
|
-
question: 'mock-question'
|
|
118
|
-
},
|
|
119
|
-
files: {
|
|
120
|
-
[filesPropertyPath]: {
|
|
121
|
-
a: 'b'
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}),
|
|
125
|
-
expected: (req, res, next) => {
|
|
126
|
-
assert.deepStrictEqual(req.body.files, { [filesPropertyPath]: [{ a: 'b' }] });
|
|
127
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
description: 'should set `req.body.files = [...req.files]` when given multiple files',
|
|
132
|
-
given: () => ({
|
|
133
|
-
...mockReq(),
|
|
134
|
-
body: {},
|
|
135
|
-
params: {
|
|
136
|
-
section: 'mock-section',
|
|
137
|
-
question: 'mock-question'
|
|
138
|
-
},
|
|
139
|
-
files: {
|
|
140
|
-
[filesPropertyPath]: [
|
|
141
|
-
{
|
|
142
|
-
a: 'b'
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
c: 'd'
|
|
146
|
-
}
|
|
147
|
-
]
|
|
148
|
-
}
|
|
149
|
-
}),
|
|
150
|
-
expected: (req, res, next) => {
|
|
151
|
-
assert.deepStrictEqual(req.body.files, {
|
|
152
|
-
[filesPropertyPath]: [
|
|
153
|
-
{
|
|
154
|
-
a: 'b'
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
c: 'd'
|
|
158
|
-
}
|
|
159
|
-
]
|
|
160
|
-
});
|
|
161
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
description: 'should retain existing req.body.files when working with a different key',
|
|
166
|
-
given: () => ({
|
|
167
|
-
...mockReq(),
|
|
168
|
-
body: {
|
|
169
|
-
files: {
|
|
170
|
-
'an-existing-key-value-pair': [
|
|
171
|
-
{
|
|
172
|
-
rgb: 'xyz'
|
|
173
|
-
}
|
|
174
|
-
]
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
files: {
|
|
178
|
-
[filesPropertyPath]: [
|
|
179
|
-
{
|
|
180
|
-
a: 'b'
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
c: 'd'
|
|
184
|
-
}
|
|
185
|
-
]
|
|
186
|
-
},
|
|
187
|
-
params: {
|
|
188
|
-
section: 'mock-section',
|
|
189
|
-
question: 'mock-question'
|
|
190
|
-
}
|
|
191
|
-
}),
|
|
192
|
-
expected: (req, res, next) => {
|
|
193
|
-
assert.deepStrictEqual(req.body.files, {
|
|
194
|
-
'an-existing-key-value-pair': [
|
|
195
|
-
{
|
|
196
|
-
rgb: 'xyz'
|
|
197
|
-
}
|
|
198
|
-
],
|
|
199
|
-
[filesPropertyPath]: [
|
|
200
|
-
{
|
|
201
|
-
a: 'b'
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
c: 'd'
|
|
205
|
-
}
|
|
206
|
-
]
|
|
207
|
-
});
|
|
208
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
].forEach(({ description, given, expected }) => {
|
|
212
|
-
it(description, () => {
|
|
213
|
-
const next = mock.fn();
|
|
214
|
-
const req = given();
|
|
215
|
-
|
|
216
|
-
dynamicReqFilesToReqBodyFilesMiddleware()(req, mockRes(), next);
|
|
217
|
-
|
|
218
|
-
expected(req, mockRes(), next);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import BaseValidator from './base-validator.js';
|
|
2
|
-
import { checkSchema } from 'express-validator';
|
|
3
|
-
|
|
4
|
-
import requiredFileUploadSchema from './schemas/required-file-upload-schema.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {import('../questions/question.js')} Question
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* enforces that at least one file is present for a question, whether it is being/already uploaded
|
|
12
|
-
* @class
|
|
13
|
-
*/
|
|
14
|
-
export default class RequiredFileUploadValidator extends BaseValidator {
|
|
15
|
-
/**
|
|
16
|
-
* creates an instance of a RequiredFileUploadValidator
|
|
17
|
-
* @param {string} [errorMessage] - custom error message to show on validation failure
|
|
18
|
-
*/
|
|
19
|
-
constructor(errorMessage) {
|
|
20
|
-
super();
|
|
21
|
-
|
|
22
|
-
if (errorMessage) {
|
|
23
|
-
this.errorMessage = errorMessage;
|
|
24
|
-
} else {
|
|
25
|
-
this.errorMessage = 'You must add your documents';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* validates against path based on questionObj's fieldname
|
|
31
|
-
* @param {Question} questionObj
|
|
32
|
-
*/
|
|
33
|
-
validate(questionObj, journeyResponse) {
|
|
34
|
-
const path = questionObj.fieldName;
|
|
35
|
-
const documentType = questionObj.documentType.name;
|
|
36
|
-
return checkSchema(requiredFileUploadSchema(path, journeyResponse, documentType, this.errorMessage))[0];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import RequiredFileUploadValidator from './required-file-upload-validator.js';
|
|
4
|
-
|
|
5
|
-
describe('./src/dynamic-forms/validator/required-file-upload-validator.js', () => {
|
|
6
|
-
describe('validate', () => {
|
|
7
|
-
it('should have a rule for path featuring fieldName', () => {
|
|
8
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
9
|
-
|
|
10
|
-
const rule = requiredFileUploadValidator
|
|
11
|
-
.validate({
|
|
12
|
-
fieldName: 'test-field-name',
|
|
13
|
-
documentType: {
|
|
14
|
-
name: 'test-doc-type'
|
|
15
|
-
}
|
|
16
|
-
})
|
|
17
|
-
.builder.build();
|
|
18
|
-
|
|
19
|
-
assert.deepStrictEqual(rule.fields, ['test-field-name']);
|
|
20
|
-
assert.strictEqual(!!rule.optional, false);
|
|
21
|
-
assert.strictEqual(rule.stack.length, 1);
|
|
22
|
-
assert.strictEqual(rule.stack[0].validator.name, 'options');
|
|
23
|
-
assert.strictEqual(!!rule.stack[0].validator.negated, false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should pass if file being uploaded', async () => {
|
|
27
|
-
const questionObj = {
|
|
28
|
-
fieldName: 'test-fieldname',
|
|
29
|
-
documentType: {
|
|
30
|
-
name: 'test-doc-type'
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
const journeyResponse = {};
|
|
34
|
-
const req = { files: { test: 'test123' } };
|
|
35
|
-
|
|
36
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
37
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
38
|
-
|
|
39
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should pass if no file being uploaded and file matching question document type already uploaded', async () => {
|
|
43
|
-
const questionObj = {
|
|
44
|
-
fieldName: 'another-test-fieldname',
|
|
45
|
-
documentType: {
|
|
46
|
-
name: 'test-doc-type'
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const uploadedFile = {
|
|
50
|
-
file: 'already-uploaded',
|
|
51
|
-
type: 'test-doc-type'
|
|
52
|
-
};
|
|
53
|
-
const journeyResponse = {
|
|
54
|
-
answers: { SubmissionDocumentUpload: [uploadedFile] }
|
|
55
|
-
};
|
|
56
|
-
const req = { body: {} };
|
|
57
|
-
|
|
58
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
59
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
60
|
-
|
|
61
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should fail if no file being uploaded and no file matching question document type already uploaded', async () => {
|
|
65
|
-
const questionObj = {
|
|
66
|
-
fieldName: 'yet-another-test-fieldname',
|
|
67
|
-
documentType: {
|
|
68
|
-
name: 'test-doc-type'
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const journeyResponse = { answers: { SubmissionDocumentUpload: [] } };
|
|
72
|
-
const req = { body: {} };
|
|
73
|
-
|
|
74
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
75
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
76
|
-
|
|
77
|
-
assert.strictEqual(validationResult.errors.length, 1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should fail if no file being uploaded matching question doc type and all existing files matching doc type being removed', async () => {
|
|
81
|
-
const questionObj = {
|
|
82
|
-
fieldName: 'test-fieldname',
|
|
83
|
-
documentType: {
|
|
84
|
-
name: 'test-doc-type'
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const journeyResponse = {
|
|
89
|
-
answers: {
|
|
90
|
-
SubmissionDocumentUpload: [
|
|
91
|
-
{
|
|
92
|
-
file: 'to-be-removed',
|
|
93
|
-
type: 'test-doc-type'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
file: 'to-be-removed-too',
|
|
97
|
-
type: 'test-doc-type'
|
|
98
|
-
}
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
const req = { body: {} };
|
|
103
|
-
req.body.removedFiles =
|
|
104
|
-
'[{"file": "to-be-removed", "type": "test-doc-type"}, {"file": "to-be-removed-too", "type": "test-doc-type"}]';
|
|
105
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
106
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
107
|
-
|
|
108
|
-
assert.strictEqual(validationResult.errors.length, 1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should pass if existing files matching question doc type and only some being removed', async () => {
|
|
112
|
-
const questionObj = {
|
|
113
|
-
fieldName: 'test-fieldname',
|
|
114
|
-
documentType: {
|
|
115
|
-
name: 'test-doc-type'
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const journeyResponse = {
|
|
119
|
-
answers: {
|
|
120
|
-
SubmissionDocumentUpload: [
|
|
121
|
-
{
|
|
122
|
-
file: 'to-be-removed',
|
|
123
|
-
type: 'test-doc-type'
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
file: 'to-be-kept',
|
|
127
|
-
type: 'test-doc-type'
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
const req = { body: {} };
|
|
133
|
-
req.body.removedFiles = '[{"file": "to-be-removed", "type": "test-doc-type"}]';
|
|
134
|
-
|
|
135
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
136
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
137
|
-
|
|
138
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|