diff --git a/src/models/ApiResponse.ts b/src/models/ApiResponse.ts index b94506b..eaf7419 100644 --- a/src/models/ApiResponse.ts +++ b/src/models/ApiResponse.ts @@ -1,8 +1,10 @@ +import { type AxiosResponse } from 'axios'; import { FieldType } from '../enums/FieldType'; import { App } from './App'; import { CollectionResponse } from './CollectionResponse'; import { CreatedWithIdResponse } from './CreatedWithIdResponse'; import { Field } from './Field'; +import { File } from './File'; import { FileInfo } from './FileInfo'; import { FormulaField } from './FormulaField'; import { GetPagedAppsResponse } from './GetPagedAppsResponse'; @@ -221,6 +223,41 @@ export class ApiResponse { ); } + public asFileType(response: AxiosResponse): ApiResponse { + const apiResponse = this as ApiResponse; + + let fileName = response.headers['content-disposition'] + .split(';')[1] + .split('=')[1]; + + fileName = fileName.substring(1, fileName.length - 1); + + const contentType = + response.headers['content-type'] ?? + response.headers['Content-Type'] ?? + null; + + let contentLength = + response.headers['content-length'] ?? + response.headers['Content-Length'] ?? + 0; + + contentLength = parseInt(contentLength); + + const file = new File( + apiResponse.data, + fileName, + contentType, + contentLength + ); + + return new ApiResponse( + apiResponse.statusCode, + apiResponse.message, + file + ); + } + /** * @method asFileCollectionType - Converts the field item to the appropriate field object based upon the field item's type. * @param fieldItem - The field item to convert. diff --git a/src/models/File.ts b/src/models/File.ts new file mode 100644 index 0000000..e5893bf --- /dev/null +++ b/src/models/File.ts @@ -0,0 +1,46 @@ +import { type Readable } from 'stream'; + +/** + * @class File - Represents a file retrieved from Onspring. + */ +export class File { + /** + * @property {Readable} stream - The stream of the file. + */ + public stream: Readable; + + /** + * @property {string} fileName - The name of the file. + */ + public fileName: string; + + /** + * @property {string} contentType - The content type of the file. + */ + public contentType: string; + + /** + * @property {number} contentLength - The content length of the file. + */ + public contentLength: number; + + /** + * @constructor - Creates a new instance of File. + * @param {Readable} stream - The stream of the file. + * @param {string} fileName - The name of the file. + * @param {string} contentType - The content type of the file. + * @param {number} contentLength - The content length of the file. + * @returns {File} - A new instance of File. + */ + constructor( + stream: Readable, + fileName: string, + contentType: string, + contentLength: number + ) { + this.stream = stream; + this.fileName = fileName; + this.contentType = contentType; + this.contentLength = contentLength; + } +} diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index 96dd540..e824900 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -1,10 +1,10 @@ import axios from 'axios'; -import { type AxiosInstance, type AxiosRequestConfig } from 'axios'; +import { PagingRequest } from './PagingRequest'; import { ArgumentValidator } from './ArgumentValidator'; import { EndpointFactory } from './EndpointFactory'; import { ApiResponseFactory } from './ApiResponseFactory'; +import { type AxiosInstance, type AxiosRequestConfig } from 'axios'; import { type ApiResponse } from './ApiResponse'; -import { PagingRequest } from './PagingRequest'; import { type GetPagedAppsResponse } from './GetPagedAppsResponse'; import { type App } from './App'; import { type CollectionResponse } from './CollectionResponse'; @@ -13,6 +13,7 @@ import { type GetPagedFieldsResponse } from './GetPagedFieldsResponse'; import { type SaveFileRequest } from './SaveFileRequest'; import { type CreatedWithIdResponse } from './CreatedWithIdResponse'; import { type FileInfo } from './FileInfo'; +import { type File } from './File'; /** * @class OnspringClient - A client that can communicate with the Onspring API. @@ -200,6 +201,37 @@ export class OnspringClient { return apiResponse.asFileInfoType(); } + /** + * @method getFileById - Gets a file by its id. + * @param {number} recordId - The id of the record that the file is attached to. + * @param {number} fieldId - The id of the field that the file is attached to. + * @param {number} fileId - The id of the file to get. + * @returns {Promise>} - A promise that resolves to an ApiResponse of type File. + */ + public async getFileById( + recordId: number, + fieldId: number, + fileId: number + ): Promise> { + const endpoint = EndpointFactory.getFileByIdEndpoint( + recordId, + fieldId, + fileId + ); + + const response = await this._client.get(endpoint, { + responseType: 'stream', + }); + + const apiResponse = ApiResponseFactory.getApiResponse(response); + + if (apiResponse.isSuccessful === false) { + return apiResponse; + } + + return apiResponse.asFileType(response); + } + /** * @method saveFile - Saves a file to a record in Onspring. * @param {SaveFileRequest} request - The request that will be used to save the file. diff --git a/tests/ApiResponse.spec.ts b/tests/ApiResponse.spec.ts index cfb24b2..4f97f10 100644 --- a/tests/ApiResponse.spec.ts +++ b/tests/ApiResponse.spec.ts @@ -14,6 +14,10 @@ import { ListValue } from '../src/models/ListValue'; import { ReferenceField } from '../src/models/ReferenceField'; import { Multiplicity } from '../src/enums/Multiplicity'; import { ListField } from '../src/models/ListField'; +import { File } from '../src/models/File'; +import fs from 'fs'; +import { type AxiosResponse } from 'axios'; +import path from 'path'; describe('ApiResponse', function () { it('should be defined', function () { @@ -122,13 +126,15 @@ describe('ApiResponse', function () { expect(appsPagedResponse).to.be.instanceOf(ApiResponse); expect(appsPagedResponse.data).to.be.instanceOf(GetPagedAppsResponse); expect(appsPagedResponse.data).to.not.be.null; + expect(appsPagedResponse.data).to.have.property('totalPages', 1); + expect(appsPagedResponse.data).to.have.property('totalRecords', 2); + expect(appsPagedResponse.data).to.have.property('pageNumber', 1); + expect(appsPagedResponse.data).to.have.property('pageSize', 2); + expect(appsPagedResponse.data) + .to.have.property('items') + .that.is.an.instanceOf(Array) + .and.has.lengthOf(2); if (appsPagedResponse.data != null) { - expect(appsPagedResponse.data.totalPages).to.equal(1); - expect(appsPagedResponse.data.totalRecords).to.equal(2); - expect(appsPagedResponse.data.pageNumber).to.equal(1); - expect(appsPagedResponse.data.pageSize).to.equal(2); - expect(appsPagedResponse.data.items).to.be.instanceOf(Array); - expect(appsPagedResponse.data.items).to.have.lengthOf(2); expect(appsPagedResponse.data.items[0]).to.be.instanceOf(App); expect(appsPagedResponse.data.items[1]).to.be.instanceOf(App); } @@ -149,13 +155,21 @@ describe('ApiResponse', function () { expect(appsPagedResponse).to.be.instanceOf(ApiResponse); expect(appsPagedResponse.data).to.be.instanceOf(GetPagedAppsResponse); expect(appsPagedResponse.data).to.not.be.null; + expect(appsPagedResponse.data).to.have.property('totalPages', 0); + expect(appsPagedResponse.data).to.have.property('totalRecords', 0); + expect(appsPagedResponse.data).to.have.property('pageNumber', 0); + expect(appsPagedResponse.data).to.have.property('pageSize', 0); + expect(appsPagedResponse.data) + .to.have.property('items') + .that.is.an.instanceOf(Array) + .and.has.lengthOf(0); if (appsPagedResponse.data != null) { - expect(appsPagedResponse.data.totalPages).to.equal(0); - expect(appsPagedResponse.data.totalRecords).to.equal(0); - expect(appsPagedResponse.data.pageNumber).to.equal(0); - expect(appsPagedResponse.data.pageSize).to.equal(0); - expect(appsPagedResponse.data.items).to.be.instanceOf(Array); - expect(appsPagedResponse.data.items).to.have.lengthOf(0); + appsPagedResponse.data.items.forEach((item) => { + expect(item).to.be.instanceOf(App); + expect(item).to.have.property('id'); + expect(item).to.have.property('name'); + expect(item).to.have.property('href'); + }); } }); }); @@ -182,13 +196,12 @@ describe('ApiResponse', function () { expect(appResponse).to.be.instanceOf(ApiResponse); expect(appResponse.data).to.be.instanceOf(App); expect(appResponse.data).to.not.be.null; - if (appResponse.data != null) { - expect(appResponse.data.id).to.equal(1); - expect(appResponse.data.name).to.equal('Test App'); - expect(appResponse.data.href).to.equal( - 'https://api.onspring.dev/apps/id/1' - ); - } + expect(appResponse.data).to.have.property('id', 1); + expect(appResponse.data).to.have.property('name', 'Test App'); + expect(appResponse.data).to.have.property( + 'href', + 'https://api.onspring.dev/apps/id/1' + ); }); }); @@ -228,12 +241,12 @@ describe('ApiResponse', function () { CollectionResponse ); expect(appCollectionResponse.data).to.not.be.null; + expect(appCollectionResponse.data).to.have.property('count', 2); + expect(appCollectionResponse.data) + .to.have.property('items') + .that.is.instanceOf(Array) + .and.has.lengthOf(2); if (appCollectionResponse.data != null) { - expect(appCollectionResponse.data).to.have.property('count'); - expect(appCollectionResponse.data).to.have.property('items'); - expect(appCollectionResponse.data.count).to.equal(2); - expect(appCollectionResponse.data.items).to.be.instanceOf(Array); - expect(appCollectionResponse.data.items).to.have.lengthOf(2); appCollectionResponse.data.items.forEach((item) => { expect(item).to.be.instanceOf(App); expect(item).to.have.property('id'); @@ -270,15 +283,16 @@ describe('ApiResponse', function () { expect(fieldResponse).to.be.instanceOf(ApiResponse); expect(fieldResponse.data).to.be.instanceOf(Field); expect(fieldResponse.data).to.not.be.null; - if (fieldResponse.data != null) { - expect(fieldResponse.data.id).to.equal(1); - expect(fieldResponse.data.appId).to.equal(1); - expect(fieldResponse.data.name).to.equal('Text Field'); - expect(fieldResponse.data.type).to.equal(FieldType.Text); - expect(fieldResponse.data.status).to.equal(FieldStatus.Enabled); - expect(fieldResponse.data.isRequired).to.equal(false); - expect(fieldResponse.data.isUnique).to.equal(false); - } + expect(fieldResponse.data).to.have.property('id', 1); + expect(fieldResponse.data).to.have.property('appId', 1); + expect(fieldResponse.data).to.have.property('name', 'Text Field'); + expect(fieldResponse.data).to.have.property('type', FieldType.Text); + expect(fieldResponse.data).to.have.property( + 'status', + FieldStatus.Enabled + ); + expect(fieldResponse.data).to.have.property('isRequired', false); + expect(fieldResponse.data).to.have.property('isUnique', false); }); it('should return an ApiResponse when data contains a list field', function () { @@ -324,19 +338,32 @@ describe('ApiResponse', function () { expect(fieldResponse.data).to.be.instanceOf(Field); expect(fieldResponse.data).to.be.instanceOf(ListField); expect(fieldResponse.data).to.not.be.null; + expect(fieldResponse.data).to.be.instanceOf(ListField); + expect(fieldResponse.data).to.have.property('id', 11929); + expect(fieldResponse.data).to.have.property('appId', 373); + expect(fieldResponse.data).to.have.property( + 'name', + 'single-select list field' + ); + expect(fieldResponse.data).to.have.property('type', FieldType.List); + expect(fieldResponse.data).to.have.property( + 'status', + FieldStatus.Enabled + ); + expect(fieldResponse.data).to.have.property('isRequired', false); + expect(fieldResponse.data).to.have.property('isUnique', false); + expect(fieldResponse.data).to.have.property( + 'multiplicity', + Multiplicity.SingleSelect + ); + expect(fieldResponse.data).to.have.property('listId', 1581); + expect(fieldResponse.data) + .to.have.property('values') + .that.is.instanceOf(Array) + .and.has.lengthOf(3); + if (fieldResponse.data != null) { const listField = fieldResponse.data as ListField; - expect(listField.id).to.equal(11929); - expect(listField.appId).to.equal(373); - expect(listField.name).to.equal('single-select list field'); - expect(listField.type).to.equal(FieldType.List); - expect(listField.status).to.equal(FieldStatus.Enabled); - expect(listField.isRequired).to.equal(false); - expect(listField.isUnique).to.equal(false); - expect(listField.multiplicity).to.equal(Multiplicity.SingleSelect); - expect(listField.listId).to.equal(1581); - expect(listField.values).to.be.instanceOf(Array); - expect(listField.values).to.have.lengthOf(3); listField.values.forEach((value) => { expect(value).to.be.instanceOf(ListValue); expect(value).to.have.property('id'); @@ -368,18 +395,22 @@ describe('ApiResponse', function () { expect(fieldResponse.data).to.be.instanceOf(Field); expect(fieldResponse.data).to.be.instanceOf(ReferenceField); expect(fieldResponse.data).to.not.be.null; - if (fieldResponse.data != null) { - const referenceField = fieldResponse.data as ReferenceField; - expect(referenceField.id).to.equal(11866); - expect(referenceField.appId).to.equal(373); - expect(referenceField.name).to.equal('Created By'); - expect(referenceField.type).to.equal(FieldType.Reference); - expect(referenceField.status).to.equal(FieldStatus.Enabled); - expect(referenceField.isRequired).to.equal(false); - expect(referenceField.isUnique).to.equal(false); - expect(referenceField.multiplicity).to.equal(Multiplicity.SingleSelect); - expect(referenceField.referencedAppId).to.equal(2); - } + expect(fieldResponse.data).to.be.instanceOf(ReferenceField); + expect(fieldResponse.data).to.have.property('id', 11866); + expect(fieldResponse.data).to.have.property('appId', 373); + expect(fieldResponse.data).to.have.property('name', 'Created By'); + expect(fieldResponse.data).to.have.property('type', FieldType.Reference); + expect(fieldResponse.data).to.have.property( + 'status', + FieldStatus.Enabled + ); + expect(fieldResponse.data).to.have.property('isRequired', false); + expect(fieldResponse.data).to.have.property('isUnique', false); + expect(fieldResponse.data).to.have.property( + 'multiplicity', + Multiplicity.SingleSelect + ); + expect(fieldResponse.data).to.have.property('referencedAppId', 2); }); it('should return an ApiResponse when data contains a list formula field', function () { @@ -417,18 +448,28 @@ describe('ApiResponse', function () { expect(fieldResponse.data).to.be.instanceOf(Field); expect(fieldResponse.data).to.be.instanceOf(FormulaField); expect(fieldResponse.data).to.not.be.null; + expect(fieldResponse.data).to.be.instanceOf(FormulaField); + expect(fieldResponse.data).to.have.property('id', 12680); + expect(fieldResponse.data).to.have.property('appId', 373); + expect(fieldResponse.data).to.have.property('name', 'List Formula Field'); + expect(fieldResponse.data).to.have.property('type', FieldType.Formula); + expect(fieldResponse.data).to.have.property( + 'status', + FieldStatus.Enabled + ); + expect(fieldResponse.data).to.have.property('isRequired', false); + expect(fieldResponse.data).to.have.property('isUnique', false); + expect(fieldResponse.data).to.have.property( + 'outputType', + FormulaOutputType.ListValue + ); + expect(fieldResponse.data) + .to.have.property('values') + .that.is.instanceOf(Array) + .and.has.lengthOf(2); + if (fieldResponse.data != null) { const formulaField = fieldResponse.data as FormulaField; - expect(formulaField.id).to.equal(12680); - expect(formulaField.appId).to.equal(373); - expect(formulaField.name).to.equal('List Formula Field'); - expect(formulaField.type).to.equal(FieldType.Formula); - expect(formulaField.status).to.equal(FieldStatus.Enabled); - expect(formulaField.isRequired).to.equal(false); - expect(formulaField.isUnique).to.equal(false); - expect(formulaField.outputType).to.equal(FormulaOutputType.ListValue); - expect(formulaField.values).to.be.instanceOf(Array); - expect(formulaField.values).to.have.lengthOf(2); formulaField.values.forEach((value) => { expect(value).to.be.instanceOf(ListValue); expect(value).to.have.property('id'); @@ -460,19 +501,25 @@ describe('ApiResponse', function () { expect(fieldResponse.data).to.be.instanceOf(Field); expect(fieldResponse.data).to.be.instanceOf(FormulaField); expect(fieldResponse.data).to.not.be.null; - if (fieldResponse.data != null) { - const formulaField = fieldResponse.data as FormulaField; - expect(formulaField.id).to.equal(12060); - expect(formulaField.appId).to.equal(373); - expect(formulaField.name).to.equal('GetWeekOfYear'); - expect(formulaField.type).to.equal(FieldType.Formula); - expect(formulaField.status).to.equal(FieldStatus.Enabled); - expect(formulaField.isRequired).to.equal(false); - expect(formulaField.isUnique).to.equal(false); - expect(formulaField.outputType).to.equal(FormulaOutputType.Text); - expect(formulaField.values).to.be.instanceOf(Array); - expect(formulaField.values).to.have.lengthOf(0); - } + expect(fieldResponse.data).to.be.instanceOf(FormulaField); + expect(fieldResponse.data).to.have.property('id', 12060); + expect(fieldResponse.data).to.have.property('appId', 373); + expect(fieldResponse.data).to.have.property('name', 'GetWeekOfYear'); + expect(fieldResponse.data).to.have.property('type', FieldType.Formula); + expect(fieldResponse.data).to.have.property( + 'status', + FieldStatus.Enabled + ); + expect(fieldResponse.data).to.have.property('isRequired', false); + expect(fieldResponse.data).to.have.property('isUnique', false); + expect(fieldResponse.data).to.have.property( + 'outputType', + FormulaOutputType.Text + ); + expect(fieldResponse.data) + .to.have.property('values') + .that.is.instanceOf(Array) + .and.has.lengthOf(0); }); }); @@ -520,9 +567,13 @@ describe('ApiResponse', function () { CollectionResponse ); expect(fieldCollectionResponse.data).to.not.be.null; + expect(fieldCollectionResponse.data).to.have.property('count', 2); + expect(fieldCollectionResponse.data) + .to.have.property('items') + .that.is.instanceOf(Array) + .and.has.lengthOf(2); + if (fieldCollectionResponse.data != null) { - expect(fieldCollectionResponse.data.items).to.be.instanceOf(Array); - expect(fieldCollectionResponse.data.items).to.have.lengthOf(2); fieldCollectionResponse.data.items.forEach((item) => { expect(item).to.be.instanceOf(Field); expect(item).to.have.property('id'); @@ -587,14 +638,16 @@ describe('ApiResponse', function () { GetPagedFieldsResponse ); expect(getPagedFieldsResponse.data).to.not.be.null; + expect(getPagedFieldsResponse.data).to.have.property('pageNumber', 1); + expect(getPagedFieldsResponse.data).to.have.property('pageSize', 2); + expect(getPagedFieldsResponse.data).to.have.property('totalPages', 1); + expect(getPagedFieldsResponse.data).to.have.property('totalRecords', 2); + expect(getPagedFieldsResponse.data) + .to.have.property('items') + .that.is.instanceOf(Array) + .and.has.lengthOf(2); if (getPagedFieldsResponse.data != null) { - expect(getPagedFieldsResponse.data.pageNumber).to.equal(1); - expect(getPagedFieldsResponse.data.pageSize).to.equal(2); - expect(getPagedFieldsResponse.data.totalPages).to.equal(1); - expect(getPagedFieldsResponse.data.totalRecords).to.equal(2); - expect(getPagedFieldsResponse.data.items).to.be.instanceOf(Array); - expect(getPagedFieldsResponse.data.items).to.have.lengthOf(2); getPagedFieldsResponse.data.items.forEach((item) => { expect(item).to.be.instanceOf(Field); expect(item).to.have.property('id'); @@ -627,15 +680,14 @@ describe('ApiResponse', function () { GetPagedFieldsResponse ); expect(getPagedFieldsResponse.data).to.not.be.null; - - if (getPagedFieldsResponse.data != null) { - expect(getPagedFieldsResponse.data.pageNumber).to.equal(0); - expect(getPagedFieldsResponse.data.pageSize).to.equal(0); - expect(getPagedFieldsResponse.data.totalPages).to.equal(0); - expect(getPagedFieldsResponse.data.totalRecords).to.equal(0); - expect(getPagedFieldsResponse.data.items).to.be.instanceOf(Array); - expect(getPagedFieldsResponse.data.items).to.have.lengthOf(0); - } + expect(getPagedFieldsResponse.data).to.have.property('pageNumber', 0); + expect(getPagedFieldsResponse.data).to.have.property('pageSize', 0); + expect(getPagedFieldsResponse.data).to.have.property('totalPages', 0); + expect(getPagedFieldsResponse.data).to.have.property('totalRecords', 0); + expect(getPagedFieldsResponse.data) + .to.have.property('items') + .that.is.instanceOf(Array) + .and.has.lengthOf(0); }); }); @@ -666,9 +718,202 @@ describe('ApiResponse', function () { CreatedWithIdResponse ); expect(createdWithIdResponse.data).to.not.be.null; - if (createdWithIdResponse.data != null) { - expect(createdWithIdResponse.data.id).to.equal(1); - } + expect(createdWithIdResponse.data).to.have.property('id', 1); + }); + }); + + describe('asFileType', function () { + it('should be defined', function () { + expect(ApiResponse.prototype.asFileType).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(ApiResponse.prototype.asFileType).to.be.a('function'); + }); + + it('should have 1 parameter', function () { + expect(ApiResponse.prototype.asFileType).to.have.lengthOf(1); + }); + + it('should return an ApiResponse when data contains an attachment file', function () { + const mockResponse = { + headers: { + 'content-type': 'text/plain', + 'content-disposition': 'attachment; filename="test-attachment.txt"', + 'Content-Length': '13', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-attachment.txt' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse).to.be.instanceOf(ApiResponse); + expect(fileResponse.data).to.be.instanceOf(File); + expect(fileResponse.data).to.not.be.null; + expect(fileResponse.data).to.have.property( + 'fileName', + 'test-attachment.txt' + ); + expect(fileResponse.data).to.have.property('contentLength', 13); + expect(fileResponse.data).to.have.property('contentType', 'text/plain'); + expect(fileResponse.data).to.have.property('stream', mockResponseData); + }); + + it('should return an ApiResponse when data contains an image file', function () { + const mockResponse = { + headers: { + 'content-type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'Content-Length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse).to.be.instanceOf(ApiResponse); + expect(fileResponse.data).to.be.instanceOf(File); + expect(fileResponse.data).to.not.be.null; + expect(fileResponse.data).to.have.property('fileName', 'test-image.jpeg'); + expect(fileResponse.data).to.have.property('contentLength', 98897); + expect(fileResponse.data).to.have.property('contentType', 'image/jpeg'); + expect(fileResponse.data).to.have.property('stream', mockResponseData); + }); + + it('should return an ApiResponse with proper contentType value when header is Content-Type', function () { + const mockResponse = { + headers: { + 'Content-Type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'Content-Length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentType', 'image/jpeg'); + }); + + it('should return an ApiResponse with proper contentType value when header is content-type', function () { + const mockResponse = { + headers: { + 'content-type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'Content-Length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentType', 'image/jpeg'); + }); + + it('should return an ApiResponse with proper contentType value when no content-type header is present', function () { + const mockResponse = { + headers: { + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'Content-Length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentType', null); + }); + + it('should return an ApiResponse with proper contentLength value when header is Content-Length', function () { + const mockResponse = { + headers: { + 'Content-Type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'Content-Length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentLength', 98897); + }); + + it('should return an ApiResponse with proper contentLength value when header is content-length', function () { + const mockResponse = { + headers: { + 'Content-Type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + 'content-length': '98897', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentLength', 98897); + }); + + it('should return an ApiResponse with proper contentLength value when no content-length header is present', function () { + const mockResponse = { + headers: { + 'Content-Type': 'image/jpeg', + 'content-disposition': 'attachment; filename="test-image.jpeg"', + } as AxiosResponse['headers'], + } as AxiosResponse; + + const attachmentPath = path.join( + __dirname, + 'testData', + 'test-image.jpeg' + ); + const mockResponseData = fs.createReadStream(attachmentPath); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const fileResponse = apiResponse.asFileType(mockResponse); + + expect(fileResponse.data).to.have.property('contentLength', 0); }); }); }); diff --git a/tests/File.spec.ts b/tests/File.spec.ts new file mode 100644 index 0000000..5ba8930 --- /dev/null +++ b/tests/File.spec.ts @@ -0,0 +1,62 @@ +import { File } from '../src/models/File'; +import { expect } from 'chai'; +import { Readable } from 'stream'; + +describe('File', function () { + it('should be defined', function () { + expect(File).to.not.be.undefined; + }); + + it('should have a constructor', function () { + expect(File).to.have.property('constructor'); + }); + + it('should have a stream property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1) + ).to.have.property('stream'); + }); + + it('should have a fileName property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1) + ).to.have.property('fileName'); + }); + + it('should have a contentType property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1) + ).to.have.property('contentType'); + }); + + it('should have a contentLength property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1) + ).to.have.property('contentLength'); + }); + + it('should have a constructor that sets the stream property', function () { + const stream = new Readable(); + expect( + new File(stream, 'File Name', 'Content Type', 1).stream + ).to.deep.equal(stream); + }); + + it('should have a constructor that sets the fileName property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1).fileName + ).to.equal('File Name'); + }); + + it('should have a constructor that sets the contentType property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1).contentType + ).to.equal('Content Type'); + }); + + it('should have a constructor that sets the contentLength property', function () { + expect( + new File(new Readable(), 'File Name', 'Content Type', 1).contentLength + ).to.equal(1); + }); +}); diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index 53dff45..be21578 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -17,6 +17,9 @@ import { SaveFileRequest } from '../src/models/SaveFileRequest'; import { Readable } from 'stream'; import { CreatedWithIdResponse } from '../src/models/CreatedWithIdResponse'; import { FileInfo } from '../src/models/FileInfo'; +import { File } from '../src/models/File'; +import fs from 'fs'; +import path from 'path'; describe('OnspringClient', function () { const baseUrl = 'https://api.onspring.dev'; @@ -1706,4 +1709,195 @@ describe('OnspringClient', function () { expect(result.data).to.be.null; }); }); + + describe('getFileById', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getFileById).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getFileById).to.be.a('function'); + }); + + it('should return a promise', function () { + expect( + new OnspringClient(baseUrl, apiKey).getFileById(1, 1, 1) + ).to.be.instanceOf(Promise); + }); + + it('should return a promise that resolves to a file when request is successful', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + const filePath = path.join(__dirname, 'testData', 'test-attachment.txt'); + const file = fs.createReadStream(filePath); + + sinon.stub(mockAxiosClient, 'get').returns( + Promise.resolve({ + status: 200, + statusText: 'OK', + data: file, + headers: { + 'content-disposition': 'attachment; filename="test-attachment.txt"', + 'content-length': 14, + 'content-type': 'text/plain', + }, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getFileById(1, 1, 1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 200); + expect(result).to.have.property('isSuccessful', true); + expect(result).to.have.property('message', ''); + expect(result.data).to.be.instanceOf(File); + expect(result.data).to.have.property('fileName', 'test-attachment.txt'); + expect(result.data).to.have.property('contentLength', 14); + expect(result.data).to.have.property('contentType', 'text/plain'); + expect(result.data) + .to.have.property('stream') + .that.is.instanceOf(Readable); + }); + + it('should return a promise that resolves to an api response when request receives a 400 response', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').returns( + Promise.resolve({ + status: 400, + statusText: 'Bad Request', + data: { field: ['Field requested is not a file type field.'] }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getFileById(1, 1, 1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 400); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property( + 'message', + '{"field":["Field requested is not a file type field."]}' + ); + expect(result.data).to.be.null; + }); + + it('should return a promise that resolves to an api response when request receives a 401 response', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').returns( + Promise.resolve({ + status: 401, + statusText: 'Unauthorized', + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getFileById(1, 1, 1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 401); + expect(result).to.have.property('isSuccessful', false); + expect(result.message).to.be.undefined; + expect(result.data).to.be.null; + }); + + it('should return a promise that resolves to an api response when request receives a 403 response', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').returns( + Promise.resolve({ + status: 403, + statusText: 'Forbidden', + data: { message: 'Forbidden' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getFileById(1, 1, 1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 403); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', 'Forbidden'); + expect(result.data).to.be.null; + }); + + it('should return a promise that resolves to an api response when request receives a 404 response', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').returns( + Promise.resolve({ + status: 404, + statusText: 'Not Found', + data: { message: 'Not Found' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getFileById(1, 1, 1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 404); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', 'Not Found'); + expect(result.data).to.be.null; + }); + }); }); diff --git a/tests/testData/test-attachment.txt b/tests/testData/test-attachment.txt new file mode 100644 index 0000000..3eae1d0 --- /dev/null +++ b/tests/testData/test-attachment.txt @@ -0,0 +1 @@ +This is a test attachment. \ No newline at end of file diff --git a/tests/testData/test-image.jpeg b/tests/testData/test-image.jpeg new file mode 100644 index 0000000..b493033 Binary files /dev/null and b/tests/testData/test-image.jpeg differ