diff --git a/.eslintignore b/.eslintignore index f9fc9c0..bec9442 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,3 @@ node_modules build -spec/**/*.json +__tests__/**/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d38e33f..44e904f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ### Changed - **Breaking change** Drop support of Node.js 12. The version [5.1.4](https://github.com/reportportal/client-javascript/releases/tag/v5.1.4) is the latest that supports it. +- The client now creates an instance of the `axios` HTTP client in the constructor. +- The `HOST` HTTP header is added to all requests as it was skipped by the HTTP client. ### Fixed -- Proxy support on HTTPS requests. Resolves [#30](https://github.com/reportportal/client-javascript/issues/30), related to [axios#4531](https://github.com/axios/axios/issues/4531). - Allow using `restClientConfig` in `checkConnect()` method. Thanks to [stevez](https://github.com/stevez). ### Security - Updated versions of vulnerable packages (braces). diff --git a/spec/client-id.spec.js b/__tests__/client-id.spec.js similarity index 100% rename from spec/client-id.spec.js rename to __tests__/client-id.spec.js diff --git a/spec/config.spec.js b/__tests__/config.spec.js similarity index 100% rename from spec/config.spec.js rename to __tests__/config.spec.js diff --git a/spec/helpers.spec.js b/__tests__/helpers.spec.js similarity index 84% rename from spec/helpers.spec.js rename to __tests__/helpers.spec.js index b477c8f..fe65089 100644 --- a/spec/helpers.spec.js +++ b/__tests__/helpers.spec.js @@ -2,7 +2,6 @@ const os = require('os'); const fs = require('fs'); const glob = require('glob'); const helpers = require('../lib/helpers'); -const RestClient = require('../lib/rest'); const pjson = require('../package.json'); describe('Helpers', () => { @@ -27,34 +26,6 @@ describe('Helpers', () => { }); }); - describe('getServerResults', () => { - it('calls RestClient#request', () => { - jest.spyOn(RestClient, 'request').mockImplementation(); - - helpers.getServerResult( - 'http://localhost:80/api/v1', - { userId: 1 }, - { - headers: { - 'X-Custom-Header': 'WOW', - }, - }, - 'POST', - ); - - expect(RestClient.request).toHaveBeenCalledWith( - 'POST', - 'http://localhost:80/api/v1', - { userId: 1 }, - { - headers: { - 'X-Custom-Header': 'WOW', - }, - }, - ); - }); - }); - describe('readLaunchesFromFile', () => { it('should return the right ids', () => { jest.spyOn(glob, 'sync').mockReturnValue(['rplaunch-fileOne.tmp', 'rplaunch-fileTwo.tmp']); diff --git a/spec/publicReportingAPI.spec.js b/__tests__/publicReportingAPI.spec.js similarity index 100% rename from spec/publicReportingAPI.spec.js rename to __tests__/publicReportingAPI.spec.js diff --git a/spec/report-portal-client.spec.js b/__tests__/report-portal-client.spec.js similarity index 95% rename from spec/report-portal-client.spec.js rename to __tests__/report-portal-client.spec.js index 5f36288..b715aef 100644 --- a/spec/report-portal-client.spec.js +++ b/__tests__/report-portal-client.spec.js @@ -1,6 +1,5 @@ const process = require('process'); const RPClient = require('../lib/report-portal-client'); -const RestClient = require('../lib/rest'); const helpers = require('../lib/helpers'); const { OUTPUT_TYPES } = require('../lib/constants/outputs'); @@ -134,41 +133,12 @@ describe('ReportPortal javascript client', () => { project: 'test', endpoint: 'https://abc.com', }); - jest.spyOn(RestClient, 'request').mockReturnValue(Promise.resolve('ok')); + jest.spyOn(client.restClient, 'request').mockReturnValue(Promise.resolve('ok')); const request = client.checkConnect(); return expect(request).resolves.toBeDefined(); }); - - it('client should include restClientConfig', () => { - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com/v1', - restClientConfig: { - proxy: false, - timeout: 0, - }, - }); - jest.spyOn(RestClient, 'request').mockImplementation(); - - client.checkConnect(); - - expect(RestClient.request).toHaveBeenCalledWith( - 'GET', - 'https://abc.com/v1/user', - {}, - { - headers: { - 'User-Agent': 'NodeJS', - Authorization: `bearer test`, - }, - proxy: false, - timeout: 0, - }, - ); - }); }); describe('triggerAnalyticsEvent', () => { @@ -278,15 +248,11 @@ describe('ReportPortal javascript client', () => { startTime: time, }); - expect(client.restClient.create).toHaveBeenCalledWith( - 'launch', - { - name: 'Test launch name', - startTime: time, - attributes: fakeSystemAttr, - }, - { headers: client.headers }, - ); + expect(client.restClient.create).toHaveBeenCalledWith('launch', { + name: 'Test launch name', + startTime: time, + attributes: fakeSystemAttr, + }); }); it('should call restClient with suitable parameters, attributes is concatenated', () => { @@ -312,22 +278,18 @@ describe('ReportPortal javascript client', () => { attributes: [{ value: 'value' }], }); - expect(client.restClient.create).toHaveBeenCalledWith( - 'launch', - { - name: 'Test launch name', - startTime: time, - attributes: [ - { value: 'value' }, - { - key: 'client', - value: 'client-name|1.0', - system: true, - }, - ], - }, - { headers: client.headers }, - ); + expect(client.restClient.create).toHaveBeenCalledWith('launch', { + name: 'Test launch name', + startTime: time, + attributes: [ + { value: 'value' }, + { + key: 'client', + value: 'client-name|1.0', + system: true, + }, + ], + }); }); it('dont start new launch if launchDataRQ.id is not empty', () => { @@ -599,9 +561,7 @@ describe('ReportPortal javascript client', () => { expect(promise.then).toBeDefined(); await promise; - expect(client.restClient.create).toHaveBeenCalledWith('launch/merge', fakeMergeDataRQ, { - headers: client.headers, - }); + expect(client.restClient.create).toHaveBeenCalledWith('launch/merge', fakeMergeDataRQ); }); it('should not call rest client if something went wrong', async () => { diff --git a/spec/rest.spec.js b/__tests__/rest.spec.js similarity index 93% rename from spec/rest.spec.js rename to __tests__/rest.spec.js index 74aa644..850b8e4 100644 --- a/spec/rest.spec.js +++ b/__tests__/rest.spec.js @@ -2,13 +2,14 @@ const nock = require('nock'); const isEqual = require('lodash/isEqual'); const http = require('http'); const RestClient = require('../lib/rest'); +const logger = require('../lib/logger'); describe('RestClient', () => { const options = { baseURL: 'http://report-portal-host:8080/api/v1', headers: { - Authorization: 'bearer 00000000-0000-0000-0000-000000000000', 'User-Agent': 'NodeJS', + Authorization: 'Bearer 00000000-0000-0000-0000-000000000000', }, restClientConfig: { agent: { @@ -34,6 +35,21 @@ describe('RestClient', () => { expect(restClient.baseURL).toBe(options.baseURL); expect(restClient.headers).toEqual(options.headers); expect(restClient.restClientConfig).toEqual(options.restClientConfig); + expect(restClient.axiosInstance).toBeDefined(); + }); + + it('adds Logger to axios instance if enabled', () => { + const spyLogger = jest.spyOn(logger, 'addLogger').mockReturnValue(); + const optionsWithLoggerEnabled = { + ...options, + restClientConfig: { + ...options.restClientConfig, + debug: true, + }, + }; + const client = new RestClient(optionsWithLoggerEnabled); + + expect(spyLogger).toHaveBeenCalledWith(client.axiosInstance); }); }); diff --git a/spec/statistics.spec.js b/__tests__/statistics.spec.js similarity index 100% rename from spec/statistics.spec.js rename to __tests__/statistics.spec.js diff --git a/jest.config.js b/jest.config.js index 4ca34a2..1070058 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,15 @@ module.exports = { moduleFileExtensions: ['js'], - "testMatch": [ - "/spec/**/*[sS]pec.js" - ], - coverageReporters: ["lcov", "text-summary"], + testRegex: '/__tests__/.*\\.(test|spec).js$', + testEnvironment: 'node', + collectCoverageFrom: ['lib/**/*.js', '!lib/logger.js'], + coverageThreshold: { + global: { + branches: 80, + functions: 75, + lines: 80, + statements: 80, + }, + }, bail: false, }; diff --git a/lib/helpers.js b/lib/helpers.js index 33ee556..6e3e11e 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -22,8 +22,9 @@ module.exports = { return new Date().valueOf(); }, + // TODO: deprecate and remove getServerResult(url, request, options, method) { - return RestClient.request(method, url, request, options); + return new RestClient(options).request(method, url, request, options); }, readLaunchesFromFile() { diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..9eb54e6 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,41 @@ +const addLogger = (axiosInstance) => { + axiosInstance.interceptors.request.use((config) => { + const startDate = new Date(); + // eslint-disable-next-line no-param-reassign + config.startTime = startDate.valueOf(); + + console.log(`Request method=${config.method} url=${config.url} [${startDate.toISOString()}]`); + + return config; + }); + + axiosInstance.interceptors.response.use( + (response) => { + const date = new Date(); + const { status, config } = response; + + console.log( + `Response status=${status} url=${config.url} time=${ + date.valueOf() - config.startTime + }ms [${date.toISOString()}]`, + ); + + return response; + }, + (error) => { + const date = new Date(); + const { response, config } = error; + const status = response ? response.status : null; + + console.log( + `Response ${status ? `status=${status}` : `message='${error.message}'`} url=${ + config.url + } time=${date.valueOf() - config.startTime}ms [${date.toISOString()}]`, + ); + + return Promise.reject(error); + }, + ); +}; + +module.exports = { addLogger }; diff --git a/lib/report-portal-client.js b/lib/report-portal-client.js index 9cc7830..44216ea 100644 --- a/lib/report-portal-client.js +++ b/lib/report-portal-client.js @@ -41,7 +41,8 @@ class RPClient { this.baseURL = [this.config.endpoint, this.config.project].join('/'); this.headers = { 'User-Agent': 'NodeJS', - Authorization: `bearer ${this.apiKey}`, + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `Bearer ${this.apiKey}`, ...(this.config.headers || {}), }; this.helpers = helpers; @@ -130,12 +131,7 @@ class RPClient { checkConnect() { const url = [this.config.endpoint.replace('/v2', '/v1'), 'user'].join('/'); - return RestClient.request( - 'GET', - url, - {}, - { headers: this.headers, ...this.restClient.getRestConfig() }, - ); + return this.restClient.request('GET', url, {}); } async triggerStatisticsEvent() { @@ -206,7 +202,7 @@ class RPClient { this.map[tempId] = this.getNewItemObj((resolve, reject) => { const url = 'launch'; this.logDebug(`Start launch with tempId ${tempId}`, launchDataRQ); - this.restClient.create(url, launchData, { headers: this.headers }).then( + this.restClient.create(url, launchData).then( (response) => { this.map[tempId].realId = response.id; this.launchUuid = response.id; @@ -265,7 +261,7 @@ class RPClient { () => { this.logDebug(`Finish launch with tempId ${launchTempId}`, finishExecutionData); const url = ['launch', launchObj.realId, 'finish'].join('/'); - this.restClient.update(url, finishExecutionData, { headers: this.headers }).then( + this.restClient.update(url, finishExecutionData).then( (response) => { this.logDebug(`Success finish launch with tempId ${launchTempId}`, response); console.log(`\nReportPortal Launch Link: ${response.link}`); @@ -337,11 +333,13 @@ class RPClient { 'filter.in.uuid': launchUUIds, 'page.size': launchUUIds.length, }); - const launchSearchUrl = this.config.mode === 'DEBUG' ? - `launch/mode?${params.toString()}` : `launch?${params.toString()}`; + const launchSearchUrl = + this.config.mode === 'DEBUG' + ? `launch/mode?${params.toString()}` + : `launch?${params.toString()}`; this.logDebug(`Find launches with UUIDs to merge: ${launchUUIds}`); return this.restClient - .retrieveSyncAPI(launchSearchUrl, { headers: this.headers }) + .retrieveSyncAPI(launchSearchUrl) .then( (response) => { const launchIds = response.content.map((launch) => launch.id); @@ -357,7 +355,7 @@ class RPClient { const request = this.getMergeLaunchesRequest(launchIds, mergeOptions); this.logDebug(`Merge launches with ids: ${launchIds}`, request); const mergeURL = 'launch/merge'; - return this.restClient.create(mergeURL, request, { headers: this.headers }); + return this.restClient.create(mergeURL, request); }) .then((response) => { this.logDebug(`Launches with UUIDs: ${launchUUIds} were successfully merged!`); @@ -426,7 +424,7 @@ class RPClient { () => { const url = ['launch', launchObj.realId, 'update'].join('/'); this.logDebug(`Update launch with tempId ${launchTempId}`, launchData); - this.restClient.update(url, launchData, { headers: this.headers }).then( + this.restClient.update(url, launchData).then( (response) => { this.logDebug(`Launch with tempId ${launchTempId} were successfully updated`, response); resolvePromise(response); @@ -534,7 +532,7 @@ class RPClient { } testItemData.launchUuid = realLaunchId; this.logDebug(`Start test item with tempId ${tempId}`, testItemData); - this.restClient.create(url, testItemData, { headers: this.headers }).then( + this.restClient.create(url, testItemData).then( (response) => { this.logDebug(`Success start item with tempId ${tempId}`, response); this.map[tempId].realId = response.id; @@ -721,7 +719,6 @@ class RPClient { return this.restClient.create( url, Object.assign(saveLogRQ, { launchUuid }, isItemUuid && { itemUuid }), - { headers: this.headers }, ); }; return this.saveLog(itemObj, requestPromise); @@ -777,7 +774,6 @@ class RPClient { return this.restClient .create(url, this.buildMultiPartStream([saveLogRQ], fileObj, MULTIPART_BOUNDARY), { headers: { - ...this.headers, 'Content-Type': `multipart/form-data; boundary=${MULTIPART_BOUNDARY}`, }, }) @@ -840,9 +836,7 @@ class RPClient { const url = ['item', itemObj.realId].join('/'); this.logDebug(`Finish test item with tempId ${itemTempId}`, itemObj); this.restClient - .update(url, Object.assign(finishTestItemData, { launchUuid: this.launchUuid }), { - headers: this.headers, - }) + .update(url, Object.assign(finishTestItemData, { launchUuid: this.launchUuid })) .then( (response) => { this.logDebug(`Success finish item with tempId ${itemTempId}`, response); diff --git a/lib/rest.js b/lib/rest.js index 45f9df9..3528329 100644 --- a/lib/rest.js +++ b/lib/rest.js @@ -2,6 +2,7 @@ const axios = require('axios'); const axiosRetry = require('axios-retry').default; const http = require('http'); const https = require('https'); +const logger = require('./logger'); const DEFAULT_MAX_CONNECTION_TIME_MS = 30000; @@ -17,7 +18,15 @@ class RestClient { this.headers = options.headers; this.restClientConfig = options.restClientConfig; - addLogger(this.restClientConfig ? this.restClientConfig.debug : false); + this.axiosInstance = axios.create({ + timeout: DEFAULT_MAX_CONNECTION_TIME_MS, + headers: this.headers, + ...this.getRestConfig(this.restClientConfig), + }); + + if (this.restClientConfig?.debug) { + logger.addLogger(this.axiosInstance); + } } buildPath(path) { @@ -28,14 +37,18 @@ class RestClient { return [this.baseURL.replace('/v2', '/v1'), path].join('/'); } - static request(method, url, data, options = {}) { - return axios({ - method, - url, - data, - timeout: DEFAULT_MAX_CONNECTION_TIME_MS, - ...options, - }) + request(method, url, data, options = {}) { + return this.axiosInstance + .request({ + method, + url, + data, + ...options, + headers: { + HOST: new URL(url).host, + ...options.headers, + }, + }) .then((response) => response.data) .catch((error) => { const errorMessage = error.message; @@ -74,85 +87,45 @@ method: ${method}`, return config; } - create(path, data, options = { headers: this.headers }) { - return RestClient.request('POST', this.buildPath(path), data, { + create(path, data, options = {}) { + return this.request('POST', this.buildPath(path), data, { ...options, - ...this.getRestConfig(), }); } - retrieve(path, options = { headers: this.headers }) { - return RestClient.request( + retrieve(path, options = {}) { + return this.request( 'GET', this.buildPath(path), {}, - { ...options, ...this.getRestConfig() }, + { + ...options, + }, ); } - update(path, data, options = { headers: this.headers }) { - return RestClient.request('PUT', this.buildPath(path), data, { + update(path, data, options = {}) { + return this.request('PUT', this.buildPath(path), data, { ...options, - ...this.getRestConfig(), }); } - delete(path, data, options = { headers: this.headers }) { - return RestClient.request('DELETE', this.buildPath(path), data, { + delete(path, data, options = {}) { + return this.request('DELETE', this.buildPath(path), data, { ...options, - ...this.getRestConfig(), }); } - retrieveSyncAPI(path, options = { headers: this.headers }) { - return RestClient.request( + retrieveSyncAPI(path, options = {}) { + return this.request( 'GET', this.buildPathToSyncAPI(path), {}, - { ...options, ...this.getRestConfig() }, - ); - } -} - -const addLogger = (debug) => { - if (debug) { - axios.interceptors.request.use((config) => { - const startDate = new Date(); - config.startTime = startDate.valueOf(); - - console.log(`Request method=${config.method} url=${config.url} [${startDate.toISOString()}]`); - - return config; - }); - - axios.interceptors.response.use( - (response) => { - const date = new Date(); - const { status, config } = response; - - console.log( - `Response status=${status} url=${config.url} time=${ - date.valueOf() - config.startTime - }ms [${date.toISOString()}]`, - ); - - return response; - }, - (error) => { - const date = new Date(); - const { response, config } = error; - const status = response ? response.status : null; - - console.log( - `Response ${status ? 'status=' + status : "message='" + error.message + "'"} url=${ - config.url - } time=${date.valueOf() - config.startTime}ms [${date.toISOString()}]`, - ); - - return Promise.reject(error); + { + ...options, }, ); } -}; +} module.exports = RestClient; diff --git a/package-lock.json b/package-lock.json index 0d1540e..ff68ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,11 @@ "version": "5.1.4", "license": "Apache-2.0", "dependencies": { - "axios": "^1.6.8", + "axios": "^1.7.7", "axios-retry": "^4.1.0", "glob": "^8.1.0", "ini": "^2.0.0", + "node-fetch": "^2.7.0", "uniqid": "^5.4.0", "uuid": "^9.0.1" }, @@ -38,7 +39,7 @@ "typescript": "^4.9.5" }, "engines": { - "node": ">=12.x" + "node": ">=14.x" } }, "node_modules/@ampproject/remapping": { @@ -1854,9 +1855,9 @@ } }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4985,6 +4986,25 @@ "node": ">= 10.13" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6078,6 +6098,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-jest": { "version": "29.1.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.5.tgz", @@ -6463,6 +6488,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index eaa9f43..d3ebe5a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "npm run clean && tsc", "clean": "rimraf ./build", - "lint": "eslint ./statistics/**/* ./lib/**/* ./spec/**/*", + "lint": "eslint ./statistics/**/* ./lib/**/* ./__tests__/**/*", "format": "npm run lint -- --fix", "test": "jest", "test:coverage": "jest --coverage" @@ -24,7 +24,7 @@ "node": ">=14.x" }, "dependencies": { - "axios": "^1.6.8", + "axios": "^1.7.7", "axios-retry": "^4.1.0", "glob": "^8.1.0", "ini": "^2.0.0", diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index fc8520e..f507e4f 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,3 +1,4 @@ { - "extends": "./tsconfig.json" + "extends": "./tsconfig.json", + "exclude": ["node_modules"] } diff --git a/tsconfig.json b/tsconfig.json index b5a99be..b655f00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "outDir": "./build" }, "include": [ - "statistics/**/*", "lib/**/*", "spec/**/*"], - "exclude": ["node_modules"] + "statistics/**/*", "lib/**/*", "__tests__/**/*"], + "exclude": ["node_modules", "__tests__"] }