Skip to content

Commit

Permalink
9068 Add encryption/locked validation and component tests to FileUplo…
Browse files Browse the repository at this point in the history
…adPreview (#246)

* Add FileUploadPreview component tests

* 9068 updates based on PR feedback

* 9068 Add encryption check and test

* 9068 Updates based on PR feedback

* 9068 Add content locked validation and corresponding test
  • Loading branch information
argush3 authored Sep 27, 2021
1 parent e3242b8 commit e59afe2
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 6 deletions.
1 change: 0 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ module.exports = {
},
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transformIgnorePatterns: []

}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "business-create-ui",
"version": "2.1.2",
"version": "2.1.3",
"private": true,
"appName": "Create UI",
"sbcName": "SBC Common Components",
Expand Down
14 changes: 12 additions & 2 deletions src/components/common/FileUploadPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ export default class FileUploadPreview extends Mixins(DocumentMixin) {
this.customErrorMessages = []
let isValid = this.$refs.fileUploadInput.validate()
// only perform page size validation when other validation has passed
if (isValid) {
let pageSizeIsValid = await this.validatePageSize(file)
if (isValid && file) {
if (typeof file.arrayBuffer === 'undefined') { return true }
const fileInfo = await this.retrieveFileInfo(file)
if (fileInfo.isEncrypted) {
this.customErrorMessages = ['File must be unencrypted']
return false
}
if (fileInfo.isContentLocked) {
this.customErrorMessages = ['File content cannot be locked']
return false
}
const pageSizeIsValid = await this.validatePageSize(file)
if (!pageSizeIsValid) {
// show page size validation error
const pageSizeErrorMsg = this.pageSizeDict[this.pdfPageSize].validationErrorMsg
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/utils-interfaces/pdf-info-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PdfInfoIF {
isEncrypted: boolean
// content is locked when copying, editing or printing of document is restricted
isContentLocked: boolean
}
18 changes: 17 additions & 1 deletion src/mixins/document-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Component, Vue } from 'vue-property-decorator'
import { DocumentUpload } from '@/interfaces'
import { PdfPageSize } from '@/enums'
import pdfjsLib from 'pdfjs-dist/build/pdf'
pdfjsLib.GlobalWorkerOptions.workerSrc = 'public/js/pdf.worker.min.js'
import { PdfInfoIF } from '@/interfaces/utils-interfaces/pdf-info-interface'
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.entry')

@Component({})
export default class DocumentMixin extends Vue {
Expand Down Expand Up @@ -69,4 +70,19 @@ export default class DocumentMixin extends Vue {
(height / pageSizeInfo.pointsPerInch === pageSizeInfo.height)
return isvalidPageSize
}

async retrieveFileInfo (file: File): Promise<PdfInfoIF> {
try {
const pdfBufferData = await file.arrayBuffer()
const pdfData = new Uint8Array(pdfBufferData) // put it in a Uint8Array
const pdf = await pdfjsLib.getDocument({ data: pdfData })
const perms = await pdf.getPermissions()
return { isEncrypted: false, isContentLocked: !!perms }
} catch (e) {
if (e.name === 'PasswordException') {
return { isEncrypted: true, isContentLocked: true }
}
}
return { isEncrypted: false, isContentLocked: false }
}
}
295 changes: 295 additions & 0 deletions tests/unit/FileUploadPreview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// Libraries
import Vue from 'vue'
import Vuetify from 'vuetify'
import { mount, Wrapper } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import FileUploadPreview from '@/components/common/FileUploadPreview.vue'
import { PdfPageSize } from '@/enums'

Vue.use(Vuetify)
const vuetify = new Vuetify({})

const oneMBFile = new File([new ArrayBuffer(1048576)], 'oneMBFile.pdf',
{ type: 'application/pdf' })
const elevenMBFile = new File([new ArrayBuffer(1048576 * 11)], 'elevenMBFile.pdf',
{ type: 'application/pdf' })

// Note: the following arrayBuffer code was needed as jest does not provide arrayBuffer and this is required
// to test the scenarios where the pdf.js library is used
// @ts-ignore
File.prototype.arrayBuffer = File.prototype.arrayBuffer || myArrayBuffer
// @ts-ignore
Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || myArrayBuffer

function myArrayBuffer () {
// this: File or Blob
return new Promise((resolve) => {
let fr = new FileReader()
fr.onload = () => {
resolve(fr.result)
}
// @ts-ignore
fr.readAsArrayBuffer(this)
})
}

/**
* Utility method to get around with the timing issues
*/
async function waitForUpdate (numTimesToFlushPromises) {
await Vue.nextTick()
for (let i = 0; i < numTimesToFlushPromises; i++) {
await flushPromises()
}
await Vue.nextTick()
}

describe('FileUploadPreview component', () => {
let inputValueGet
let inputValueSet
let inputValue = ''
let inputFilesGet

// Note: The DataTransfer object can be used to assign files to the file input but this isn't supported
// by JSDOM yet. The following(setFileInput and the code in beforeEach functions) code was required
// in order to set the file associated with the file input.
function setupFileInput (fileInput: Wrapper<Vue>) {
Object.defineProperty(fileInput.element, 'files', {
get: inputFilesGet
})
Object.defineProperty(fileInput.element, 'value', {
get: inputValueGet,
set: inputValueSet
})
}

beforeEach(() => {
inputFilesGet = jest.fn()
inputValueGet = jest.fn().mockReturnValue(inputValue)
inputValueSet = jest.fn().mockImplementation(v => {
inputValue = v
})
})

it('displays file upload preview component', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { maxSize: 10 * 1024 },
vuetify
})

expect(wrapper.find('.file-upload-preview').exists()).toBe(true)
expect(wrapper.find('.file-upload-preview input[type="file"]').exists()).toBe(true)
expect(wrapper.find('.file-upload-preview button').exists()).toBe(true)
expect(wrapper.find('.v-messages__message').text()).toEqual('File must be a PDF. Maximum 10MB.')
wrapper.destroy()
})

it('accepts when file is not required and not provided', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { inputFile: null, isRequired: false },
vuetify
})
const fileInput = wrapper.find('.file-upload-preview input[type="file"]')
fileInput.trigger('change')
await Vue.nextTick()

expect(wrapper.find('.error--text .v-messages__message').exists()).toBeFalsy()

wrapper.destroy()
})

