diff --git a/src/FileUploader.ts b/src/FileUploader.ts index 7bced7b..8e0b643 100644 --- a/src/FileUploader.ts +++ b/src/FileUploader.ts @@ -1,6 +1,7 @@ import { isLogInRequired, needsRetry, isUrl } from './utils' import { CsrfToken } from './types/CsrfToken' import { UploadFile } from './types/UploadFile' +import { ErrorResponse } from './types' const requestRetryLimit = 5 @@ -18,29 +19,31 @@ export class FileUploader { private retryCount = 0 public uploadFile(sasJob: string, files: UploadFile[], params: any) { - if (files?.length < 1) - throw new Error('At least one file must be provided.') - - let paramsString = '' - - for (let param in params) { - if (params.hasOwnProperty(param)) { - paramsString += `&${param}=${params[param]}` - } - } - - const program = this.appLoc - ? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') - : sasJob - const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${ - '_program=' + program - }${paramsString}` - - const headers = { - 'cache-control': 'no-cache' - } - return new Promise((resolve, reject) => { + if (files?.length < 1) + reject(new ErrorResponse('At least one file must be provided.')) + if (!sasJob || sasJob === '') + reject(new ErrorResponse('sasJob must be provided.')) + + let paramsString = '' + + for (let param in params) { + if (params.hasOwnProperty(param)) { + paramsString += `&${param}=${params[param]}` + } + } + + const program = this.appLoc + ? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') + : sasJob + const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${ + '_program=' + program + }${paramsString}` + + const headers = { + 'cache-control': 'no-cache' + } + const formData = new FormData() for (let file of files) { @@ -76,7 +79,7 @@ export class FileUploader { }) .then((responseText) => { if (isLogInRequired(responseText)) - reject('You must be logged in to upload a file') + reject(new ErrorResponse('You must be logged in to upload a file.')) if (needsRetry(responseText)) { if (this.retryCount < requestRetryLimit) { @@ -95,10 +98,18 @@ export class FileUploader { try { resolve(JSON.parse(responseText)) } catch (e) { - reject(e) + reject( + new ErrorResponse( + 'Error while parsing json from upload response.', + e + ) + ) } } }) + .catch((err: any) => { + reject(new ErrorResponse('Upload request failed.', err)) + }) }) } } diff --git a/src/test/FileUploader.spec.ts b/src/test/FileUploader.spec.ts new file mode 100644 index 0000000..a9fa746 --- /dev/null +++ b/src/test/FileUploader.spec.ts @@ -0,0 +1,137 @@ +import { FileUploader } from '../FileUploader' +import { UploadFile } from '../types' + +const sampleResponse = `{ + "SYSUSERID": "cas", + "_DEBUG":" ", + "SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000", + "_PROGRAM" : "/Public/app/editors/loadfile", + "SYSCC" : "0", + "SYSJOBID" : "117382", + "SYSWARNINGTEXT" : "" +}` + +const prepareFilesAndParams = () => { + const files: UploadFile[] = [ + { + file: new File([''], 'testfile'), + fileName: 'testfile' + } + ] + const params = { table: 'libtable' } + + return { files, params } +} + +describe('FileUploader', () => { + let originalFetch: any + const fileUploader = new FileUploader( + '/sample/apploc', + 'https://sample.server.com', + '/jobs/path', + null, + null + ) + + beforeAll(() => { + originalFetch = (global as any).fetch + }) + + beforeEach(() => { + ;(global as any).fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + text: () => Promise.resolve(sampleResponse) + }) + ) + }) + + afterAll(() => { + ;(global as any).fetch = originalFetch + }) + + it('should upload successfully', async (done) => { + const sasJob = 'test/upload' + const { files, params } = prepareFilesAndParams() + + fileUploader.uploadFile(sasJob, files, params).then((res: any) => { + expect(JSON.stringify(res)).toEqual( + JSON.stringify(JSON.parse(sampleResponse)) + ) + done() + }) + }) + + it('should an error when no files are provided', async (done) => { + const sasJob = 'test/upload' + const files: UploadFile[] = [] + const params = { table: 'libtable' } + + fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { + expect(err.error.message).toEqual('At least one file must be provided.') + done() + }) + }) + + it('should throw an error when no sasJob is provided', async (done) => { + const sasJob = '' + const { files, params } = prepareFilesAndParams() + + fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { + expect(err.error.message).toEqual('sasJob must be provided.') + done() + }) + }) + + it('should throw an error when login is required', async (done) => { + ;(global as any).fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + text: () => Promise.resolve('
') + }) + ) + + const sasJob = 'test' + const { files, params } = prepareFilesAndParams() + + fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { + expect(err.error.message).toEqual( + 'You must be logged in to upload a file.' + ) + done() + }) + }) + + it('should throw an error when invalid JSON is returned by the server', async (done) => { + ;(global as any).fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + text: () => Promise.resolve('{invalid: "json"') + }) + ) + + const sasJob = 'test' + const { files, params } = prepareFilesAndParams() + + fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { + expect(err.error.message).toEqual( + 'Error while parsing json from upload response.' + ) + done() + }) + }) + + it('should throw an error when the server request fails', async (done) => { + ;(global as any).fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + text: () => Promise.reject('{message: "Server error"}') + }) + ) + + const sasJob = 'test' + const { files, params } = prepareFilesAndParams() + + fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { + expect(err.error.message).toEqual('Upload request failed.') + + done() + }) + }) +}) diff --git a/src/utils/parseGeneratedCode.spec.ts b/src/test/utils/parseGeneratedCode.spec.ts similarity index 96% rename from src/utils/parseGeneratedCode.spec.ts rename to src/test/utils/parseGeneratedCode.spec.ts index 116a88f..d08108d 100644 --- a/src/utils/parseGeneratedCode.spec.ts +++ b/src/test/utils/parseGeneratedCode.spec.ts @@ -1,4 +1,4 @@ -import { parseGeneratedCode } from './index' +import { parseGeneratedCode } from '../../utils/index' it('should parse generated code', async (done) => { expect(sampleResponse).toBeTruthy() diff --git a/src/utils/parseSourceCode.spec.ts b/src/test/utils/parseSourceCode.spec.ts similarity index 95% rename from src/utils/parseSourceCode.spec.ts rename to src/test/utils/parseSourceCode.spec.ts index d407d49..4e779cb 100644 --- a/src/utils/parseSourceCode.spec.ts +++ b/src/test/utils/parseSourceCode.spec.ts @@ -1,4 +1,4 @@ -import { parseSourceCode } from './index' +import { parseSourceCode } from '../../utils/index' it('should parse SAS9 source code', async (done) => { expect(sampleResponse).toBeTruthy() diff --git a/src/types/ErrorResponse.ts b/src/types/ErrorResponse.ts index c00822a..85635df 100644 --- a/src/types/ErrorResponse.ts +++ b/src/types/ErrorResponse.ts @@ -1,5 +1,5 @@ export class ErrorResponse { - body: ErrorBody + error: ErrorBody constructor(message: string, details?: any) { let detailsString = '' @@ -11,7 +11,7 @@ export class ErrorResponse { raw = details } - this.body = { + this.error = { message, details: detailsString, raw