"
+ Examples:
+ | form | summaryValue |
+ | files | Uploaded |
+ | files-show-filenames-enabled | passes.png |
+
+
+ Scenario: Uploading a file shows "You previously uploaded" message
+ And I navigate to the "files" form
+ When I upload the file "passes.png"
+ And I navigate to "/files/file-one"
+ Then I see "You previously uploaded the file ‘passes.png’"
diff --git a/e2e/cypress/e2e/runner/files.js b/e2e/cypress/e2e/runner/files.js
new file mode 100644
index 0000000000..d907a8c5b9
--- /dev/null
+++ b/e2e/cypress/e2e/runner/files.js
@@ -0,0 +1,6 @@
+import { When } from "@badeball/cypress-cucumber-preprocessor";
+
+When("I upload the file {string}", (filename) => {
+ cy.get("input[type=file]").attachFile(filename);
+ cy.findByRole("button", { name: /continue/i }).click();
+});
diff --git a/e2e/cypress/fixtures/files-show-filenames-enabled.json b/e2e/cypress/fixtures/files-show-filenames-enabled.json
new file mode 100644
index 0000000000..f0e7af8827
--- /dev/null
+++ b/e2e/cypress/fixtures/files-show-filenames-enabled.json
@@ -0,0 +1,48 @@
+{
+ "startPage": "/file-one",
+ "pages": [
+ {
+ "path": "/file-one",
+ "components": [
+ {
+ "type": "FileUploadField",
+ "name": "file1",
+ "title": "File 1",
+ "options": {
+ "required": true
+ },
+ "schema": {}
+ }
+ ],
+ "section": "checkBeforeYouStart",
+ "next": [
+ {
+ "path": "/summary"
+ }
+ ],
+ "title": "Upload file 1"
+ },
+ {
+ "path": "/summary",
+ "controller": "./pages/summary.js",
+ "title": "Summary",
+ "components": [],
+ "next": []
+ }
+ ],
+ "lists": [],
+ "sections": [
+ {
+ "name": "checkBeforeYouStart",
+ "title": "Check before you start"
+ }
+ ],
+ "phaseBanner": {},
+ "fees": [],
+ "payApiKey": "",
+ "outputs": [],
+ "declaration": "All the answers you have provided are true to the best of your knowledge.
",
+ "version": 2,
+ "conditions": [],
+ "showFilenamesOnSummaryPage": true
+}
diff --git a/e2e/cypress/fixtures/files.json b/e2e/cypress/fixtures/files.json
new file mode 100644
index 0000000000..a8b41bcd7c
--- /dev/null
+++ b/e2e/cypress/fixtures/files.json
@@ -0,0 +1,47 @@
+{
+ "startPage": "/file-one",
+ "pages": [
+ {
+ "path": "/file-one",
+ "components": [
+ {
+ "type": "FileUploadField",
+ "name": "file1",
+ "title": "File 1",
+ "options": {
+ "required": true
+ },
+ "schema": {}
+ }
+ ],
+ "section": "checkBeforeYouStart",
+ "next": [
+ {
+ "path": "/summary"
+ }
+ ],
+ "title": "Upload file 1"
+ },
+ {
+ "path": "/summary",
+ "controller": "./pages/summary.js",
+ "title": "Summary",
+ "components": [],
+ "next": []
+ }
+ ],
+ "lists": [],
+ "sections": [
+ {
+ "name": "checkBeforeYouStart",
+ "title": "Check before you start"
+ }
+ ],
+ "phaseBanner": {},
+ "fees": [],
+ "payApiKey": "",
+ "outputs": [],
+ "declaration": "All the answers you have provided are true to the best of your knowledge.
",
+ "version": 2,
+ "conditions": []
+}
diff --git a/model/src/schema/schema.ts b/model/src/schema/schema.ts
index 45b2fc520b..ec71847a9d 100644
--- a/model/src/schema/schema.ts
+++ b/model/src/schema/schema.ts
@@ -327,6 +327,7 @@ export const Schema = joi
specialPages: specialPagesSchema.optional(),
feeOptions: feeOptionSchema,
exitOptions: exitSchema.optional(),
+ showFilenamesOnSummaryPage: joi.boolean().optional(),
});
/**
diff --git a/runner/src/server/plugins/engine/models/FormModel.ts b/runner/src/server/plugins/engine/models/FormModel.ts
index 61a00ba427..088e49b6a0 100644
--- a/runner/src/server/plugins/engine/models/FormModel.ts
+++ b/runner/src/server/plugins/engine/models/FormModel.ts
@@ -63,6 +63,7 @@ export class FormModel {
specialPages: FormDefinition["specialPages"];
exitOptions?: ExitOptions;
allowExit: boolean = false;
+ showFilenamesOnSummaryPage: boolean = false;
constructor(def, options) {
const result = Schema.validate(def, { abortEarly: false });
@@ -120,6 +121,8 @@ export class FormModel {
this.allowExit = true;
}
+ this.showFilenamesOnSummaryPage = def.showFilenamesOnSummaryPage ?? false;
+
// @ts-ignore
this.pages = def.pages.map((pageDef) => this.makePage(pageDef));
this.startPage = this.pages.find((page) => page.path === def.startPage);
diff --git a/runner/src/server/plugins/engine/models/SummaryViewModel.ts b/runner/src/server/plugins/engine/models/SummaryViewModel.ts
index fe05c724a5..ddeb52ab58 100644
--- a/runner/src/server/plugins/engine/models/SummaryViewModel.ts
+++ b/runner/src/server/plugins/engine/models/SummaryViewModel.ts
@@ -158,6 +158,8 @@ export class SummaryViewModel {
const items: any[] = [];
let sectionState = section ? state[section.name] || {} : state;
+ sectionState.originalFilenames = state.originalFilenames ?? {};
+
const sectionPages = relevantPages.filter(
(page) => page.section === section
);
@@ -352,7 +354,7 @@ function Item(
});
}
- return {
+ const item = {
name: component.name,
path: page.path,
label: component.localisedString(component.title),
@@ -365,4 +367,14 @@ function Item(
dataType: component.dataType,
immutable: component.options.disableChangingFromSummary,
};
+
+ if (
+ component.type === "FileUploadField" &&
+ model.showFilenamesOnSummaryPage
+ ) {
+ item.filename =
+ sectionState.originalFilenames[component.name]?.originalFilename;
+ }
+
+ return item;
}
diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts
index 9bb88b832e..71a6a5d236 100644
--- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts
+++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts
@@ -611,7 +611,7 @@ export class PageControllerBase {
/**
* other file related errors.. assuming file fields will be on their own page. This will replace all other errors from the page if not..
*/
- if (preHandlerErrors) {
+ if (preHandlerErrors?.length) {
const reformattedErrors: any[] = [];
preHandlerErrors.forEach((error) => {
const reformatted = error;
diff --git a/runner/src/server/plugins/engine/plugin.ts b/runner/src/server/plugins/engine/plugin.ts
index ec83858a2f..8fe5a777ae 100644
--- a/runner/src/server/plugins/engine/plugin.ts
+++ b/runner/src/server/plugins/engine/plugin.ts
@@ -11,6 +11,11 @@ import { FormPayload } from "./types";
import { shouldLogin } from "server/plugins/auth";
import config from "../../config";
import * as exit from "./pluginHandlers/exit";
+import {
+ getFiles,
+ handleUpload,
+ validateContentTypes,
+} from "./pluginHandlers/files/prehandlers";
configure([
// Configure Nunjucks to allow rendering of content that is revealed conditionally.
@@ -280,15 +285,6 @@ export const plugin = {
const { uploadService } = server.services([]);
- const handleFiles = (request: HapiRequest, h: HapiResponseToolkit) => {
- const { path, id } = request.params;
- const model = forms[id];
- const page = model?.pages.find(
- (page) => normalisePath(page.path) === normalisePath(path)
- );
- return uploadService.handleUploadRequest(request, h, page.pageDef);
- };
-
//TODO:- Merge with POST /{id}/{path*} route, and move to ./pluginHandlers/id/*
const postHandler = async (
request: HapiRequest,
@@ -330,7 +326,11 @@ export const plugin = {
return h.continue;
},
},
- pre: [{ method: handleFiles }],
+ pre: [
+ { method: getFiles, assign: "files" },
+ { method: validateContentTypes, assign: "validFiles" },
+ { method: handleUpload },
+ ],
handler: postHandler,
},
});
diff --git a/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/getFiles.ts b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/getFiles.ts
new file mode 100644
index 0000000000..bae29f4307
--- /dev/null
+++ b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/getFiles.ts
@@ -0,0 +1,18 @@
+import { HapiRequest, HapiResponseToolkit } from "server/types";
+
+/**
+ * Prehandler that parses the payload and finds Buffers (i.e. files).
+ */
+export function getFiles(request: HapiRequest, _h: HapiResponseToolkit) {
+ const { uploadService } = request.services([]);
+ const files = uploadService.fileStreamsFromPayload(request.payload);
+ if (files.length) {
+ request.server.logger.info(
+ { id: request.yar.id, path: request.path },
+ `Found ${uploadService.fileSummary(files)} to process on ${request.path}`
+ );
+ return files;
+ }
+
+ return null;
+}
diff --git a/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/handleUpload.ts b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/handleUpload.ts
new file mode 100644
index 0000000000..a775ff098c
--- /dev/null
+++ b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/handleUpload.ts
@@ -0,0 +1,119 @@
+import { HapiRequest, HapiResponseToolkit } from "server/types";
+
+const parsedError = (key: string, error?: string) => {
+ return {
+ path: key,
+ href: `#${key}`,
+ name: key,
+ text: error,
+ };
+};
+
+export async function handleUpload(
+ request: HapiRequest,
+ h: HapiResponseToolkit
+) {
+ const { cacheService, uploadService } = request.services([]);
+ const state = await cacheService.getState(request);
+ let originalFilenames = state?.originalFilenames ?? {};
+ let logger = request.server.logger;
+ const files = request.pre.files;
+
+ if (!files) {
+ return h.continue;
+ }
+
+ /**
+ * files is an array of tuples containing key and value.
+ * value may be an array of file data where multiple files have been uploaded
+ */
+ const validFiles = request.pre.validFiles;
+
+ for (const entry of validFiles) {
+ const [fieldName, streams] = entry;
+ const loggerIdentifier = uploadService.getLoggerIdentifier(request, {
+ fieldName,
+ });
+ const previousUpload = originalFilenames[fieldName];
+
+ if (previousUpload) {
+ logger.info(
+ loggerIdentifier,
+ `User is attempting to overwrite ${fieldName} with location ${previousUpload.location}`
+ );
+ }
+
+ let response;
+
+ try {
+ response = await uploadService.uploadDocuments(streams);
+ } catch (err) {
+ if (err.data?.res) {
+ const { error } = uploadService.parsedDocumentUploadResponse(err.data);
+ request.pre.errors = [
+ ...request.pre.errors,
+ parsedError(fieldName, error),
+ ];
+ } else if (err.code === "EPIPE") {
+ // ignore this error, it happens when the request is responded to by the doc upload service before the
+ // body has finished being sent. A valid response is still received.
+ logger.warn(loggerIdentifier, `Ignoring EPIPE response ${err.message}`);
+ } else {
+ logger.error(
+ { ...loggerIdentifier, err },
+ `Error uploading document: ${err.message}`
+ );
+ request.pre.errors = [
+ ...(h.request.pre.errors || []),
+ parsedError(fieldName, err),
+ ];
+ }
+ }
+
+ const { location, warning, error } = response;
+
+ if (location) {
+ const originalFilename = streams
+ .map((stream) => stream.hapi?.filename)
+ .join(", ");
+
+ logger.info(
+ loggerIdentifier,
+ `Uploaded ${fieldName} successfully to ${location}`
+ );
+
+ originalFilenames[fieldName] = { location, originalFilename };
+ const {
+ originalFilenames: updatedFilenames,
+ } = await cacheService.mergeState(request, { originalFilenames });
+
+ logger.info(
+ { ...loggerIdentifier, allFiles: updatedFilenames },
+ `Updated originalFileNames for user`
+ );
+ request.payload[fieldName] = location;
+ originalFilenames = updatedFilenames;
+ }
+
+ if (warning) {
+ request.pre.warning = warning;
+ logger.warn(
+ loggerIdentifier,
+ `File was uploaded successfully but there was a warning ${warning}`
+ );
+ }
+
+ if (error) {
+ logger.error(
+ loggerIdentifier,
+ `Document upload API responded with an error ${error}`
+ );
+ request.pre.errors = [
+ ...(request.pre.errors || []),
+ parsedError(fieldName, error),
+ ];
+ }
+ }
+
+ return h.continue;
+}
diff --git a/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/index.ts b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/index.ts
new file mode 100644
index 0000000000..7404041602
--- /dev/null
+++ b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/index.ts
@@ -0,0 +1,3 @@
+export { getFiles } from "./getFiles";
+export { validateContentTypes } from "./validateContentTypes";
+export { handleUpload } from "./handleUpload";
diff --git a/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/validateContentTypes.ts b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/validateContentTypes.ts
new file mode 100644
index 0000000000..533e7cbedf
--- /dev/null
+++ b/runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/validateContentTypes.ts
@@ -0,0 +1,96 @@
+import { HapiRequest, HapiResponseToolkit } from "server/types";
+import { ReadableStreamEntry } from "server/services/upload/uploadService";
+
+/**
+ * This prehandler must follow {@link getFiles}. This prehandler checks if the content-type in the FormData is correct
+ * (or empty). If the user uploaded an empty field, a previous upload will be assigned if it exits.
+ */
+export async function validateContentTypes(
+ request: HapiRequest,
+ h: HapiResponseToolkit
+) {
+ const files = request.pre.files;
+
+ if (!files) {
+ return h.continue;
+ }
+
+ const { uploadService, cacheService } = request.services([]);
+ const logger = request.server.logger;
+ const loggerIdentifier = { id: request.yar.id, path: request.path };
+
+ const validFields: ReadableStreamEntry[] = [];
+ const erroredFields: string[] = [];
+
+ const { originalFilenames = {} } = await cacheService.getState(request);
+
+ const { id, path } = request.params;
+ const form = request.server.app.forms[id];
+ const page = form.pages.find((page) => page.path === `/${path}`);
+ const components = page.components.formItems;
+
+ for (const [fieldName, values] of files) {
+ const component = components.find(
+ (component) => component.name === fieldName
+ );
+
+ const originalFilenameLocation = originalFilenames[fieldName]?.location;
+
+ const filesArePopulated = values.every((value) => value?._data?.length > 1);
+ const componentIsOptional = component?.options?.required === false;
+
+ if (!filesArePopulated && componentIsOptional) {
+ logger.warn(
+ loggerIdentifier,
+ `${fieldName} is optional, user skipped uploading a file. ${
+ originalFilenameLocation
+ ? `Using ${originalFilenameLocation} instead`
+ : ""
+ }`
+ );
+
+ request.payload[fieldName] = originalFilenameLocation;
+ continue;
+ }
+
+ const invalidFile = values.find(
+ (value) => !uploadService.validateContentType(value)
+ );
+
+ if (invalidFile) {
+ logger.error(
+ loggerIdentifier,
+ `User uploaded file with invalid content type or empty field for ${fieldName}, attempting to find previous upload`
+ );
+
+ if (!originalFilenameLocation) {
+ logger.error(
+ loggerIdentifier,
+ `User uploaded invalid content type or empty field for ${fieldName}, and has no previous upload for field. Deleting ${fieldName} from payload`
+ );
+ delete request.payload[fieldName];
+ erroredFields.push(fieldName);
+ }
+
+ if (originalFilenameLocation) {
+ logger.warn(
+ loggerIdentifier,
+ `User uploaded invalid content type or empty field for ${fieldName}, using existing upload ${originalFilenameLocation} instead`
+ );
+ request.payload[fieldName] = originalFilenameLocation;
+ }
+
+ continue;
+ }
+
+ validFields.push([fieldName, values]);
+ }
+
+ if (erroredFields) {
+ request.pre.errors = erroredFields.map((field) =>
+ uploadService.invalidFileTypeError(field)
+ );
+ }
+
+ return validFields;
+}
diff --git a/runner/src/server/plugins/engine/views/components/fileuploadfield.html b/runner/src/server/plugins/engine/views/components/fileuploadfield.html
index fab533da45..085a0a2e86 100644
--- a/runner/src/server/plugins/engine/views/components/fileuploadfield.html
+++ b/runner/src/server/plugins/engine/views/components/fileuploadfield.html
@@ -2,7 +2,7 @@
{% macro FileUploadField(component) %}
{{ govukFileUpload(component.model) }}
{% if component.model.value %}
- You have already provided this file.
+ You previously uploaded the file ‘{{component.model.value}}’
{% endif %}
diff --git a/runner/src/server/services/upload/uploadService.ts b/runner/src/server/services/upload/uploadService.ts
index 79f49e0907..fb2b12494e 100644
--- a/runner/src/server/services/upload/uploadService.ts
+++ b/runner/src/server/services/upload/uploadService.ts
@@ -5,6 +5,16 @@ import { get, post } from "../httpService";
import { HapiRequest, HapiResponseToolkit, HapiServer } from "../../types";
type Payload = HapiRequest["payload"];
+type HapiReadableStream = ReadableStream & {
+ hapi: {
+ filename: string;
+ headers: {
+ "content-disposition": string;
+ "content-type": string;
+ };
+ };
+};
+export type ReadableStreamEntry = [string, Array];
const parsedError = (key: string, error?: string) => {
return {
@@ -32,32 +42,63 @@ export class UploadService {
this.logger = server.logger;
}
+ validContentTypes = new Set(["image/jpeg", "application/pdf", "image/png"]);
+ validFiletypes = ["jpg", "jpeg", "png", "pdf"];
+ validFiletypesString = `The selected file for "%s" must be a ${this.validFiletypes
+ .slice(0, -1)
+ .join(", ")} or ${this.validFiletypes.slice(-1)}`;
+
get fileSizeLimit() {
return config.maxClientFileSize;
}
- get validFiletypes(): ["jpg", "jpeg", "png", "pdf"] {
- return ["jpg", "jpeg", "png", "pdf"];
+ isReadable(value: any | HapiReadableStream) {
+ return value?.isReadable;
+ }
+
+ payloadEntryIsFile(entry: [string, any]) {
+ const [key, value] = entry;
+ if (!value) {
+ return;
+ }
+
+ if (Array.isArray(value)) {
+ return value.every((v) => v?.readable);
+ }
+ return value?.readable;
+ }
+
+ convertFileValueToArray(
+ entry: [string, HapiReadableStream | HapiReadableStream[]]
+ ): [string, HapiReadableStream[]] {
+ const [key, value] = entry;
+ if (Array.isArray(value)) {
+ return [key, value];
+ }
+ return [key, [value]];
+ }
+
+ fileStreamsFromPayload(payload: Payload): ReadableStreamEntry[] {
+ if (!payload) {
+ return [];
+ }
+ const entries = Object.entries(payload);
+ const payloadFileEntries = entries.filter(this.payloadEntryIsFile);
+ return payloadFileEntries.map(this.convertFileValueToArray);
}
- fileStreamsFromPayload(payload: Payload) {
- return Object.entries(payload).filter(([_key, value]: [string, any]) => {
- if (value) {
- if (Array.isArray(value)) {
- return value.every((nv) => !!nv._data && nv._data.length > 1);
- }
- return !!value._data && value._data.length > 1;
- }
- return false;
+ fileSummary(files: ReadableStreamEntry[]) {
+ return files.map(([fieldName, value]) => {
+ return `${value?.length ?? 0} files for field ${fieldName}`;
});
}
- async uploadDocuments(locations: any[]) {
+ async uploadDocuments(streams: any[]) {
const form = new FormData();
- for (const location of locations) {
- form.append("files", location, {
- filename: location.hapi.filename,
- contentType: location.hapi.headers["content-type"],
+ for (const stream of streams) {
+ form.append("files", stream, {
+ filename: stream.hapi.filename,
+ contentType: stream.hapi.headers["content-type"],
});
}
@@ -103,143 +144,24 @@ export class UploadService {
return h.continue;
}
- async handleUploadRequest(
- request: HapiRequest,
- h: HapiResponseToolkit,
- page: any
- ) {
- const { cacheService } = request.services([]);
- const state = await cacheService.getState(request);
- const originalFilenames = state?.originalFilenames ?? {};
-
- let files: [string, any][] = [];
-
- if (request.payload !== null) {
- files = this.fileStreamsFromPayload(request.payload);
- }
-
- /**
- * If there are no valid file(buffer)s, reassign any empty buffers with empty string
- * allows bypassing of file upload for whatever reason it doesn't work.
- */
- if (!files.length && request.payload) {
- const fields = Object.entries(request.payload);
-
- for (const [key, value] of fields) {
- if (value._data) {
- const originalFilename = originalFilenames[key];
- request.payload[key] =
- (originalFilename && originalFilename.location) || "";
- }
- }
-
- return h.continue;
- }
-
- /**
- * files is an array of tuples containing key and value.
- * value may be an array of file data where multiple files have been uploaded
- */
-
- for (const file of files) {
- const key = file[0];
- const previousUpload = originalFilenames[key] || {};
-
- let values: any;
-
- if (Array.isArray(file[1])) {
- values = file[1];
- } else {
- values = [file[1]];
- }
-
- const validFiles = (
- await Promise.all(
- values.map(async (fileValue) => {
- const extension = fileValue.hapi.filename.split(".").pop();
- if (
- !this.validFiletypes.includes((extension || "").toLowerCase())
- ) {
- request.pre.errors = [
- ...(h.request.pre.errors || []),
- parsedError(
- key,
- `The selected file for "%s" must be a ${this.validFiletypes
- .slice(0, -1)
- .join(", ")} or ${this.validFiletypes.slice(-1)}`
- ),
- ];
- return null;
- }
- try {
- return fileValue;
- } catch (e) {
- request.pre.errors = [
- ...(h.request.pre.errors || []),
- parsedError(key, e),
- ];
- }
- })
- )
- ).filter((value) => !!value);
-
- if (validFiles.length === values.length) {
- try {
- const { error, location, warning } = await this.uploadDocuments(
- validFiles
- );
- if (location) {
- originalFilenames[key] = { location };
- request.payload[key] = location;
- request.pre.warning = warning;
- }
- if (error) {
- request.pre.errors = [
- ...(h.request.pre.errors || []),
- parsedError(key, error),
- ];
- }
- } catch (e) {
- if (e.data?.res) {
- const { error } = this.parsedDocumentUploadResponse(e.data);
- request.pre.errors = [
- ...(h.request.pre.errors || []),
- parsedError(key, error),
- ];
- } else if (e.code === "EPIPE") {
- // ignore this error, it happens when the request is responded to by the doc upload service before the
- // body has finished being sent. A valid response is still received.
- request.server.log(
- ["info", "documentupload"],
- `Ignoring EPIPE response: ${e.message}`
- );
- } else {
- request.server.log(
- ["error", "documentupload"],
- `Error uploading document: ${e.message}`
- );
- request.pre.errors = [
- ...(h.request.pre.errors || []),
- parsedError(key, e),
- ];
- }
- }
- } else {
- request.payload[key] = previousUpload.location || "";
- }
-
- if (request.pre.errors && request.pre.errors.length) {
- delete request.payload[key];
- }
- }
-
- await cacheService.mergeState(request, { originalFilenames });
+ validateContentType(file: HapiReadableStream) {
+ return this.validContentTypes.has(file?.hapi?.headers?.["content-type"]);
+ }
- return h.continue;
+ invalidFileTypeError(fieldName: string) {
+ return parsedError(fieldName, this.validFiletypesString);
}
downloadDocuments(paths: string[]) {
const promises = paths.map((path) => get(path, {}));
return Promise.all(promises);
}
+
+ getLoggerIdentifier(request: HapiRequest, mergedObject: Object) {
+ return {
+ id: request.yar.id,
+ path: request.path,
+ ...mergedObject,
+ };
+ }
}
diff --git a/runner/src/server/views/partials/summary-row.html b/runner/src/server/views/partials/summary-row.html
index 703726868b..be137fa78a 100644
--- a/runner/src/server/views/partials/summary-row.html
+++ b/runner/src/server/views/partials/summary-row.html
@@ -6,7 +6,7 @@
{% if item.value %}
{% if item.type == 'FileUploadField' %}
- Uploaded
+ {{ item.filename if item.filename else "Uploaded" }}
{% else %}
{{item.value}}
{% endif %}
diff --git a/runner/test/cases/server/upload.json b/runner/test/cases/server/upload.json
index 8a8cbe4eb1..4471a2438e 100644
--- a/runner/test/cases/server/upload.json
+++ b/runner/test/cases/server/upload.json
@@ -51,6 +51,24 @@
}
]
},
+ {
+ "path": "/favourite",
+ "components": [
+ {
+ "type": "TextField",
+ "name": "favouriteEgg",
+ "title": {
+ "en": "What is your favourite egg"
+ }
+ }
+ ],
+ "section": "personalDetails",
+ "next": [
+ {
+ "path": "/summary"
+ }
+ ]
+ },
{
"path": "/summary",
"controller": "./pages/summary.ts",
diff --git a/runner/test/cases/server/upload.test.js b/runner/test/cases/server/upload.test.js
index da991d7fe6..2cf5d77052 100644
--- a/runner/test/cases/server/upload.test.js
+++ b/runner/test/cases/server/upload.test.js
@@ -53,10 +53,17 @@ suite("uploads", () => {
return [
[
"file1",
- {
- hapi: { filename: "file.jpg" },
- _data: fs.readFileSync(path.join(__dirname, "dummy.pdf")),
- },
+ [
+ {
+ hapi: {
+ filename: "file.jpg",
+ headers: {
+ "content-type": "application/pdf",
+ },
+ },
+ _data: fs.readFileSync(path.join(__dirname, "dummy.pdf")),
+ },
+ ],
],
];
});
@@ -98,7 +105,7 @@ suite("uploads", () => {
method: "POST",
url: "/upload/upload-file",
headers: form.getHeaders(),
- payload: null,
+ payload: form.getBuffer(),
};
const response = await server.inject(options);
@@ -112,15 +119,15 @@ suite("uploads", () => {
test("request with file upload field containing invalid file type returns with error message", async () => {
restore();
stub(UploadService.prototype, "fileStreamsFromPayload").callsFake(() => {
- return [
- [
- "file1",
- {
- hapi: { filename: "file.test" },
- _data: fs.readFileSync(path.join(__dirname, "dummy.pdf")),
- },
- ],
- ];
+ const buffer = fs.readFileSync(path.join(__dirname, "dummy.pdf"));
+ buffer.hapi = {
+ filename: "dummy.pdf",
+ headers: {
+ "content-type": "image/gif",
+ },
+ };
+
+ return [["file1", [buffer]]];
});
const form = new FormData();
@@ -139,4 +146,23 @@ suite("uploads", () => {
'The selected file for "Passport photo" must be a jpg, jpeg, png or pdf'
);
});
+
+ test("Request with no files skips all prehandlers", async () => {
+ restore();
+
+ const form = new FormData();
+ form.append("favouriteEgg", "scrambled");
+ const options = {
+ method: "POST",
+ url: "/upload/favourite",
+ headers: form.getHeaders(),
+ payload: form.getBuffer(),
+ };
+ const response = await server.inject(options);
+
+ const $ = cheerio.load(response.payload);
+ expect(response.statusCode).to.equal(302);
+ expect(response.headers).to.include("location");
+ expect(response.headers.location).to.equal("/upload/summary");
+ });
});