Skip to content

Commit

Permalink
feat: vehicle management functions (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbry authored Aug 17, 2023
1 parent 0b18b9c commit cb3c2f2
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 28 deletions.
54 changes: 54 additions & 0 deletions doc/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ the following fields :</p>
* [~User](#module_smartcar..User) : <code>Object</code>
* [~VehicleIds](#module_smartcar..VehicleIds) : <code>Object</code>
* [~Compatibility](#module_smartcar..Compatibility) : <code>Object</code>
* [~GetConnections](#module_smartcar..GetConnections) ⇒ <code>GetConnections</code>
* [~DeleteConnections](#module_smartcar..DeleteConnections) ⇒ <code>DeleteConnections</code>

<a name="module_smartcar.SmartcarError"></a>

Expand Down Expand Up @@ -321,6 +323,58 @@ Verify webhook payload with AMT and signature.
}
}
```
<a name="module_smartcar..GetConnections"></a>

### smartcar~GetConnections ⇒ <code>GetConnections</code>
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 [<code>smartcar</code>](#module_smartcar)

| Param | Type | Description |
| --- | --- | --- |
| amt | <code>String</code> | Application Management Token |
| filter | <code>object</code> | |
| filter.userId | <code>String</code> | |
| filter.vehicleId | <code>String</code> | |
| paging | <code>object</code> | |
| paging.limit | <code>number</code> | |
| paging.cursor | <code>String</code> | |

**Properties**

| Name | Type |
| --- | --- |
| vehicleId | <code>String</code> |
| userId | <code>String</code> |
| connectedAt | <code>String</code> |
| connections | <code>Array.&lt;Connection&gt;</code> |
| [paging] | <code>Object</code> |
| [paging.cursor] | <code>string</code> |

<a name="module_smartcar..DeleteConnections"></a>

### smartcar~DeleteConnections ⇒ <code>DeleteConnections</code>
Deletes all the connections by vehicle or user ID and returns a
list of all connections that were deleted.

**Kind**: inner typedef of [<code>smartcar</code>](#module_smartcar)

| Param | Type | Description |
| --- | --- | --- |
| amt | <code>String</code> | Application Management Token |
| filter | <code>object</code> | |
| filter.userId | <code>String</code> | |
| filter.vehicleId | <code>String</code> | |

**Properties**

| Name | Type |
| --- | --- |
| vehicleId | <code>String</code> |
| userId | <code>String</code> |
| connections | <code>Array.&lt;Connection&gt;</code> |

<a name="AuthClient"></a>

## AuthClient
Expand Down
149 changes: 124 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable max-len */
/* eslint-disable camelcase */
'use strict';

const _ = require('lodash');
const crypto = require('crypto');

const {emitWarning} = require('process');
Expand Down Expand Up @@ -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'",
);
}
}
Expand Down Expand Up @@ -65,7 +69,7 @@ smartcar.setApiVersion = function(version) {
* @method
* @return {String} version
*/
smartcar.getApiVersion = () => (config.version);
smartcar.getApiVersion = () => config.version;

/**
* @type {Object}
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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,
Expand All @@ -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;
};

Expand All @@ -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;
1 change: 1 addition & 0 deletions lib/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
14 changes: 14 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions test/end-to-end/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
58 changes: 57 additions & 1 deletion test/end-to-end/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
}
});
Loading

0 comments on commit cb3c2f2

Please sign in to comment.