diff --git a/package.json b/package.json index 52bf56f..dd0f6c5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "@emartech/escher-request", "description": "Requests with Escher authentication", "scripts": { - "test": "mocha --require ts-node/register ./src --recursive", + "test": "mocha --require ts-node/register --extension ts ./src --recursive", + "test:watch": "mocha --require ts-node/register --extension ts ./src --recursive --watch", "lint": "eslint ./src/**/*.{ts,js}", "build": "rm -rf dist && tsc --project ./tsconfig.json", "release": "CI=true semantic-release" @@ -33,7 +34,12 @@ "escher-auth": "3.2.4" }, "devDependencies": { - "@types/node": "18.7.8", + "@types/chai": "4.3.3", + "@types/chai-subset": "1.3.3", + "@types/mocha": "10.0.0", + "@types/node": "18.7.23", + "@types/sinon": "10.0.13", + "@types/sinon-chai": "3.2.8", "@typescript-eslint/parser": "5.35.1", "chai": "4.3.6", "chai-subset": "1.6.0", diff --git a/src/escher-auth.d.ts b/src/escher-auth.d.ts deleted file mode 100644 index 0508380..0000000 --- a/src/escher-auth.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -declare module 'escher-auth' { - export type EscherRequest = { - method: string; - host: string; - port: number; - url: string; - body?: string | Buffer; - headers?: string[][]; // [["Date","Fri, 09 Sep 2011 23:36:00 GMT"],["Host","host.foo.com"] - }; - - export type Config = { - algoPrefix?: string; - vendorKey?: string; - hashAlgo?: 'SHA256' | 'SHA512'; - credentialScope?: string; - accessKeyId?: string; - apiSecret?: string; - authHeaderName?: string; - dateHeaderName?: string; - clockSkew?: number; - } & Record; - - export default class Escher { - // eslint-disable-next-line no-unused-vars - constructor(configToMerge: Config); - // eslint-disable-next-line no-unused-vars - static create(configToMerge?: Config): Escher; - public signRequest( - // eslint-disable-next-line no-unused-vars,no-undef - requestOptions: ExtendedRequestOption, - // eslint-disable-next-line no-unused-vars - body?: string | Buffer, - // eslint-disable-next-line no-unused-vars - headersToSign?: string[] - ): EscherRequest; - } -} diff --git a/src/request.spec.js b/src/request.spec.ts similarity index 94% rename from src/request.spec.js rename to src/request.spec.ts index a7e2f53..d8dfda7 100644 --- a/src/request.spec.js +++ b/src/request.spec.ts @@ -1,10 +1,10 @@ -const sinon = require('sinon'); -const { expect } = require('chai'); -const axios = require('axios'); +import sinon, { SinonStub } from 'sinon'; +import { expect } from 'chai'; +import axios from 'axios'; const Escher = require('escher-auth'); -const http = require('http'); -const https = require('https'); -const { EscherRequest, EscherRequestOption } = require('./request'); +import http from 'http'; +import https from 'https'; +import { EscherRequest, EscherRequestOption } from './request'; describe('EscherRequest', function() { const serviceConfig = { @@ -23,9 +23,9 @@ describe('EscherRequest', function() { }; }; - let requestOptions; - let requestStub; - let escherRequest; + let requestOptions: EscherRequestOption; + let requestStub: SinonStub; + let escherRequest: EscherRequest; beforeEach(function() { requestOptions = new EscherRequestOption(serviceConfig.host, serviceConfig); @@ -62,7 +62,7 @@ describe('EscherRequest', function() { }); it('should sign headers with non string values', async () => { - requestOptions.setHeader(['x-customer-id', 15]); + requestOptions.setHeader(['x-customer-id', '15']); await escherRequest.post('/path', { name: 'Almanach' }); diff --git a/src/request.ts b/src/request.ts index e92a929..1f58f4f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,4 +1,4 @@ -import Escher from 'escher-auth'; +const Escher = require('escher-auth'); import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; import { EscherRequestOption } from './requestOption'; @@ -17,10 +17,10 @@ export class EscherRequest { authHeaderName: 'X-Ems-Auth', dateHeaderName: 'X-Ems-Date' }; - private escher: Escher; + private escher: any; private options: EscherRequestOption; - private readonly httpAgent?: HttpAgent; - private readonly httpsAgent?: HttpsAgent; + public readonly httpAgent?: HttpAgent; + public readonly httpsAgent?: HttpsAgent; public static create(accessKeyId: string, apiSecret: string, requestOptions: EscherRequestOption) { return new EscherRequest(accessKeyId, apiSecret, requestOptions); diff --git a/src/requestError.spec.js b/src/requestError.spec.ts similarity index 80% rename from src/requestError.spec.js rename to src/requestError.spec.ts index 71a357e..3b7b5a2 100644 --- a/src/requestError.spec.js +++ b/src/requestError.spec.ts @@ -1,5 +1,5 @@ -const { expect } = require('chai'); -const { EscherRequestError } = require('./requestError'); +import { expect } from 'chai'; +import { EscherRequestError } from './requestError'; describe('EscherRequestError', function() { it('should extend base Error class', function() { @@ -10,6 +10,10 @@ describe('EscherRequestError', function() { it('should store constructor parameters', function() { const error = new EscherRequestError('Invalid request', 400, { + status: 200, + statusText: 'OK', + headers: {}, + config: {}, data: { replyText: 'Too long', detailedMessage: 'Line too long' @@ -26,17 +30,11 @@ describe('EscherRequestError', function() { }); it('should store response as is when no data attribute present', function() { - const error = new EscherRequestError('Invalid request', 400, { - replyText: 'Too long', - detailedMessage: 'Line too long' - }); + const error = new EscherRequestError('Invalid request', 400, 'Line too long'); expect(error.message).to.eql('Invalid request'); expect(error.code).to.eql(400); - expect(error.data).to.eql({ - replyText: 'Too long', - detailedMessage: 'Line too long' - }); + expect(error.data).to.eql('Line too long'); }); it('should always contain data on error', function() { diff --git a/src/requestOption.spec.js b/src/requestOption.spec.ts similarity index 74% rename from src/requestOption.spec.js rename to src/requestOption.spec.ts index 5f130ca..164c040 100644 --- a/src/requestOption.spec.js +++ b/src/requestOption.spec.ts @@ -1,8 +1,9 @@ -const { expect } = require('chai'); -const { EscherRequestOption } = require('./requestOption'); +import { expect } from 'chai'; +import { EscherRequestOption, RequestOptions } from './requestOption'; describe('EscherRequestOption', function() { - let dummyServiceConfig; + let dummyServiceConfig: RequestOptions; + const host = 'localhost'; beforeEach(function() { dummyServiceConfig = { @@ -53,7 +54,7 @@ describe('EscherRequestOption', function() { it('can accept additional headers', function() { const dummyHeader = ['header-name', 'header-value']; - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); requestOptions.setHeader(dummyHeader); @@ -61,14 +62,14 @@ describe('EscherRequestOption', function() { }); it('should add default content type', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.getHeader('content-type')).to.eql('application/json'); }); it('should not duplicate headers with same name', function() { const expectedContentTypeHeader = ['content-type', 'text/csv']; - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); requestOptions.setHeader(expectedContentTypeHeader); @@ -79,14 +80,14 @@ describe('EscherRequestOption', function() { describe('allowEmptyResponse', function() { it('should be set to false by default', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.allowEmptyResponse).to.eql(false); }); it('should be set to the value provided in config', function() { dummyServiceConfig.allowEmptyResponse = true; - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.allowEmptyResponse).to.eql(true); }); @@ -94,7 +95,7 @@ describe('EscherRequestOption', function() { describe('timeout', function() { it('should return a default value', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.getTimeout()).to.be.eql(15000); }); @@ -102,13 +103,13 @@ describe('EscherRequestOption', function() { it('should return the timeout passed in the constructor', function() { const options = Object.assign({}, dummyServiceConfig); options.timeout = 0; - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, options); + const requestOptions = new EscherRequestOption(host, options); expect(requestOptions.getTimeout()).to.be.eql(0); }); it('should return the timeout set by setTimeout', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); requestOptions.setTimeout(60000); @@ -120,7 +121,7 @@ describe('EscherRequestOption', function() { describe('toHash', function() { it('should return the proper object', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.toHash()).to.be.eql({ headers: [ @@ -139,21 +140,22 @@ describe('EscherRequestOption', function() { it('should add allowEmptyResponse to hash if set to TRUE', function() { dummyServiceConfig.allowEmptyResponse = true; - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); expect(requestOptions.toHash()).to.have.property('allowEmptyResponse', true); }); it('should not cache headers', function() { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); - requestOptions.toHash().headers.push('from_test'); - expect(requestOptions.toHash().headers).not.to.include('from_test'); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); + const headers = requestOptions.toHash().headers as string[][]; + headers.push(['from_test']); + expect(requestOptions.toHash().headers).not.to.include(['from_test']); }); }); describe('setHost', () => { it('should set host', () => { - const requestOptions = new EscherRequestOption(dummyServiceConfig.host, dummyServiceConfig); + const requestOptions = new EscherRequestOption(host, dummyServiceConfig); requestOptions.setHost('suitedocker.ett.local'); diff --git a/src/setup.spec.ts b/src/setup.spec.ts new file mode 100644 index 0000000..e9f5235 --- /dev/null +++ b/src/setup.spec.ts @@ -0,0 +1,11 @@ +import sinon from 'sinon'; +import chai from 'chai'; +import chaiSubset from 'chai-subset'; +import chaiSinon from 'sinon-chai'; + +chai.use(chaiSinon); +chai.use(chaiSubset); + +afterEach(function() { + sinon.restore(); +}); diff --git a/src/testSetup.spec.js b/src/testSetup.spec.js deleted file mode 100644 index 37fb689..0000000 --- a/src/testSetup.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -const sinon = require('sinon'); -const chai = require('chai'); -const chaiSubset = require('chai-subset'); -const chaiSinon = require('sinon-chai'); - -before(function() { - chai.use(chaiSinon); - chai.use(chaiSubset); -}); - -afterEach(function() { - sinon.restore(); -}); diff --git a/src/wrapper.spec.js b/src/wrapper.spec.ts similarity index 66% rename from src/wrapper.spec.js rename to src/wrapper.spec.ts index 021dfb2..dde4e5a 100644 --- a/src/wrapper.spec.js +++ b/src/wrapper.spec.ts @@ -1,17 +1,18 @@ -const axios = require('axios'); -const http = require('http'); -const https = require('https'); -const sinon = require('sinon'); -const { expect } = require('chai'); -const { RequestWrapper } = require('./wrapper'); -const { EscherRequestError } = require('./requestError'); -const { EscherRequestOption } = require('./requestOption'); +import axios, { CancelToken } from 'axios'; +import http from 'http'; +import https from 'https'; +import sinon, { SinonStub } from 'sinon'; +import { expect } from 'chai'; +import { ExtendedRequestOption, RequestWrapper } from './wrapper'; +import { EscherRequestError } from './requestError'; +import { AxiosRequestConfig } from 'axios'; describe('RequestWrapper', function() { - let apiResponse; - let expectedApiResponse; - let escherRequestOptions; - let expectedRequestOptions; + let apiResponse: any; + let expectedApiResponse: any; + let extendedRequestOptions: ExtendedRequestOption; + let expectedRequestOptions: AxiosRequestConfig; + let cancelToken: CancelToken; beforeEach(function() { apiResponse = { @@ -28,15 +29,29 @@ describe('RequestWrapper', function() { statusMessage: 'OK' }; - escherRequestOptions = new EscherRequestOption('very.host.io', { + extendedRequestOptions = { + secure: true, port: 443, + host: 'very.host.io', + rejectUnauthorized: true, headers: [ ['content-type', 'very-format'], ['x-custom', 'alma'] ], + prefix: '', + timeout: 15000, + allowEmptyResponse: false, + maxContentLength: 10485760, + keepAlive: false, + credentialScope: '', method: 'GET', + url: 'http://very.host.io:443/purchases/1/content', path: '/purchases/1/content' - }); + }; + + const CancelToken = axios.CancelToken; + const source = CancelToken.source(); + cancelToken = source.token; expectedRequestOptions = { method: 'get', @@ -48,25 +63,25 @@ describe('RequestWrapper', function() { }, timeout: 15000, maxContentLength: 10485760, - cancelToken: 'source-token' + cancelToken: cancelToken }; }); describe('request handling', function() { - let wrapper; - let requestGetStub; - let source; + let wrapper: RequestWrapper; + let requestGetStub: SinonStub; + let source: any; beforeEach(function() { requestGetStub = sinon.stub(axios, 'request'); requestGetStub.resolves(apiResponse); source = { - token: 'source-token', + token: cancelToken, cancel: sinon.stub() }; sinon.stub(axios.CancelToken, 'source').returns(source); - wrapper = new RequestWrapper(escherRequestOptions, 'http:'); + wrapper = new RequestWrapper(extendedRequestOptions, 'http:'); }); it('should send GET request and return its response', async () => { @@ -85,7 +100,7 @@ describe('RequestWrapper', function() { httpsAgent: new https.Agent({ keepAlive: true }) }; wrapper = new RequestWrapper( - Object.assign(agents, escherRequestOptions), + Object.assign(agents, extendedRequestOptions), 'http:' ); @@ -104,17 +119,18 @@ describe('RequestWrapper', function() { await wrapper.send(); throw new Error('Error should have been thrown'); } catch (err) { - expect(err).to.be.an.instanceof(EscherRequestError); - expect(err.message).to.eql('Error in http response (status: 400)'); - expect(err.code).to.eql(400); - expect(err.data).to.eql({ replyText: 'Unknown route' }); + const error = err as EscherRequestError; + expect(error).to.be.an.instanceof(EscherRequestError); + expect(error.message).to.eql('Error in http response (status: 400)'); + expect(error.code).to.eql(400); + expect(error.data).to.eql({ replyText: 'Unknown route' }); } }); describe('when empty response is allowed', function() { beforeEach(function() { - escherRequestOptions.allowEmptyResponse = true; - wrapper = new RequestWrapper(escherRequestOptions, 'http:'); + extendedRequestOptions.allowEmptyResponse = true; + wrapper = new RequestWrapper(extendedRequestOptions, 'http:'); }); @@ -135,9 +151,10 @@ describe('RequestWrapper', function() { try { await wrapper.send(); } catch (err) { - expect(err).to.be.an.instanceof(EscherRequestError); - expect(err.message).to.match(/Unexpected end/); - expect(err.code).to.eql(500); + const error = err as EscherRequestError; + expect(error).to.be.an.instanceof(EscherRequestError); + expect(error.message).to.match(/Unexpected end/); + expect(error.code).to.eql(500); return; } throw new Error('Error should have been thrown'); @@ -151,10 +168,11 @@ describe('RequestWrapper', function() { try { await wrapper.send(); } catch (err) { - expect(err).to.be.an.instanceof(EscherRequestError); - expect(err.message).to.eql('Empty http response'); - expect(err.code).to.eql(500); - expect(err.data).to.eql(expectedApiResponse.statusMessage); + const error = err as EscherRequestError; + expect(error).to.be.an.instanceof(EscherRequestError); + expect(error.message).to.eql('Empty http response'); + expect(error.code).to.eql(500); + expect(error.data).to.eql(expectedApiResponse.statusMessage); return; } throw new Error('Error should have been thrown'); @@ -167,7 +185,8 @@ describe('RequestWrapper', function() { try { await wrapper.send(); } catch (err) { - expect(err.data).to.eql(apiResponse.statusText); + const error = err as EscherRequestError; + expect(error.data).to.eql(apiResponse.statusText); return; } throw new Error('Error should have been thrown'); @@ -182,17 +201,18 @@ describe('RequestWrapper', function() { await wrapper.send(); throw new Error('should throw'); } catch (err) { - expect(err).to.be.an.instanceOf(EscherRequestError); - expect(err.code).to.eql(apiResponse.status); - expect(err.message).to.eql('Error in http response (status: 404)'); - expect(err.data).to.eql(apiResponse.data); + const error = err as EscherRequestError; + expect(error).to.be.an.instanceOf(EscherRequestError); + expect(error.code).to.eql(apiResponse.status); + expect(error.message).to.eql('Error in http response (status: 404)'); + expect(error.data).to.eql(apiResponse.data); } }); }); describe('when there was an axios error', function() { - let isCancel; - let axiosError; + let isCancel: any; + let axiosError: any; beforeEach(function() { axiosError = { @@ -240,9 +260,10 @@ describe('RequestWrapper', function() { await wrapper.send(); throw new Error('should throw SuiteRequestError'); } catch (err) { - expect(err.originalCode).to.eql('ECONNABORTED'); - expect(err.code).to.eql(503); - expect(err.data).to.eql({ replyText: 'axios error message' }); + const error = err as EscherRequestError; + expect(error.originalCode).to.eql('ECONNABORTED'); + expect(error.code).to.eql(503); + expect(error.data).to.eql({ replyText: 'axios error message' }); } }); }); @@ -254,9 +275,10 @@ describe('RequestWrapper', function() { try { await wrapper.send(); } catch (err) { - expect(err).to.be.an.instanceof(EscherRequestError); - expect(err.message).to.match(/Unexpected token/); - expect(err.code).to.eql(500); + const error = err as EscherRequestError; + expect(error).to.be.an.instanceof(EscherRequestError); + expect(error.message).to.match(/Unexpected token/); + expect(error.code).to.eql(500); return; } throw new Error('Error should have been thrown'); @@ -273,11 +295,11 @@ describe('RequestWrapper', function() { }); it('should send GET request with given timeout in options', async () => { - escherRequestOptions.timeout = 60000; - await (new RequestWrapper(escherRequestOptions, 'http:')).send(); + extendedRequestOptions.timeout = 60000; + await (new RequestWrapper(extendedRequestOptions, 'http:')).send(); const requestArgument = requestGetStub.args[0][0]; - expect(requestArgument.timeout).to.eql(escherRequestOptions.timeout); + expect(requestArgument.timeout).to.eql(extendedRequestOptions.timeout); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 55cb716..77a3f1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,6 @@ { "exclude": ["dist", "node_modules", "src/**/*.spec.js"], "include": ["src/**/*.ts", "src/**/*.js"], - "files": [ - "src/escher-auth.d.ts" - ], "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ @@ -104,5 +101,8 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "ts-node": { + "files": true } }