From 25790fc6fb288c9bb12d4685d47e1250c18a5675 Mon Sep 17 00:00:00 2001 From: Robert Simari Date: Fri, 23 Apr 2021 17:56:06 -0700 Subject: [PATCH] feat: add setApiVersion method and SmartcarErrorV2 class (#122) This release adds support for v2.0 of Smartcar's API by introducing the `smartcar.setApiVersion` method. We have also introduced a `SmartcarErrorV2` class whose fields match the error fields returned by v2.0 of the API as documented on the [API Reference](https://smartcar.com/docs/api/?version=v2.0#errors). This class extends the `SmartcarError` class to ease the migration process. For a detailed breakdown of the changes and how to migrate see our [API Changelog for v2.0](https://smartcar.com/docs/changelog/v2.0/) and our [v2.0 Error Guides](https://smartcar.com/docs/errors/v2.0/billing). Co-authored-by: Gurpreet Atwal --- doc/readme.md | 105 +++++++++++++++++++++++++++++++++++++ index.js | 11 +++- lib/config.json | 4 +- lib/errors.js | 71 +++++++++++++++++++++++++ lib/util.js | 5 +- package.json | 2 +- test/end-to-end/vehicle.js | 1 + test/unit/lib/errors.js | 11 +++- test/unit/lib/util.js | 51 ++++++++++++++++++ 9 files changed, 255 insertions(+), 6 deletions(-) diff --git a/doc/readme.md b/doc/readme.md index 2158b024..e70b5f18 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -68,6 +68,7 @@ Smartcar Node SDK documentation. * [.errors](#module_smartcar.errors) * [.Vehicle](#module_smartcar.Vehicle) * [.AuthClient](#module_smartcar.AuthClient) + * [.setApiVersion(version)](#module_smartcar.setApiVersion) * [.isExpired(expiration)](#module_smartcar.isExpired) ⇒ Boolean * [.getVehicleIds(token, [paging])](#module_smartcar.getVehicleIds) ⇒ [Promise.<VehicleIds>](#module_smartcar..VehicleIds) * [.getUserId(token)](#module_smartcar.getUserId) ⇒ Promise.<String> @@ -89,6 +90,17 @@ Smartcar Node SDK documentation. ### smartcar.AuthClient **Kind**: static property of [smartcar](#module_smartcar) **See**: [AuthClient](#AuthClient) + + +### smartcar.setApiVersion(version) +Sets the version of Smartcar API you are using + +**Kind**: static method of [smartcar](#module_smartcar) + +| Param | Type | +| --- | --- | +| version | String | + ### smartcar.isExpired(expiration) ⇒ Boolean @@ -171,6 +183,16 @@ Return the user's id. ## errors * [errors](#module_errors) + * [.SmartcarErrorV2](#module_errors.SmartcarErrorV2) + * [new errors.SmartcarErrorV2(error)](#new_module_errors.SmartcarErrorV2_new) + * [.type](#module_errors.SmartcarErrorV2+type) : string + * [.code](#module_errors.SmartcarErrorV2+code) : string + * [.description](#module_errors.SmartcarErrorV2+description) : string + * [.statusCode](#module_errors.SmartcarErrorV2+statusCode) : number + * [.requestId](#module_errors.SmartcarErrorV2+requestId) : string + * [.resolution](#module_errors.SmartcarErrorV2+resolution) : string + * [.docURL](#module_errors.SmartcarErrorV2+docURL) : string + * [.detail](#module_errors.SmartcarErrorV2+detail) : Array.<object> * [.SmartcarError(message)](#module_errors.SmartcarError) ⇐ Error * [.ValidationError(message)](#module_errors.ValidationError) ⇐ SmartcarError * [.AuthenticationError(message)](#module_errors.AuthenticationError) ⇐ SmartcarError @@ -184,6 +206,89 @@ Return the user's id. * [.SmartcarNotCapableError(message)](#module_errors.SmartcarNotCapableError) ⇐ SmartcarError * [.GatewayTimeoutError(message)](#module_errors.GatewayTimeoutError) + + +### errors.SmartcarErrorV2 +Enhanced errors from API v2.0 +Please see our [v2.0 error guides](https://smartcar.com/docs/errors/v2.0/billing) to see a list of all the possible error types and codes + +**Kind**: static class of [errors](#module_errors) + +* [.SmartcarErrorV2](#module_errors.SmartcarErrorV2) + * [new errors.SmartcarErrorV2(error)](#new_module_errors.SmartcarErrorV2_new) + * [.type](#module_errors.SmartcarErrorV2+type) : string + * [.code](#module_errors.SmartcarErrorV2+code) : string + * [.description](#module_errors.SmartcarErrorV2+description) : string + * [.statusCode](#module_errors.SmartcarErrorV2+statusCode) : number + * [.requestId](#module_errors.SmartcarErrorV2+requestId) : string + * [.resolution](#module_errors.SmartcarErrorV2+resolution) : string + * [.docURL](#module_errors.SmartcarErrorV2+docURL) : string + * [.detail](#module_errors.SmartcarErrorV2+detail) : Array.<object> + + + +#### new errors.SmartcarErrorV2(error) + +| Param | Type | Description | +| --- | --- | --- | +| error | Object \| String | response body from a v2.0 request | + + + +#### smartcarErrorV2.type : string +Type of error + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.code : string +Error code + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.description : string +Description of meaning of the error + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.statusCode : number +HTTP status code + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.requestId : string +Unique identifier for request + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.resolution : string +Possible resolution for fixing the error + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.docURL : string +Reference to Smartcar documentation + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public + + +#### smartcarErrorV2.detail : Array.<object> +Further detail about the error + +**Kind**: instance property of [SmartcarErrorV2](#module_errors.SmartcarErrorV2) +**Access**: public ### errors.SmartcarError(message) ⇐ Error diff --git a/index.js b/index.js index 65ef5921..0a5fb9d6 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,16 @@ const smartcar = { }; /* eslint-enable global-require */ + +/** + * Sets the version of Smartcar API you are using + * @method + * @param {String} version + */ +smartcar.setApiVersion = function(version) { + config.version = version; +}; + /** * Check if a token has expired. * @@ -85,7 +95,6 @@ smartcar.getVehicleIds = Promise.method(function(token, paging) { if (!_.isString(token)) { throw new TypeError('"token" argument must be a string'); } - return util.request.get(util.getUrl(), { auth: { bearer: token, diff --git a/lib/config.json b/lib/config.json index d9b3a1f5..be86d4a0 100644 --- a/lib/config.json +++ b/lib/config.json @@ -2,6 +2,6 @@ "auth": "https://auth.smartcar.com", "api": "https://api.smartcar.com", "connect": "https://connect.smartcar.com", - "version": "1.0", - "timeout": 310000 + "timeout": 310000, + "version": "1.0" } diff --git a/lib/errors.js b/lib/errors.js index 3b67fb90..9f6c072c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -31,6 +31,7 @@ util.inherits(errors.SmartcarError, Error); errors.ValidationError = function(message) { this.name = 'validation_error'; this.statusCode = 400; + /* istanbul ignore next */ this.message = message || 'Invalid or missing request parameters'; Error.captureStackTrace(this, this.constructor); }; @@ -180,9 +181,79 @@ util.inherits(errors.SmartcarNotCapableError, errors.SmartcarError); */ errors.GatewayTimeoutError = function(message) { this.name = 'smartcar_gateway_timeout_error'; + /* istanbul ignore next */ this.message = message || 'ELB threw a 504.'; Error.captureStackTrace(this, this.constructor); }; util.inherits(errors.GatewayTimeoutError, errors.SmartcarError); +/** + * Enhanced errors from API v2.0 + * Please see our [v2.0 error guides]{@link https://smartcar.com/docs/errors/v2.0/billing} to see a list of all the possible error types and codes + * + * @param {Object|String} error - response body from a v2.0 request + */ +errors.SmartcarErrorV2 = class extends errors.SmartcarError { + constructor(error) { + if (typeof error === 'string') { + super(error); + this.description = error; + this.name = 'SmartcarErrorV2'; + return; + } else { + super(`${error.type}:${error.code} - ${error.description}`); + } + this.name = 'SmartcarErrorV2'; + + /** + * Type of error + * @type {string} + * @public + */ + this.type = error.type; + /** + * Error code + * @type {string} + * @public + */ + this.code = error.code; + /** + * Description of meaning of the error + * @type {string} + * @public + */ + this.description = error.description; + /** + * HTTP status code + * @type {number} + * @public + */ + this.statusCode = error.statusCode; + /** + * Unique identifier for request + * @type {string} + * @public + */ + this.requestId = error.requestId; + /** + * Possible resolution for fixing the error + * @type {string} + * @public + */ + this.resolution = error.resolution; + /** + * Reference to Smartcar documentation + * @type {string} + * @public + */ + this.docURL = error.docURL; + /** + * Further detail about the error + * @type {object[]} + * @public + */ + this.detail = error.detail; + } +}; + module.exports = errors; diff --git a/lib/util.js b/lib/util.js index b0116b24..2ac186a0 100644 --- a/lib/util.js +++ b/lib/util.js @@ -72,10 +72,13 @@ util.wrap = function(promise) { }; util.catch = function(caught) { - const options = caught.options; const body = _.get(caught, 'response.body', {}); + if (options.uri.includes('/v2.0/')) { + throw new errors.SmartcarErrorV2(body); + } + switch (caught.statusCode) { case 400: throw new errors.ValidationError(body.error_description || body.message); diff --git a/package.json b/package.json index ffe40a35..e1e19001 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "cover": "nyc npm run test:unit -s", "cover:integration": "nyc npm test:integration -s", "jsdoc": "jsdoc -c .jsdoc.json .", - "docs": "mkdir -p doc && jsdoc2md --example-lang js --template doc/.template.hbs --files .docs.js index.js lib/* | sed 's/[ \t]*$//' > doc/readme.md" + "docs": "mkdir -p doc && jsdoc2md --example-lang js --template doc/.template.hbs --files .docs.js index.js lib/* | sed -e 's/[ \t]*$//' -e 's/\\[\\ '//g' -e 's/'\\ \\]//g' > doc/readme.md" }, "dependencies": { "bluebird": "^3.5.5", diff --git a/test/end-to-end/vehicle.js b/test/end-to-end/vehicle.js index 4fce01f7..e33341f2 100644 --- a/test/end-to-end/vehicle.js +++ b/test/end-to-end/vehicle.js @@ -34,6 +34,7 @@ test.before(async(t) => { getVehicle('VOLKSWAGEN', ['required:control_charge']), ]); + smartcar.setApiVersion('1.0'); t.context = {volt, egolf}; }); diff --git a/test/unit/lib/errors.js b/test/unit/lib/errors.js index c221f984..69751e44 100644 --- a/test/unit/lib/errors.js +++ b/test/unit/lib/errors.js @@ -15,7 +15,7 @@ test('inheritance check', function(t) { ); break; default: - t.true(new errors[error]() instanceof errors.SmartcarError); + t.true(new errors[error]('Message') instanceof errors.SmartcarError); } }); @@ -30,6 +30,15 @@ test('message check', function(t) { case 'VehicleStateError': t.regex(new errors[error]('R2D2', {code: 'VS_000'}).message, /R2D2/); break; + case 'SmartcarErrorV2': + t.regex(new errors[error]('R2D2').description, /R2D2/); + const sampleError = { + type: '', + code: '', + description: '', + }; + t.is(new errors[error](sampleError).message, ': - '); + break; default: t.regex(new errors[error]('R2D2').message, /R2D2/); } diff --git a/test/unit/lib/util.js b/test/unit/lib/util.js index 8a08b481..08aed5f1 100644 --- a/test/unit/lib/util.js +++ b/test/unit/lib/util.js @@ -7,11 +7,17 @@ const Promise = require('bluebird'); const {StatusCodeError} = require('request-promise/errors'); const util = require('../../../lib/util'); +const smartcar = require('../../../'); const config = require('../../../lib/config'); const errors = require('../../../lib/errors'); const API_URL = config.api + '/v' + config.version; +test.afterEach((t) => { + smartcar.setApiVersion('1.0'); + t.true(config.version === '1.0'); +}); + test('formatAccess', function(t) { /* eslint-disable camelcase */ @@ -63,6 +69,12 @@ test('getUrl - id & endpoint', function(t) { t.is(url, API_URL + '/vehicles/VID/odometer'); }); +test('getUrl - version 2.0', function(t) { + smartcar.setApiVersion('2.0'); + const url = util.getUrl('VID', 'odometer'); + t.is(url, 'https://api.smartcar.com/v2.0/vehicles/VID/odometer'); +}); + test('request - default opts', async function(t) { const n = nock('https://mock.com') .get('/test') @@ -291,3 +303,42 @@ test('catch - SmartcarError', async function(t) { t.true(n.isDone()); }); + +test.serial('catch - SmartcarErrorV2', async function(t) { + + const n = nock('https://api.smartcar.com/v2.0') + .get('/something') + .reply(500, { + type: 'type', + code: 'code', + description: 'description', + resolution: null, + detail: null, + requestId: '123', + docURL: null, + statusCode: 500, + }); + + const err = await t.throwsAsync(util.request('https://api.smartcar.com/v2.0/something')); + const boxed = t.throws(() => util.catch(err)); + + t.true(boxed instanceof errors.SmartcarErrorV2); + t.is(boxed.description, 'description'); + t.true(n.isDone()); + +}); + +test.serial('catch - SmartcarErrorV2 - string response', async function(t) { + + const n = nock('https://api.smartcar.com/v2.0') + .get('/something') + .reply(500, 'just a string response'); + + const err = await t.throwsAsync(util.request('https://api.smartcar.com/v2.0/something')); + const boxed = t.throws(() => util.catch(err)); + + t.true(boxed instanceof errors.SmartcarErrorV2); + t.is(boxed.description, 'just a string response'); + t.true(n.isDone()); + +});