From cb3c2f27beccf1f6b99c2151c716a6d700bbd142 Mon Sep 17 00:00:00 2001 From: Nathan Bryant <60950167+nbry@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:12:21 -0700 Subject: [PATCH] feat: vehicle management functions (#162) --- doc/readme.md | 54 +++++++++++ index.js | 149 +++++++++++++++++++++++++------ lib/config.json | 1 + lib/util.js | 14 +++ package.json | 2 +- test/end-to-end/helpers/index.js | 10 +++ test/end-to-end/index.js | 58 +++++++++++- test/end-to-end/vehicle.js | 2 +- test/unit/index.js | 13 +++ test/unit/lib/util.js | 7 ++ 10 files changed, 282 insertions(+), 28 deletions(-) diff --git a/doc/readme.md b/doc/readme.md index 722a8fc..0c988bc 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -102,6 +102,8 @@ the following fields :
* [~User](#module_smartcar..User) :Object
* [~VehicleIds](#module_smartcar..VehicleIds) : Object
* [~Compatibility](#module_smartcar..Compatibility) : Object
+ * [~GetConnections](#module_smartcar..GetConnections) ⇒ GetConnections
+ * [~DeleteConnections](#module_smartcar..DeleteConnections) ⇒ DeleteConnections
@@ -321,6 +323,58 @@ Verify webhook payload with AMT and signature.
}
}
```
+
+
+### smartcar~GetConnections ⇒ GetConnections
+Returns a paged list of all the vehicles that are connected to the application associated
+with the management API token used sorted in descending order by connection date.
+
+**Kind**: inner typedef of [smartcar
](#module_smartcar)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| amt | String
| Application Management Token |
+| filter | object
| |
+| filter.userId | String
| |
+| filter.vehicleId | String
| |
+| paging | object
| |
+| paging.limit | number
| |
+| paging.cursor | String
| |
+
+**Properties**
+
+| Name | Type |
+| --- | --- |
+| vehicleId | String
|
+| userId | String
|
+| connectedAt | String
|
+| connections | Array.<Connection>
|
+| [paging] | Object
|
+| [paging.cursor] | string
|
+
+
+
+### smartcar~DeleteConnections ⇒ DeleteConnections
+Deletes all the connections by vehicle or user ID and returns a
+list of all connections that were deleted.
+
+**Kind**: inner typedef of [smartcar
](#module_smartcar)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| amt | String
| Application Management Token |
+| filter | object
| |
+| filter.userId | String
| |
+| filter.vehicleId | String
| |
+
+**Properties**
+
+| Name | Type |
+| --- | --- |
+| vehicleId | String
|
+| userId | String
|
+| connections | Array.<Connection>
|
+
## AuthClient
diff --git a/index.js b/index.js
index eecb0d9..f9e9a07 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,8 @@
+/* eslint-disable max-len */
+/* eslint-disable camelcase */
'use strict';
+const _ = require('lodash');
const crypto = require('crypto');
const {emitWarning} = require('process');
@@ -29,15 +32,16 @@ const buildQueryParams = function(vin, scope, country, options) {
parameters.flags = util.getFlagsString(options.flags);
}
if (options.hasOwnProperty('testMode')) {
- emitWarning(// eslint-disable-next-line max-len
+ emitWarning(
+ // eslint-disable-next-line max-len
'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
);
parameters.mode = options.testMode === true ? 'test' : 'live';
} else if (options.hasOwnProperty('mode')) {
parameters.mode = options.mode;
if (!['test', 'live', 'simulated'].includes(parameters.mode)) {
- throw new Error(// eslint-disable-next-line max-len
- 'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'',
+ throw new Error( // eslint-disable-next-line max-len
+ "The \"mode\" parameter MUST be one of the following: 'test', 'live', 'simulated'",
);
}
}
@@ -65,7 +69,7 @@ smartcar.setApiVersion = function(version) {
* @method
* @return {String} version
*/
-smartcar.getApiVersion = () => (config.version);
+smartcar.getApiVersion = () => config.version;
/**
* @type {Object}
@@ -96,10 +100,7 @@ smartcar.getUser = async function(accessToken) {
const response = await new SmartcarService({
baseUrl: util.getConfig('SMARTCAR_API_ORIGIN') || config.api,
auth: {bearer: accessToken},
- }).request(
- 'get',
- `/v${config.version}/user`,
- );
+ }).request('get', `/v${config.version}/user`);
return response;
};
@@ -213,17 +214,12 @@ smartcar.getVehicles = async function(accessToken, paging = {}) {
* See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
* for all possible errors.
*/
-smartcar.getCompatibility = async function(
- vin,
- scope,
- country,
- options = {},
-) {
+smartcar.getCompatibility = async function(vin, scope, country, options = {}) {
country = country || 'US';
- const clientId = options.clientId
- || util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
- const clientSecret = options.clientSecret
- || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
+ const clientId =
+ options.clientId || util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
+ const clientSecret =
+ options.clientSecret || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
const response = await new SmartcarService({
baseUrl: util.getConfig('SMARTCAR_API_ORIGIN') || config.api,
@@ -232,10 +228,7 @@ smartcar.getCompatibility = async function(
pass: clientSecret,
},
qs: buildQueryParams(vin, scope, country, options),
- }).request(
- 'get',
- `v${options.version || config.version}/compatibility`,
- );
+ }).request('get', `v${options.version || config.version}/compatibility`);
return response;
};
@@ -261,8 +254,114 @@ smartcar.hashChallenge = function(amt, challenge) {
* @param {object} body - webhook response body
* @return {Boolean} true if signature matches the hex digest of amt and body
*/
-smartcar.verifyPayload = (amt, signature, body) => (
- smartcar.hashChallenge(amt, JSON.stringify(body)) === signature
-);
+smartcar.verifyPayload = (amt, signature, body) =>
+ smartcar.hashChallenge(amt, JSON.stringify(body)) === signature;
+
+/**
+ * Returns a paged list of all the vehicles that are connected to the application associated
+ * with the management API token used sorted in descending order by connection date.
+ *
+ * @type {Object}
+ * @typedef Connection
+ * @property {String} vehicleId
+ * @property {String} userId
+ * @property {String} connectedAt
+ *
+ * @type {Object}
+ * @typedef GetConnections
+ * @property {Connection[]} connections
+ * @property {Object} [paging]
+ * @property {string} [paging.cursor]
+ *
+ * @param {String} amt - Application Management Token
+ * @param {object} filter
+ * @param {String} filter.userId
+ * @param {String} filter.vehicleId
+ * @param {object} paging
+ * @param {number} paging.limit
+ * @param {String} paging.cursor
+ * @returns {GetConnections}
+ */
+smartcar.getConnections = async function(amt, filter = {}, paging = {}) {
+ const {userId, vehicleId} = _.pick(filter, ['userId', 'vehicleId']);
+ const {limit, cursor} = _.pick(paging, ['limit', 'cursor']);
+
+ const qs = {};
+ if (userId) {
+ qs.user_id = userId;
+ }
+ if (vehicleId) {
+ qs.vehicle_id = vehicleId;
+ }
+ if (limit) {
+ qs.limit = limit;
+ }
+ // istanbul ignore next
+ if (cursor) {
+ qs.cursor = cursor;
+ }
+
+ const response = await new SmartcarService({
+ // eslint-disable-next-line max-len
+ baseUrl:
+ `${util.getConfig('SMARTCAR_MANAGEMENT_API_ORIGIN') || config.management}/v${config.version}`,
+ auth: {
+ user: 'default',
+ pass: amt,
+ },
+ qs,
+ }).request('get', '/management/connections');
+
+ return response;
+};
+
+/**
+ * Deletes all the connections by vehicle or user ID and returns a
+ * list of all connections that were deleted.
+ *
+ * @type {Object}
+ * @typedef Connection
+ * @property {String} vehicleId
+ * @property {String} userId
+ *
+ * @type {Object}
+ * @typedef DeleteConnections
+ * @property {Connection[]} connections
+ *
+ * @param {String} amt - Application Management Token
+ * @param {object} filter
+ * @param {String} filter.userId
+ * @param {String} filter.vehicleId
+ * @returns {DeleteConnections}
+ */
+smartcar.deleteConnections = async function(amt, filter) {
+ const {userId, vehicleId} = _.pick(filter, ['userId', 'vehicleId']);
+ if (userId && vehicleId) {
+ // eslint-disable-next-line max-len
+ throw new Error(
+ 'Filter can contain EITHER user_id OR vehicle_id, not both',
+ );
+ }
+
+ const qs = {};
+ if (userId) {
+ qs.user_id = userId;
+ }
+ if (vehicleId) {
+ qs.vehicle_id = vehicleId;
+ }
+
+ const response = await new SmartcarService({
+ baseUrl:
+ `${util.getConfig('SMARTCAR_MANAGEMENT_API_ORIGIN') || config.management}/v${config.version}`,
+ auth: {
+ user: 'default',
+ pass: amt,
+ },
+ qs,
+ }).request('delete', '/management/connections');
+
+ return response;
+};
module.exports = smartcar;
diff --git a/lib/config.json b/lib/config.json
index d21d4b3..1deb2b7 100644
--- a/lib/config.json
+++ b/lib/config.json
@@ -2,6 +2,7 @@
"auth": "https://auth.smartcar.com",
"api": "https://api.smartcar.com",
"connect": "https://connect.smartcar.com",
+ "management": "https://management.smartcar.com",
"timeout": 310000,
"version": "2.0"
}
diff --git a/lib/util.js b/lib/util.js
index e9accb3..4afa532 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -112,4 +112,18 @@ util.handleError = function(caught) {
}
};
+/**
+ *
+ * Generate the token for vehicle management APIs using the amt.
+ *
+ * @method
+ * @param {String} amt - Application Management Token
+ * @param {String} username
+ * @return {String} managementToken
+ */
+util.getManagementToken = function(amt, username = 'default') {
+ const credentials = `${username}:${amt}`;
+ return Buffer.from(credentials).toString('base64');
+};
+
module.exports = util;
diff --git a/package.json b/package.json
index d615114..2b71c7f 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"scripts": {
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "ava test/unit",
- "test:e2e": "ava --timeout=60s test/end-to-end",
+ "test:e2e": "ava --timeout=120s test/end-to-end",
"test:integration": "cross-env NOCK_OFF=true npm test",
"posttest": "npm run lint -s",
"lint": "eslint . --cache",
diff --git a/test/end-to-end/helpers/index.js b/test/end-to-end/helpers/index.js
index ca8e9a8..e55fd07 100644
--- a/test/end-to-end/helpers/index.js
+++ b/test/end-to-end/helpers/index.js
@@ -87,6 +87,16 @@ helpers.runAuthFlow = async function(
);
await continueButton.click();
+ // Filter
+ if (brand === 'KIA') {
+ const brandInput = await driver.wait(
+ until.elementLocated(
+ By.css('input[id=brand-search]'),
+ ),
+ );
+ await brandInput.sendKeys(brand);
+ }
+
// Brand Selector
const brandButton = await driver.wait(
until.elementLocated(
diff --git a/test/end-to-end/index.js b/test/end-to-end/index.js
index 2b1f50a..5480b82 100644
--- a/test/end-to-end/index.js
+++ b/test/end-to-end/index.js
@@ -4,14 +4,18 @@ const _ = require('lodash');
const test = require('ava');
const smartcar = require('../../');
+const util = require('../../lib/util');
const {getAuthClientParams, runAuthFlow, DEFAULT_SCOPES} = require('./helpers');
test.before(async(t) => {
const client = new smartcar.AuthClient(getAuthClientParams());
const code = await runAuthFlow(client.getAuthUrl(DEFAULT_SCOPES));
const {accessToken} = await client.exchangeCode(code);
+ const {vehicles} = await smartcar.getVehicles(accessToken);
+ const {id: userId} = await smartcar.getUser(accessToken);
+ t.context.userId = userId;
t.context.accessToken = accessToken;
- // t.context.accessToken = 'f14a1599-b5d9-4fe7-bff0-c890f837b7b4';
+ t.context.connectedVehicles = vehicles;
});
test('getVehicles', async(t) => {
@@ -81,3 +85,55 @@ test('getCompatibility', async(t) => {
t.truthy(_.every(audiComp.capabilities, ['capable', false]));
t.truthy(_.every(teslaComp.capabilities, ['capable', true]));
});
+
+test.serial('getConnections', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const res = await smartcar.getConnections(amt);
+ t.truthy(res.connections[0].userId);
+ t.truthy(res.connections[0].vehicleId);
+ t.truthy(res.connections[0].connectedAt);
+});
+
+test.serial('getConnections - by vehicleId', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const testVehicleId = t.context.connectedVehicles[0];
+ const res = await smartcar.getConnections(amt, {vehicleId: testVehicleId});
+ t.is(res.connections.length, 1);
+ t.is(res.connections[0].vehicleId, testVehicleId);
+});
+
+test.serial('getConnections - by userId', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const res = await smartcar.getConnections(amt, {userId: t.context.userId});
+ t.is(res.connections.length, t.context.connectedVehicles.length);
+ for (const connection of res.connections) {
+ t.is(connection.userId, t.context.userId);
+ }
+});
+
+test.serial('getConnections - by userId - limit 1', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const res = await smartcar.getConnections(amt,
+ {userId: t.context.userId},
+ {limit: 1},
+ );
+ t.is(res.connections.length, t.context.connectedVehicles.length);
+});
+
+test.serial('deleteConnections - by vehicleId', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const testVehicleId = t.context.connectedVehicles[0];
+ const res = await smartcar.deleteConnections(amt, {vehicleId: testVehicleId});
+ t.is(res.connections.length, 1);
+ t.is(res.connections[0].vehicleId, testVehicleId);
+});
+
+test.serial('deleteConnections - by userId', async(t) => {
+ const amt = util.getOrThrowConfig('E2E_SMARTCAR_AMT');
+ const res = await smartcar.deleteConnections(amt, {userId: t.context.userId});
+ // to account for serial test above
+ t.is(res.connections.length, t.context.connectedVehicles.length - 1);
+ for (const connection of res.connections) {
+ t.is(connection.userId, t.context.userId);
+ }
+});
diff --git a/test/end-to-end/vehicle.js b/test/end-to-end/vehicle.js
index 523f18b..925cc2f 100644
--- a/test/end-to-end/vehicle.js
+++ b/test/end-to-end/vehicle.js
@@ -474,7 +474,7 @@ test('vehicle request - set charge limit', async(t) => {
});
test.after.always('vehicle disconnect', async(t) => {
- const response = await t.context.volt.disconnect();
+ const response = await t.context.kia.disconnect();
t.deepEqual(
_.xor(_.keys(response), [
'status',
diff --git a/test/unit/index.js b/test/unit/index.js
index 84307b4..db0ad19 100644
--- a/test/unit/index.js
+++ b/test/unit/index.js
@@ -214,3 +214,16 @@ test('getCompatibility - with test_mode false [deprecated]', async function(t) {
t.is(response.pizza, 'pasta');
t.true(n.isDone());
});
+
+test('deleteConnections - both vehicleId and userId passed', async function(t) {
+ const error = await t.throwsAsync(
+ smartcar.deleteConnections('fake-amt', {
+ vehicleId: 'vehicle id',
+ userId: 'user id',
+ }),
+ );
+ t.is(
+ error.message,
+ 'Filter can contain EITHER user_id OR vehicle_id, not both',
+ );
+});
diff --git a/test/unit/lib/util.js b/test/unit/lib/util.js
index 2678f1c..d83b8fc 100644
--- a/test/unit/lib/util.js
+++ b/test/unit/lib/util.js
@@ -268,3 +268,10 @@ test('handleError - SmartcarError V2 with all attrbutes', function(t) {
t.is(boxed.message, 'type:code - description');
t.is(boxed.detail[0], 'pizza');
});
+
+test('getManagementToken', function(t) {
+ const res = util.getManagementToken('amt');
+ t.is(res, 'ZGVmYXVsdDphbXQ=');
+ const res2 = util.getManagementToken('amt', 'default');
+ t.is(res2, 'ZGVmYXVsdDphbXQ=');
+});