diff --git a/apps/paf/behaviours/disable-file-upload.js b/apps/paf/behaviours/disable-file-upload.js new file mode 100644 index 00000000..f9210b99 --- /dev/null +++ b/apps/paf/behaviours/disable-file-upload.js @@ -0,0 +1,13 @@ +module.exports = superclass => class extends superclass { + locals(req, res) { + const locals = super.locals(req, res); + const images = req.sessionModel.get('images'); + if (images && images.length >= 3) { + // disable file upload if attachment limit reached. + req.form.options.fields['other-info-file-upload'].attributes = [{attribute: 'disabled'}]; + return locals; + } + req.form.options.fields['other-info-file-upload'].attributes = []; + return locals; +} +} diff --git a/apps/paf/behaviours/limit-documents.js b/apps/paf/behaviours/limit-documents.js new file mode 100644 index 00000000..833fc911 --- /dev/null +++ b/apps/paf/behaviours/limit-documents.js @@ -0,0 +1,16 @@ +module.exports = superclass => class LimitDocs extends superclass { + validate(req, res, next) { + const images = req.sessionModel.get('images'); + if (images && images.length >= 3 && req.form.values['other-info-file-uploads-add-another'] === 'yes') { + return next({ + 'other-info-file-uploads-add-another': new this.ValidationError( + 'other-info-file-uploads-add-another', + { + type: 'tooMany' + } + ) + }); + } super.validate(req, res, next); + return next; + } +}; diff --git a/apps/paf/behaviours/remove-file.js b/apps/paf/behaviours/remove-file.js new file mode 100644 index 00000000..31401a45 --- /dev/null +++ b/apps/paf/behaviours/remove-file.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = superclass => class extends superclass { + configure(req, res, next) { + if (req.query.delete) { + const images = req.sessionModel.get('images') || []; + const remaining = images.filter(i => i.id !== req.query.delete); + req.log('info', `Reference: ${req.sessionModel.get('reference')}, Removing image: ${req.query.delete}`); + req.sessionModel.set('images', remaining); + const path = req.baseUrl + req.path; + return res.redirect(path); + } + return super.configure(req, res, next); + } +}; diff --git a/apps/paf/behaviours/save-file.js b/apps/paf/behaviours/save-file.js new file mode 100644 index 00000000..13bff411 --- /dev/null +++ b/apps/paf/behaviours/save-file.js @@ -0,0 +1,41 @@ +'use strict'; + +const _ = require('lodash'); +const Model = require('../models/file-upload'); + +module.exports = name => superclass => class extends superclass { + process(req) { + if (req.files && req.files[name]) { + // set image name on values for filename extension validation + // N:B validation controller gets values from + // req.form.values and not on req.files + req.form.values[name] = req.files[name].name; + req.log('info', `Reference: ${req.sessionModel.get('reference')}, + Processing image: ${req.form.values[name]}`); + } + super.process.apply(this, arguments); + } + + locals(req, res, next) { + if (!Object.keys(req.form.errors).length) { + req.form.values['other-info-file-upload'] = null; + } + return super.locals(req, res, next); + } + + saveValues(req, res, next) { + const images = req.sessionModel.get('images') || []; + if (req.files && req.files[name]) { + req.log('info', `Reference: ${req.sessionModel.get('reference')}, Saving image: ${req.files[name].name}`); + const image = _.pick(req.files[name], ['name', 'data', 'mimetype']); + const model = new Model(image); + return model.save() + .then(() => { + req.sessionModel.set('images', [...images, model.toJSON()]); + return super.saveValues(req, res, next); + }) + .catch(next); + } + return super.saveValues.apply(this, arguments); + } +}; diff --git a/apps/paf/fields/index.js b/apps/paf/fields/index.js index 8c23afad..e14508f2 100644 --- a/apps/paf/fields/index.js +++ b/apps/paf/fields/index.js @@ -221,7 +221,7 @@ module.exports = { dependent: { field: 'vehicle-type', value: 'cars' - }, + } }, 'crime-hgv-group': { mixin: 'radio-group', @@ -238,7 +238,7 @@ module.exports = { dependent: { field: 'vehicle-type', value: 'hgvs' - }, + } }, 'crime-lorry-group': { mixin: 'radio-group', @@ -252,7 +252,7 @@ module.exports = { dependent: { field: 'vehicle-type', value: 'lorries' - }, + } }, 'crime-van-group': { mixin: 'radio-group', @@ -268,7 +268,7 @@ module.exports = { dependent: { field: 'vehicle-type', value: 'vans' - }, + } }, 'boat-type': { isPageHeading: true, @@ -320,7 +320,7 @@ module.exports = { dependent: { field: 'boat-type', value: 'carriers' - }, + } }, 'crime-general-cargo-group': { mixin: 'radio-group', @@ -334,7 +334,7 @@ module.exports = { dependent: { field: 'boat-type', value: 'general-cargos' - }, + } }, 'crime-vessel-group': { mixin: 'radio-group', @@ -349,7 +349,7 @@ module.exports = { dependent: { field: 'boat-type', value: 'vessels' - }, + } }, 'boat-name': { mixin: 'input-text' @@ -913,7 +913,7 @@ module.exports = { dependent: { field: 'report-person-transport-type', value: 'cars' - }, + } }, 'report-person-transport-hgv-group': { mixin: 'radio-group', @@ -930,7 +930,7 @@ module.exports = { dependent: { field: 'report-person-transport-type', value: 'hgv' - }, + } }, 'report-person-transport-lorry-group': { mixin: 'radio-group', @@ -944,7 +944,7 @@ module.exports = { dependent: { field: 'report-person-transport-type', value: 'lorries' - }, + } }, 'report-person-transport-van-group': { mixin: 'radio-group', @@ -960,7 +960,7 @@ module.exports = { dependent: { field: 'report-person-transport-type', value: 'vans' - }, + } }, 'report-person-transport-make': { mixin: 'input-text' @@ -1167,6 +1167,16 @@ module.exports = { value: 'yes' } }, + 'other-info-file-upload': { + mixin: 'input-file', + className: 'govuk-file-upload', + attributes: [] + }, + 'add-other-info-file-upload': { + isPageHeading: true, + mixin: 'radio-group', + options: ['yes', 'no'] + }, 'about-you-first-name': { mixin: 'input-text' }, diff --git a/apps/paf/index.js b/apps/paf/index.js index 708a372c..eac8b3f2 100644 --- a/apps/paf/index.js +++ b/apps/paf/index.js @@ -1,4 +1,9 @@ 'use strict'; +const saveImage = require('./behaviours/save-file'); +const removeImage = require('./behaviours/remove-file'); +const CombineAndLoopFields = require('hof').components.combineAndLoopFields; +const limitDocs = require('./behaviours/limit-documents'); +const disableUpload = require('./behaviours/disable-file-upload'); const SummaryPageBehaviour = require('hof').components.summary; const transportBehaviour = require('./behaviours/transport-behaviour'); const Aggregate = require('./behaviours/aggregator'); @@ -356,7 +361,7 @@ module.exports = { field: 'report-person-occupation', value: 'yes' } - }, + } ] }, '/report-person-occupation-type': { @@ -662,6 +667,36 @@ module.exports = { next: '/other-info-file-upload' }, '/other-info-file-upload': { + behaviours: [saveImage('other-info-file-upload'), disableUpload], + fields: ['other-info-file-upload'], + continueOnEdit: true, + forks: [{ + target: '/add-other-info-file-upload', + condition: req => { + if (req.form.values['other-info-file-upload']) { + return true + } + return false; + } + }], + next: '/about-you' + }, + '/add-other-info-file-upload': { + template: 'list-add-looped-files', + behaviours: [CombineAndLoopFields({ + groupName: 'other-info-file-uploads', + fieldsToGroup: [ + 'other-info-file-upload' + ], + groupOptional: true, + removePrefix: 'other-', + combineValuesToSingleField: 'record', + returnTo: '/other-info-file-upload' + }), removeImage, limitDocs], + next: '/about-you', + locals: { + section: 'other-info-file-upload' + } }, '/about-you': { fields: ['how-did-you-find-out-about-the-crime'], diff --git a/apps/paf/models/file-upload.js b/apps/paf/models/file-upload.js new file mode 100644 index 00000000..db94ae8a --- /dev/null +++ b/apps/paf/models/file-upload.js @@ -0,0 +1,75 @@ +'use strict'; + +const url = require('url'); +const Model = require('hof').model; +const uuid = require('uuid').v4; +const config = require('../../../config'); + +module.exports = class UploadModel extends Model { + constructor(...args) { + super(...args); + this.set('id', uuid()); + } + + async save() { + const result = await new Promise((resolve, reject) => { + const attributes = { + url: config.upload.hostname + }; + const reqConf = url.parse(this.url(attributes)); + reqConf.formData = { + document: { + value: this.get('data'), + options: { + filename: this.get('name'), + contentType: this.get('mimetype') + } + } + }; + reqConf.method = 'POST'; + this.request(reqConf, (err, data) => { + if (err) { + return reject(err); + } + resolve(data); + }); + }); + this.set({ url: result.url }); + return this.unset('data'); + } + + auth() { + if (!config.keycloak.token) { + // eslint-disable-next-line no-console + console.error('keycloak token url is not defined'); + return Promise.resolve({ + bearer: 'abc123' + }); + } + const tokenReq = { + url: config.keycloak.token, + form: { + username: config.keycloak.username, + password: config.keycloak.password, + grant_type: 'password', + client_id: config.keycloak.clientId, + client_secret: config.keycloak.secret + }, + method: 'POST' + }; + + return new Promise((resolve, reject) => { + this._request(tokenReq, (err, response) => { + const body = JSON.parse(response.body); + + if (err || body.error) { + return reject(err || new Error(`${body.error} - ${body.error_description}`)); + } + + resolve({ + bearer: JSON.parse(response.body).access_token + }); + }); + }); + } +}; diff --git a/apps/paf/sections/summary-data-sections.js b/apps/paf/sections/summary-data-sections.js index 8e78f576..b06003f9 100644 --- a/apps/paf/sections/summary-data-sections.js +++ b/apps/paf/sections/summary-data-sections.js @@ -221,7 +221,17 @@ module.exports = { 'other-info': [ 'other-info-description', 'other-info-another-crime', - 'other-info-another-crime-description' + 'other-info-another-crime-description', + { + step: '/add-other-info-file-upload', + field: 'images', + parse: (list, req) => { + if (!req.sessionModel.get('images') ) { + return null; + } + return list && list.map(a => a.name).join('\n————————————————\n') || 'None'; + } + } ], 'about-you': [ 'how-did-you-find-out-about-the-crime', diff --git a/apps/paf/translations/src/en/fields.json b/apps/paf/translations/src/en/fields.json index 3b47cd00..c9b5d9c6 100644 --- a/apps/paf/translations/src/en/fields.json +++ b/apps/paf/translations/src/en/fields.json @@ -1087,6 +1087,21 @@ }, "legendClassName": "govuk-fieldset__legend" }, + "other-info-file-upload": { + "hint": "You can add up to 3 documents", + "label": "Attachment" + }, + "other-info-file-uploads-add-another": { + "options": { + "yes": { + "label": "Yes" + }, + "no": { + "label": "No" + } + }, + "legendClassName": "govuk-fieldset__legend" + }, "persons": { "label": "Additional People", "changeLinkDescription": "Additional People" @@ -1356,5 +1371,8 @@ }, "when-to-contact": { "label": "When would you like us to contact you?" + }, + "images": { + "label": "Attachments" } } diff --git a/apps/paf/translations/src/en/pages.json b/apps/paf/translations/src/en/pages.json index 7b1e11f8..91c178ee 100644 --- a/apps/paf/translations/src/en/pages.json +++ b/apps/paf/translations/src/en/pages.json @@ -122,8 +122,15 @@ "other-info-description": { "header": "Other information" }, - "other-info-file-upload": { - "header": "Please attach any documents which may help us investigate this crime - TO DO" + "other-info-file-upload":{ + "header": "Please attach any documents which may help us investigate this crime" + }, + "add-other-info-file-upload": { + "header": "Do you need to add any other documents?", + "hint": "You can submit up to 3 attachments" + }, + "other-info-file-uploads": { + "header": "Attachments" }, "about-you": { "header": "About You" diff --git a/apps/paf/translations/src/en/validation.json b/apps/paf/translations/src/en/validation.json index da70b892..8e0d32a2 100644 --- a/apps/paf/translations/src/en/validation.json +++ b/apps/paf/translations/src/en/validation.json @@ -25,5 +25,9 @@ }, "company-website": { "url": "Enter a website address in the correct format, like www.example.com" + }, + "other-info-file-uploads-add-another": { + "required": "Please select an option", + "tooMany": "You can only submit up to 3 attachments." } } diff --git a/apps/paf/views/list-add-looped-files.html b/apps/paf/views/list-add-looped-files.html new file mode 100644 index 00000000..1568645a --- /dev/null +++ b/apps/paf/views/list-add-looped-files.html @@ -0,0 +1,8 @@ +{{{{#t}}pages.add-{{section}}.hint{{/t}}

+ + {{>partials-looped-files}} + + {{/page-content}} +{{/partials-page}} diff --git a/apps/paf/views/other-info-file-upload.html b/apps/paf/views/other-info-file-upload.html new file mode 100644 index 00000000..54676756 --- /dev/null +++ b/apps/paf/views/other-info-file-upload.html @@ -0,0 +1,10 @@ +{{ + {{^values.images}} +

No files added

+ {{/values.images}} + {{#values.images.length}} +

{{#t}}pages.{{field}}.header{{/t}}

+ {{/values.images.length}} + + + {{#values.images}} + + + + + {{/values.images}} + +
{{name}}
Delete
+ diff --git a/apps/paf/views/partials/side-nav.html b/apps/paf/views/partials/side-nav.html index 3a758e2c..7a37753c 100644 --- a/apps/paf/views/partials/side-nav.html +++ b/apps/paf/views/partials/side-nav.html @@ -42,7 +42,7 @@ Organisation
  • - Other Info + Other Information
  • About You diff --git a/assets/scss/app.scss b/assets/scss/app.scss index 440321fa..81a7d900 100644 --- a/assets/scss/app.scss +++ b/assets/scss/app.scss @@ -225,3 +225,9 @@ $navigation-height: 50px; #govuk-header { background-color: black; } +pre.looped-records { + white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; +} + diff --git a/config.js b/config.js index d97d46df..3a43dc1d 100644 --- a/config.js +++ b/config.js @@ -2,8 +2,27 @@ /* eslint no-process-env: 0 */ const env = process.env.NODE_ENV; +const useMocks = process.env.USE_MOCKS === 'true' || !env; module.exports = { env: env, - PRETTY_DATE_FORMAT: 'Do MMMM YYYY' + PRETTY_DATE_FORMAT: 'Do MMMM YYYY', + csp: { + imgSrc: ['data:'] + }, + useMocks: useMocks, + upload: { + maxFileSize: '100mb', + // if mocks set use file service served up by app otherwise use filevault's port 3000 + hostname: !useMocks && process.env.FILE_VAULT_URL ? + process.env.FILE_VAULT_URL : + `http://localhost:${useMocks ? (process.env.PORT || 8080) : 3000}/file` + }, + keycloak: { + token: process.env.KEYCLOAK_TOKEN_URL, + username: process.env.KEYCLOAK_USERNAME, + password: process.env.KEYCLOAK_PASSWORD, + clientId: process.env.KEYCLOAK_CLIENT_ID, + secret: process.env.KEYCLOAK_SECRET + }, }; diff --git a/mock-apis.js b/mock-apis.js new file mode 100644 index 00000000..1d3bbe38 --- /dev/null +++ b/mock-apis.js @@ -0,0 +1,7 @@ +'use strict'; + +const router = require('express').Router(); + +router.use('/file', require('./mocks/image-upload')); + +module.exports = router; diff --git a/mocks/image-upload.js b/mocks/image-upload.js new file mode 100644 index 00000000..87dc4e08 --- /dev/null +++ b/mocks/image-upload.js @@ -0,0 +1,16 @@ +'use strict'; + +const router = require('express').Router(); +const busboy = require('busboy-body-parser'); + +router.use(busboy()); + +router.post('/', (req, res, next) => { + if (req.files.document) { + res.json({url: `http://s3.com/foo/${Math.random()}`}); + } else { + next(new Error('No file uploaded')); + } +}); + +module.exports = router; diff --git a/server.js b/server.js index a71fadc9..eb83e1b7 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,8 @@ const hof = require('hof'); let settings = require('./hof.settings'); const config = require('./config.js'); +const mockAPIs = require('./mock-apis'); +const bodyParser = require('busboy-body-parser'); settings = Object.assign({}, settings, { routes: settings.routes.map(require), @@ -11,6 +13,10 @@ settings = Object.assign({}, settings, { const app = hof(settings); +if (config.useMocks) { + app.use(mockAPIs); +} + app.use((req, res, next) => { // Set HTML Language res.locals.htmlLang = 'en'; @@ -19,6 +25,10 @@ app.use((req, res, next) => { next(); }); +if (config.env !== 'test') { + app.use(bodyParser({limit: config.upload.maxFileSize})); +} + if (config.env === 'development' || config.env === 'test') { app.use('/test/bootstrap-session', (req, res) => { const appName = req.body.appName; diff --git a/test/_ui-integration/paf/application.spec.js b/test/_ui-integration/paf/application.spec.js index 7c34a843..6737e45b 100644 --- a/test/_ui-integration/paf/application.spec.js +++ b/test/_ui-integration/paf/application.spec.js @@ -329,10 +329,27 @@ describe('the journey of a paf application', () => { expect(response.text).to.contain('Found. Redirecting to /paf/other-info-file-upload'); }); - it('goes to the about-you page', async () => { + it('goes to the add-other-info-file-upload page', async () => { const URI = '/other-info-file-upload'; await initSession(URI); - const response = await passStep(URI, {}); + const response = await passStep(URI, { + 'other-info-file-upload': { + name: 'story.png', + encoding: '7bit', + mimetype: 'png', + truncated: false, + size: 144148 + } + }); + expect(response.text).to.contain('Found. Redirecting to /paf/add-other-info-file-upload'); + }); + + it('goes to the about-you page', async () => { + const URI = '/add-other-info-file-upload'; + await initSession(URI); + const response = await passStep(URI, { + 'other-info-file-uploads-add-another': 'no' + }); expect(response.text).to.contain('Found. Redirecting to /paf/about-you'); }); diff --git a/test/_unit/behaviours/limit-documents.spec.js b/test/_unit/behaviours/limit-documents.spec.js new file mode 100644 index 00000000..6662ae1b --- /dev/null +++ b/test/_unit/behaviours/limit-documents.spec.js @@ -0,0 +1,55 @@ +'use strict'; + +const Behaviour = require('../../../apps/paf/behaviours/limit-documents'); +const Controller = require('hof').controller; + + +describe("apps/paf 'limit-documents' behaviour should ", () => { + let behaviour; + let req; + let res; + + beforeEach(done => { + req = reqres.req(); + res = reqres.res(); + + const LimitDocs = Behaviour(Controller); + behaviour = new LimitDocs({ template: 'index', route: '/index' }); + behaviour._configure(req, res, done); + }); + + describe("The limit-documents '.validate' method", () => { + it('returns an too many error if 3 files have already been added', () => { + req.form.values['images'] = [{ + name: 'bass.png', + encoding: '7bit', + mimetype: 'png', + truncated: false, + size: 144148 + }, + { + name: 'treble.png', + encoding: '7bit', + mimetype: 'png', + truncated: false, + size: 144149 + }, + { + name: 'quaver.png', + encoding: '7bit', + mimetype: 'png', + truncated: false, + size: 144150 + } + ]; + + req.sessionModel.set('images', req.form.values['images']); + const images = req.sessionModel.get('images') + req.form.values['other-info-file-uploads-add-another'] = 'yes' + behaviour.validate(req, res, err => { + err['other-info-file-uploads-add-another'].should.be.an.instanceof(behaviour.ValidationError); + err['other-info-file-uploads-add-another'].should.have.property('type').and.equal('tooMany'); + }); + }); + }); +}); diff --git a/test/_unit/behaviours/remove-file.spec.js b/test/_unit/behaviours/remove-file.spec.js new file mode 100644 index 00000000..3d52dd4c --- /dev/null +++ b/test/_unit/behaviours/remove-file.spec.js @@ -0,0 +1,72 @@ +'use strict'; + +const { expect } = require('chai'); +const Behaviour = require('../../../apps/paf/behaviours/remove-file'); + +describe("apps/paf 'remove-file' behaviour should ", () => { + it('export a function', () => { + expect(Behaviour).to.be.a('function'); + }); + + class Base { + constructor() {} + configure() {} + } + + let req; + let res; + let instance; + const next = 'foo'; + + const images = [ + { + name: 'violin.png', + mimetype: 'image/png', + id: 'a1', + url: 'http://s3.com/foo/0.4283270873546463' + }, + { + name: 'piano.png', + mimetype: 'image/png', + id: 'b2', + url: 'http://s3.com/foo/0.4283270873546463' + } + ]; + + beforeEach(() => { + req = reqres.req(); + res = reqres.res(); + }); + describe("The 'configure' method ", () => { + beforeEach(() => { + sinon.stub(Base.prototype, 'configure').returns(req, res, next); + instance = new (Behaviour(Base))(); + }); + + it('should be called even if no images are to be removed', () => { + instance.configure(req, res, next); + expect(Base.prototype.configure).to.have.been.called; + expect(req.sessionModel.get('images')); + }); + + it('should remove a file if an ID is passed to it', () => { + req.sessionModel.set('images', images); + req.query.delete = images[0].id; + instance.configure(req, res, next); + const remainginImages = req.sessionModel.get('images'); + remainginImages.map(image => { + expect(image.id).to.not.equal(images[0].id); + }); + }); + + it('should redirect if a file is removed', () => { + req.sessionModel.set('images', images); + req.query.delete = images[0].id; + instance.configure(req, res, next); + expect(res.redirect).to.be.called; + }); + afterEach(() => { + Base.prototype.configure.restore(); + }); + }); +}); diff --git a/test/_unit/behaviours/save-file.spec.js b/test/_unit/behaviours/save-file.spec.js new file mode 100644 index 00000000..ec5a1a40 --- /dev/null +++ b/test/_unit/behaviours/save-file.spec.js @@ -0,0 +1,119 @@ +'use strict'; + +const expect = chai.expect; +const Behaviour = require('../../../apps/paf/behaviours/save-file'); + +describe("apps/paf 'save-file' behaviour should ", () => { + it('export a function', () => { + expect(Behaviour).to.be.a('function'); + }); + + class Base { + process() {} + locals() {} + saveValues() {} + } + + let req; + let res; + let next; + + let instance; + + const imageFiles = { + image: { + name: 'guitar.png', + encoding: '7bit', + mimetype: 'png', + truncated: false, + size: 144148 + } + }; + + beforeEach(() => { + req = reqres.req(); + res = reqres.res(); + req.files = imageFiles; + }); + + describe("The save-file ' process ' method", () => { + before(() => { + sinon.stub(Base.prototype, 'process'); + instance = new (Behaviour('image')(Base))(); + }); + + it('should be called ', () => { + instance.process(req); + expect(Base.prototype.process).to.have.been.called; + }); + + it('should have a file attached to it', () => { + req.files = imageFiles; + instance.process(req); + expect(req.files).to.eql(imageFiles); + }); + + it('should add files to form.values', () => { + req.files.images = imageFiles; + instance.process(req); + expect(req.form.values.image).to.eql('guitar.png'); + }); + + after(() => { + Base.prototype.process.restore(); + }); + }); + + describe("The save-file ' locals ' method", () => { + before(() => { + sinon.stub(Base.prototype, 'locals').returns(req, res, next); + instance = new (Behaviour('name')(Base))(); + }); + + it('should be called ', () => { + req.form.errors = {}; + instance.locals(req, res, next); + expect(Base.prototype.locals).to.have.been.called; + }); + + it("should not return null to 'other-info-file-upload' on request form values if errors", () => { + req.form.errors = { error: 'err' }; + instance.locals(req, res, next); + expect(req.form.values['other-info-file-upload']).to.not.eql(null); + expect(req.form.values['other-info-file-upload']).to.eql(); + }); + + it("should return null to 'other-info-file-upload' on request form values if there are no errors", () => { + req.form.errors = {}; + instance.locals(req, res, next); + expect(req.form.values['other-info-file-upload']).to.eql(null); + }); + + after(() => { + Base.prototype.locals.restore(); + }); + }); + + describe("The save-file ' saveValues ' method", () => { + before(() => { + sinon.stub(Base.prototype, 'saveValues').returns(req, res, next); + instance = new (Behaviour('name')(Base))(); + }); + + it('should be called ', () => { + instance.saveValues(req, res, next); + expect(Base.prototype.saveValues).to.have.been.calledOnce; + }); + + it('should attach files to the sessionModel ', () => { + req.sessionModel.set('images', imageFiles); + instance.saveValues(req, res, next); + const sessionModel = req.sessionModel.get('images'); + expect(sessionModel.image.name).to.eql('guitar.png'); + }); + + after(() => { + Base.prototype.saveValues.restore(); + }); + }); +}); diff --git a/test/_unit/models/file-upload.spec.js b/test/_unit/models/file-upload.spec.js new file mode 100644 index 00000000..7d0424c3 --- /dev/null +++ b/test/_unit/models/file-upload.spec.js @@ -0,0 +1,81 @@ + +'use strict'; + +const Model = require('../../../apps/paf/models/file-upload'); +const config = require('../../../config'); + +describe('File Upload Model', () => { + let sandbox; + + beforeEach(function () { + config.upload.hostname = 'http://file-upload.example.com/file/upload'; + sandbox = sinon.createSandbox(); + sandbox.stub(Model.prototype, 'request').yieldsAsync(null, { + api: 'response', + url: '/file/12341212132123?foo=bar' + }); + sandbox.stub(Model.prototype, 'auth').returns(new Promise(resolve => { + resolve({bearer: 'myaccesstoken'}); + })); + }); + + afterEach(() => sandbox.restore()); + + describe('save', () => { + it('returns a promise', () => { + const model = new Model(); + const response = model.save(); + expect(response).to.be.an.instanceOf(Promise); + }); + + it('makes a call to file upload api', () => { + const model = new Model(); + const response = model.save(); + return response.then(() => { + expect(model.request).to.have.been.calledOnce; + expect(model.request).to.have.been.calledWith(sinon.match({ + method: 'POST', + host: 'file-upload.example.com', + path: '/file/upload', + protocol: 'http:' + })); + }); + }); + + it('resolves with response from api endpoint', async () => { + const model = new Model(); + const response = await model.save(); + return expect(response.attributes.url).to.equal('/file/12341212132123?foo=bar'); + }); + + it('rejects if api call fails', () => { + const model = new Model(); + const err = new Error('test error'); + model.request.yieldsAsync(err); + const response = model.save(); + return expect(response).to.be.rejectedWith(err); + }); + + it('adds a formData property to api request with details of uploaded file', () => { + const uploadedFile = new Model({ + data: 'foo', + name: 'myfile.png', + mimetype: 'image/png' + }); + const response = uploadedFile.save(); + return response.then(() => { + expect(uploadedFile.request).to.have.been.calledWith(sinon.match({ + formData: { + document: { + value: 'foo', + options: { + filename: 'myfile.png', + contentType: 'image/png' + } + } + } + })); + }); + }); + }); +}); diff --git a/test/_unit/sections/summary-data-sections.spec.js b/test/_unit/sections/summary-data-sections.spec.js index 14058f9e..b0dfefdb 100644 --- a/test/_unit/sections/summary-data-sections.spec.js +++ b/test/_unit/sections/summary-data-sections.spec.js @@ -334,7 +334,8 @@ describe('PAF Summary Data Sections', () => { const expectedFields = [ 'other-info-description', 'other-info-another-crime', - 'other-info-another-crime-description' + 'other-info-another-crime-description', + 'images' ]; const result = areOrderedEqual(sectionFields, expectedFields); expect(result).to.be.true; diff --git a/test/_unit/server.spec.js b/test/_unit/server.spec.js index 029bad37..71ed477e 100644 --- a/test/_unit/server.spec.js +++ b/test/_unit/server.spec.js @@ -76,7 +76,7 @@ describe('Server.js app file', () => { useStub.callCount.should.equal(2); }); - it('should call the app use method once if env set to anything else', () => { + it('should call the app use method twice if env set to anything else', () => { const use = sinon.stub(); const hof = () => ({ use }); @@ -85,7 +85,7 @@ describe('Server.js app file', () => { './config': { env: 'production' } }); - use.should.have.been.calledOnce; + use.should.have.been.calledTwice; }); });