From d3a8a67fd49214fd7ebfaccbcd738222efe36b60 Mon Sep 17 00:00:00 2001 From: Naomi Date: Thu, 27 Jan 2022 16:44:44 -0800 Subject: [PATCH] feat(vehicle): add general purpose request method to Vehicle prototype (#143) Add a new general purpose/BSE related method Vehicle.prototype.request = async function( method, path, body = {}, headers = {} ) to Vehicle that allows for making requests to the Smartcar API. Also, add E2E tests for this new request method. Test Plan: Use getting-started-app to make requests with this method once it's in staging/prod. * feat(vehicle): add general purpose request method to Vehicle prototype * chore: switched post request test to batch instead of lock * chore: Add Response type and improve attribute checks * Update readme * test: override non-Smartcar headers * fix: resolve linting problems Co-authored-by: Naomi Perez --- doc/readme.md | 45 ++++++++++++++++++++ lib/vehicle.js | 59 ++++++++++++++++++++++++++ test/end-to-end/vehicle.js | 87 +++++++++++++++++++++++++++++++++++++- test/unit/lib/vehicle.js | 19 +++++++++ 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/doc/readme.md b/doc/readme.md index 34ad1a5..4067785 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -54,6 +54,8 @@ the following fields :

Batch : Object
+
Response : Object
+
Meta : Object
Vin : Object
@@ -536,6 +538,7 @@ Initializes a new Service object to make requests to the Smartcar API. * [.subscribe(webhookId)](#Vehicle+subscribe) ⇒ Object * [.unsubscribe(amt, webhookId)](#Vehicle+unsubscribe) ⇒ [Meta](#Meta) * [.batch(paths)](#Vehicle+batch) ⇒ [Batch](#Batch) + * [.request(method, path, body, headers)](#Vehicle+request) ⇒ [Response](#Response) * [.vin()](#Vehicle+vin) ⇒ [Vin](#Vin) * [.charge()](#Vehicle+charge) ⇒ [Charge](#Charge) * [.battery()](#Vehicle+battery) ⇒ [Battery](#Battery) @@ -638,6 +641,26 @@ Make batch requests for supported items | --- | --- | --- | | paths | Array.<String> | A list of paths of endpoints to send requests to. | + + +### vehicle.request(method, path, body, headers) ⇒ [Response](#Response) +General purpose method to make a request to a Smartcar endpoint. + +**Kind**: instance method of [Vehicle](#Vehicle) +**Throws**: + +- [SmartcarError](#SmartcarError) - an instance of SmartcarError. + See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors) + for all possible errors. + + +| Param | Type | Description | +| --- | --- | --- | +| method | String | The HTTP request method to use. | +| path | String | The path to make the request to. | +| body | Object | The request body. | +| headers | Object | The headers to inlcude in the request. | + ### vehicle.vin() ⇒ [Vin](#Vin) @@ -936,6 +959,28 @@ the following fields : "location" : function() => returns odometer location or throws SmartcarError, } ``` + + +## Response : Object +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| body | String | The response body | +| meta | [Meta](#Meta) | | + +**Example** +```js +{ + body: { distance: 59801.6373441601 }, + meta: { + dataAge: 2022-01-20T02:55:25.041Z, + unitSystem: 'imperial', + requestId: 'f787849d-d228-482d-345f-459a5154sg73' + } +} +``` ## Meta : Object diff --git a/lib/vehicle.js b/lib/vehicle.js index 353a44c..72353f9 100644 --- a/lib/vehicle.js +++ b/lib/vehicle.js @@ -212,6 +212,65 @@ Vehicle.prototype.batch = async function(paths) { return response; }; +/** + * @type {Object} + * @typedef Response + * @property {String} body - The response body + * @property {Meta} meta + * + * @example + * { + * body: { distance: 59801.6373441601 }, + * meta: { + * dataAge: 2022-01-20T02:55:25.041Z, + * unitSystem: 'imperial', + * requestId: 'f787849d-d228-482d-345f-459a5154sg73' + * } + * } + */ +/** + * General purpose method to make a request to a Smartcar endpoint. + * + * @method + * @param {String} method - The HTTP request method to use. + * @param {String} path - The path to make the request to. + * @param {Object} body - The request body. + * @param {Object} headers - The headers to inlcude in the request. + * @return {Response} + * @throws {SmartcarError} - an instance of SmartcarError. + * See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors) + * for all possible errors. + */ +Vehicle.prototype.request = async function( + method, + path, + body = {}, + headers = {} +) { + const options = { + baseUrl: util.getUrl(this.id, '', this.version), + headers: _.merge({'sc-unit-system': this.unitSystem}, headers), + }; + + if (!headers.Authorization) { + options.auth = {bearer: this.token}; + } + + const rawResponse = await new SmartcarService(options).request( + method, + path, + {body} + ); + + const response = { + body: _.omit(rawResponse, 'meta'), + meta: rawResponse.meta, + }; + + return response; +}; + + /* JSDOC for dynamically generated methods */ /** * @type {Object} diff --git a/test/end-to-end/vehicle.js b/test/end-to-end/vehicle.js index ad091dd..6a745eb 100644 --- a/test/end-to-end/vehicle.js +++ b/test/end-to-end/vehicle.js @@ -153,7 +153,7 @@ test('vehicle odometer', async(t) => { 'distance', 'meta', ]), - [], + [] ); t.truthy(typeof response.distance === 'number'); @@ -179,7 +179,6 @@ test('vehicle location', async(t) => { t.is(response.meta.requestId.length, 36); }); - test('vehicle attributes', async(t) => { const response = await t.context.volt.attributes(); t.deepEqual( @@ -374,6 +373,90 @@ test('vehicle batch', async(t) => { t.is(location.meta.requestId.length, 36); }); +test('vehicle request - odometer', async(t) => { + const response = await t.context.volt.request( + 'get', + 'odometer', + {}, + { + 'sc-unit-system': 'imperial', + } + ); + + t.deepEqual( + _.xor(_.keys(response), [ + 'body', + 'meta', + ]), + [] + ); + + t.truthy(typeof response.body.distance === 'number'); + t.truthy(response.meta.dataAge instanceof Date); + t.is(response.meta.requestId.length, 36); + t.is(response.meta.unitSystem, 'imperial'); +}); + +test('vehicle request - batch', async(t) => { + const response = await t.context.volt.request('post', 'batch', { + requests: [ + {path: '/odometer'}, + {path: '/tires/pressure'}, + ], + }); + + t.deepEqual( + _.xor(_.keys(response), [ + 'body', + 'meta', + ]), + [] + ); + + t.truthy(response.meta.requestId.length, 36); + + t.truthy(response.body.responses[0].path === '/odometer'); + t.truthy(response.body.responses[0].code === 200); + t.truthy(response.body.responses[0].headers['sc-unit-system'] === 'metric'); + t.truthy(new Date( + response.body.responses[0].headers['sc-data-age'] + ) instanceof Date); + t.truthy(typeof response.body.responses[0].body.distance === 'number'); + + t.truthy(response.body.responses[1].path === '/tires/pressure'); + t.truthy(response.body.responses[1].code === 200); + t.truthy(response.body.responses[1].headers['sc-unit-system'] === 'metric'); + t.truthy(new Date( + response.body.responses[1].headers['sc-data-age'] + ) instanceof Date); + t.truthy(typeof response.body.responses[1].body.frontLeft === 'number'); + t.truthy(typeof response.body.responses[1].body.frontRight === 'number'); + t.truthy(typeof response.body.responses[1].body.backLeft === 'number'); + t.truthy(typeof response.body.responses[1].body.backRight === 'number'); + +}); + +test('vehicle request - override auth header', async(t) => { + const errorMessage = 'The authorization header is missing or malformed, ' + + 'or it contains invalid or expired authentication credentials. Please ' + + 'check for missing parameters, spelling and casing mistakes, and ' + + 'other syntax issues.'; + + await t.context.volt.request('get', + 'odometer', + {}, + { + 'sc-unit-system': 'imperial', + Authorization: 'Bearer abc', + } + ).catch((err) => { + t.is(err.statusCode, 401); + t.is(err.type, 'AUTHENTICATION'); + t.is(err.description, errorMessage); + t.is(err.docURL, 'https://smartcar.com/docs/errors/v2.0/other-errors/#authentication'); + }); +}); + test.after.always('vehicle disconnect', async(t) => { const response = await t.context.volt.disconnect(); t.deepEqual( diff --git a/test/unit/lib/vehicle.js b/test/unit/lib/vehicle.js index 077ac45..3e6e197 100644 --- a/test/unit/lib/vehicle.js +++ b/test/unit/lib/vehicle.js @@ -201,3 +201,22 @@ test('batch - error', async function(t) { t.is(error.message, 'monkeys_on_mars:undefined - yes, really'); t.is(error.type, 'monkeys_on_mars'); }); + +test('request - override non-sc headers', async function(t) { + t.context.n = nock( + `https://api.smartcar.com/v${vehicle.version}/vehicles/${VID}` + ) + .matchHeader('User-Agent', 'monkeys_on_mars') + .matchHeader('Authorization', `Bearer ${TOKEN}`) + .matchHeader('Origin', 'monkeys_on_pluto') + .get('/odometer') + .reply(200, {distance: 10}, {'sc-request-id': 'requestId'}); + + const response = await vehicle.request('get', 'odometer', undefined, { + 'User-Agent': 'monkeys_on_mars', + Origin: 'monkeys_on_pluto', + }); + + t.is(response.body.distance, 10); + t.is(response.meta.requestId, 'requestId'); +});