it('rejects when file is required and not provided', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { inputFile: null },
vuetify
})
const fileInput = wrapper.find('.file-upload-preview input[type="file"]')
fileInput.trigger('change')
await Vue.nextTick()

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('File is required')

wrapper.destroy()
})

it('accepts when file size is below max size', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { maxSize: 10 * 1024 },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = oneMBFile.name
inputFilesGet.mockReturnValue([oneMBFile])
fileInput.trigger('change')
await Vue.nextTick()

expect(wrapper.find('.error--text .v-messages__message').exists()).toBeFalsy()

wrapper.destroy()
})

it('rejects when max file size is exceeded', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { maxSize: 10 * 1024 },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = elevenMBFile.name
inputFilesGet.mockReturnValue([elevenMBFile])
fileInput.trigger('change')
await Vue.nextTick()

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('Exceeds maximum 10 MB file size')

wrapper.destroy()
})

it('correctly displays custom error message', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: {},
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = oneMBFile.name
inputFilesGet.mockReturnValue([oneMBFile])
wrapper.setProps({ customErrorMessage: 'test custom error message' })
await flushPromises()
await Vue.nextTick()

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('test custom error message')

wrapper.destroy()
})

it('accepts when pdf page size is accepted size', async () => {
const fs = require('fs')
const data = fs.readFileSync('./tests/unit/test-data/letterSize.pdf', 'utf8')
const letterSizePdf = new File([data], 'letterSize.pdf', { type: 'application/pdf' })
const wrapper = mount(FileUploadPreview, {
propsData: { pdfPageSize: PdfPageSize.LETTER_SIZE },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = letterSizePdf.name
inputFilesGet.mockReturnValue([letterSizePdf])
fileInput.trigger('change')
await waitForUpdate(3)

expect(wrapper.find('.error--text .v-messages__message').exists()).toBeFalsy()

wrapper.destroy()
}, 30000)

it('rejects when pdf page size is not accepted size', async () => {
const fs = require('fs')
const data = fs.readFileSync('./tests/unit/test-data/nonLetterSize.pdf', 'utf8')
const nonLetterSizePdf = new File([data], 'nonLetterSize.pdf', { type: 'application/pdf' })
const wrapper = mount(FileUploadPreview, {
propsData: { pdfPageSize: PdfPageSize.LETTER_SIZE },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = nonLetterSizePdf.name
inputFilesGet.mockReturnValue([nonLetterSizePdf])
fileInput.trigger('change')

await waitForUpdate(5)

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('Document must be set to fit onto 8.5” x 11” letter-size paper')

wrapper.destroy()
}, 30000)

it('rejects encrypted files', async () => {
const fs = require('fs')
const data = fs.readFileSync('./tests/unit/test-data/encrypted.pdf', 'utf8')
const encryptedPdf = new File([data], 'encrypted.pdf', { type: 'application/pdf' })
const wrapper = mount(FileUploadPreview, {
propsData: { pdfPageSize: PdfPageSize.LETTER_SIZE },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = encryptedPdf.name
inputFilesGet.mockReturnValue([encryptedPdf])
fileInput.trigger('change')

await waitForUpdate(3)

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('File must be unencrypted')

wrapper.destroy()
})

it('rejects copy, print and edit locked file', async () => {
const fs = require('fs')
const data = fs.readFileSync('./tests/unit/test-data/copyPrintEditContentLocked.pdf', 'utf8')
const encryptedPdf =
new File([data], 'copyPrintEditContentLocked.pdf', { type: 'application/pdf' })
const wrapper = mount(FileUploadPreview, {
propsData: { pdfPageSize: PdfPageSize.LETTER_SIZE },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = encryptedPdf.name
inputFilesGet.mockReturnValue([encryptedPdf])
fileInput.trigger('change')

await waitForUpdate(3)

const messages = wrapper.findAll('.error--text .v-messages__message')
expect(messages.length).toBe(1)
expect(messages.at(0).text()).toBe('File content cannot be locked')

wrapper.destroy()
})

it('fileSelected event emitted when file is selected', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { maxSize: 10 * 1024 },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = oneMBFile.name
inputFilesGet.mockReturnValue([oneMBFile])
fileInput.trigger('change')
await waitForUpdate(3)
expect(wrapper.emitted('fileSelected').pop()[0]).toEqual(oneMBFile)

wrapper.destroy()
})

it('isFileValid event emitted when file is selected', async () => {
const wrapper = mount(FileUploadPreview, {
propsData: { maxSize: 10 * 1024 },
vuetify
})
const fileInput = wrapper.find('input[type="file"]')
setupFileInput(fileInput)
inputValue = oneMBFile.name
inputFilesGet.mockReturnValue([oneMBFile])
fileInput.trigger('change')
await waitForUpdate(3)

expect(wrapper.emitted('isFileValid').pop()[0]).toEqual(true)

wrapper.destroy()
})
})
Binary file not shown.
Binary file added tests/unit/test-data/encrypted.pdf
Binary file not shown.
Binary file added tests/unit/test-data/letterSize.pdf
Binary file not shown.
Binary file added tests/unit/test-data/nonLetterSize.pdf
Binary file not shown.

0 comments on commit e59afe2

Please sign in to comment.