Skip to content

Commit

Permalink
feat(smartcar): added support for simulated mode and feature flags (#154
Browse files Browse the repository at this point in the history
)

feat(smartcar): added support for simulated mode and feature flags
  • Loading branch information
mdheri authored Jul 21, 2022
1 parent a0a1190 commit 06ff733
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 31 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const client = new smartcar.AuthClient({
clientId: '<Smartcar Client Id>', // fallback to SMARTCAR_CLIENT_ID ENV variable
clientSecret: '<Smartcar Client Secret>', // fallback to SMARTCAR_CLIENT_SECRET ENV variable
redirectUri: '<Your callback URI>', // fallback to SMARTCAR_REDIRECT_URI ENV variable
testMode: true, // launch Smartcar Connect in test mode
mode: 'test', // launch Smartcar Connect in test mode
});

// Redirect to Smartcar Connect
Expand All @@ -94,11 +94,14 @@ app.get('/callback', async function(req, res, next) {
}

// exchange auth code for access token
const tokens = await client.exchangeCode(req.query.code)
const tokens = await client.exchangeCode(req.query.code);
// get the user's vehicles
const vehicles = await smartcar.getVehicles(tokens.accessToken);
// instantiate first vehicle in vehicle list
const vehicle = new smartcar.Vehicle(vehicles.vehicles[0], tokens.accessToken);
const vehicle = new smartcar.Vehicle(
vehicles.vehicles[0],
tokens.accessToken
);
// get identifying information about a vehicle
const attributes = await vehicle.attributes();
console.log(attributes);
Expand Down Expand Up @@ -135,20 +138,22 @@ To test:
npm run test
```

Note: In order to run tests locally the following environment variables would have to be set :
Note: In order to run tests locally the following environment variables would have to be set :

- `E2E_SMARTCAR_CLIENT_ID` - Client ID to be used.
- `E2E_SMARTCAR_CLIENT_SECRET` - Client secret to be used.
- `E2E_SMARTCAR_REDIRECT_URI` - Redirect URI for the auth flow.
- `E2E_SMARTCAR_AMT` - AMT from dashboard for webhooks tests.
- `E2E_SMARTCAR_WEBHOOK_ID` - Webhook ID use in the webhook tests success case.

Your application needs to have https://example.com/auth set as a valid redirect URI

[ci-url]: https://travis-ci.com/smartcar/node-sdk
[ci-image]: https://travis-ci.com/smartcar/node-sdk.svg?token=jMbuVtXPGeJMPdsn7RQ5&branch=master
[npm-url]: https://badge.fury.io/js/smartcar
[npm-image]: https://badge.fury.io/js/smartcar.svg

## Supported Node.js Versions
## Supported Node.js Versions

Smartcar aims to support the SDK on all Node.js versions that have a status of "Maintenance" or "Active LTS" as defined in the [Node.js Release schedule](https://github.com/nodejs/Release#release-schedule).

In accordance with the Semantic Versioning specification, the addition of support for new Node.js versions would result in a MINOR version bump and the removal of support for Node.js versions would result in a MAJOR version bump.
12 changes: 9 additions & 3 deletions doc/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ the following fields :</p>
* [.getApiVersion()](#module_smartcar.getApiVersion) ⇒ <code>String</code>
* [.getUser(accessToken)](#module_smartcar.getUser)[<code>User</code>](#module_smartcar..User)
* [.getVehicles(accessToken, [paging])](#module_smartcar.getVehicles)[<code>VehicleIds</code>](#module_smartcar..VehicleIds)
* [.getCompatibility(vin, scope, [country])](#module_smartcar.getCompatibility)[<code>Compatibility</code>](#module_smartcar..Compatibility)
* [.getCompatibility(vin, scope, [country], [options])](#module_smartcar.getCompatibility)[<code>Compatibility</code>](#module_smartcar..Compatibility)
* [.hashChallenge(amt, challenge)](#module_smartcar.hashChallenge) ⇒ <code>String</code>
* [.verifyPayload(amt, signature, body)](#module_smartcar.verifyPayload) ⇒ <code>Boolean</code>
* _inner_
Expand Down Expand Up @@ -173,7 +173,7 @@ Return list of the user's vehicles ids.

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

### smartcar.getCompatibility(vin, scope, [country]) ⇒ [<code>Compatibility</code>](#module_smartcar..Compatibility)
### smartcar.getCompatibility(vin, scope, [country], [options]) ⇒ [<code>Compatibility</code>](#module_smartcar..Compatibility)
Determine whether a vehicle is compatible with Smartcar.

A compatible vehicle is a vehicle that:
Expand All @@ -196,6 +196,10 @@ _To use this function, please contact us!_
| vin | <code>String</code> | | the VIN of the vehicle |
| scope | <code>Array.&lt;String&gt;</code> | | list of permissions to check compatibility for |
| [country] | <code>String</code> | <code>&#x27;US&#x27;</code> | an optional country code according to [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). |
| [options] | <code>Object</code> | | |
| [options.testMode] | <code>Boolean</code> | | Deprecated, please use `mode` instead. Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/). |
| [options.mode] | <code>String</code> | | Determine what mode Smartcar Connect should be launched in. Should be one of test, live or simulated. |
| [options.testModeCompatibilityLevel] | <code>String</code> | | This string determines which permissions the simulated vehicle is capable of. Possible Values can be found at this link: (https://smartcar.com/docs/integration-guide/test-your-integration/test-requests/#test-successful-api-requests-with-specific-vins) |

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

Expand Down Expand Up @@ -338,7 +342,8 @@ Create a Smartcar OAuth client for your application.
| options.clientId | <code>String</code> | | Application client id obtained from [Smartcar Developer Portal](https://developer.smartcar.com). If you do not have access to the dashboard, please [request access](https://smartcar.com/subscribe). |
| options.clientSecret | <code>String</code> | | The application's client secret. |
| options.redirectUri | <code>String</code> | | Redirect URI registered in the [application settings](https://developer.smartcar.com/apps). The given URL must exactly match one of the registered URLs. |
| [options.testMode] | <code>Boolean</code> | <code>false</code> | Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/). |
| [options.testMode] | <code>Boolean</code> | <code>false</code> | Deprecated, please use `mode` instead. Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/). |
| [options.mode] | <code>String</code> | <code>&#x27;live&#x27;</code> | Determine what mode Smartcar Connect should be launched in. Should be one of test, live or simulated. |

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

Expand Down Expand Up @@ -577,6 +582,7 @@ Initializes a new Vehicle to use for making requests to the Smartcar API.
| [options] | <code>Object</code> | | |
| [options.unitSystem] | <code>String</code> | <code>metric</code> | The unit system to use for vehicle data must be either `metric` or `imperial`. |
| [options.version] | <code>Object</code> | | API version to use |
| [options.flags] | <code>Object</code> | | Object of flags where key is the name of the flag and value is string or boolean value. |

<a name="Vehicle+permissions"></a>

Expand Down
23 changes: 20 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const crypto = require('crypto');

const {emitWarning} = require('process');
const SmartcarService = require('./lib/smartcar-service');
const util = require('./lib/util');
const config = require('./lib/config.json');
Expand All @@ -27,11 +28,19 @@ const buildQueryParams = function(vin, scope, country, options) {
if (options.flags) {
parameters.flags = util.getFlagsString(options.flags);
}

if (options.hasOwnProperty('testMode')) {
parameters.mode = options.testMode ? 'test' : 'live';
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\'',
);
}
}

if (options.testModeCompatibilityLevel) {
// eslint-disable-next-line camelcase
parameters.test_mode_compatibility_level =
Expand Down Expand Up @@ -191,6 +200,14 @@ smartcar.getVehicles = async function(accessToken, paging = {}) {
* @param {String} vin - the VIN of the vehicle
* @param {String[]} scope - list of permissions to check compatibility for
* @param {String} [country='US'] - an optional country code according to [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
* @param {Object} [options]
* @param {Boolean} [options.testMode] - Deprecated, please use `mode` instead.
* Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
* @param {String} [options.mode] - Determine what mode Smartcar Connect should be
* launched in. Should be one of test, live or simulated.
* @param {String} [options.testModeCompatibilityLevel] - This string determines which permissions
* the simulated vehicle is capable of. Possible Values can be found at this link:
* (https://smartcar.com/docs/integration-guide/test-your-integration/test-requests/#test-successful-api-requests-with-specific-vins)
* @return {module:smartcar~Compatibility}
* @throws {SmartcarError} - an instance of SmartcarError.
* See the [errors section](https://github.com/smartcar/node-sdk/tree/master/doc#errors)
Expand Down
42 changes: 30 additions & 12 deletions lib/auth-client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const {emitWarning} = require('process');
const qs = require('querystring');
const SmartcarService = require('./smartcar-service');
const util = require('./util');
Expand Down Expand Up @@ -40,18 +41,33 @@ const config = require('./config.json');
* @param {String} options.redirectUri - Redirect URI registered in the
* [application settings](https://developer.smartcar.com/apps). The given URL
* must exactly match one of the registered URLs.
* @param {Boolean} [options.testMode=false] - Launch Smartcar Connect in
* [test mode](https://smartcar.com/docs/guides/testing/).
* @param {Boolean} [options.testMode=false] - Deprecated, please use `mode` instead.
* Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
* @param {String} [options.mode='live'] - Determine what mode Smartcar Connect should be
* launched in. Should be one of test, live or simulated.
*/
function AuthClient(options = {}) {
this.clientId = options.clientId
|| util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
this.clientSecret = options.clientSecret
|| util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
this.redirectUri = options.redirectUri
|| util.getOrThrowConfig('SMARTCAR_REDIRECT_URI');

this.testMode = options.testMode === true;
this.clientId =
options.clientId || util.getOrThrowConfig('SMARTCAR_CLIENT_ID');
this.clientSecret =
options.clientSecret || util.getOrThrowConfig('SMARTCAR_CLIENT_SECRET');
this.redirectUri =
options.redirectUri || util.getOrThrowConfig('SMARTCAR_REDIRECT_URI');

this.mode = 'live';
if (options.hasOwnProperty('testMode')) {
emitWarning(// eslint-disable-next-line max-len
'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
);
this.mode = options.testMode === true ? 'test' : 'live';
} else if (options.hasOwnProperty('mode')) {
this.mode = options.mode;
}
if (!['test', 'live', 'simulated'].includes(this.mode)) {
throw new Error(// eslint-disable-next-line max-len
'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'',
);
}
this.authUrl = util.getConfig('SMARTCAR_AUTH_ORIGIN') || config.auth;

this.service = new SmartcarService({
Expand Down Expand Up @@ -141,11 +157,13 @@ AuthClient.prototype.getAuthUrl = function(scope, options = {}) {
parameters.flags = util.getFlagsString(options.flags);
}

parameters.mode = this.testMode ? 'test' : 'live';
parameters.mode = this.mode;

const query = qs.stringify(parameters);

return `${config.connect}/oauth/authorize?${query}`;
return `${
util.getConfig('SMARTCAR_API_ORIGIN') || config.connect
}/oauth/authorize?${query}`;
};

/**
Expand Down
9 changes: 9 additions & 0 deletions lib/vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,20 @@ const METHODS_MAP = {
* @param {String} [options.unitSystem=metric] - The unit system to use for vehicle data
* must be either `metric` or `imperial`.
* @param {Object} [options.version] - API version to use
* @param {Object} [options.flags] - Object of flags where key is the name of the flag
* and value is string or boolean value.
*/
function Vehicle(id, token, options = {}) {
this.id = id;
this.token = token;
this.unitSystem = options.unitSystem || 'metric';
this.version = options.version || config.version;
this.query = {};
if (options.flags) {
this.query.flags = util.getFlagsString(options.flags);
}


this.service = new SmartcarService({
baseUrl: util.getUrl(this.id, '', this.version),
auth: {
Expand All @@ -59,6 +67,7 @@ function Vehicle(id, token, options = {}) {
headers: {
'sc-unit-system': this.unitSystem,
},
qs: this.query,
});
}

Expand Down
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=30s test/end-to-end",
"test:e2e": "ava --timeout=60s test/end-to-end",
"test:integration": "cross-env NOCK_OFF=true npm test",
"posttest": "npm run lint -s",
"lint": "eslint . --cache",
Expand Down
2 changes: 1 addition & 1 deletion test/end-to-end/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ helpers.getAuthClientParams = () => ({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: 'https://example.com/auth',
testMode: true,
mode: 'test',
});

const getCodeFromUri = function(uri) {
Expand Down
47 changes: 45 additions & 2 deletions test/unit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,54 @@ test('getCompatibility - with flags, testModeCompatibilityLevel and override ver
t.true(n.isDone());
});

test('getCompatibility - with testMode true', async function(t) {
test('getCompatibility - mode invalid input errors', async function(t) {
const vin = 'fake_vin';
const scope = ['read_location', 'read_odometer'];


const err = await t.throwsAsync(smartcar.getCompatibility(vin, scope, 'US', {
clientId: 'clientId',
clientSecret: 'clientSecret',
version: '6.6',
mode: 'pizzapasta',
}));
t.is(
err.message,
// eslint-disable-next-line max-len
'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'',
);
});

test('getCompatibility - with mode simulated', async function(t) {
const vin = 'fake_vin';
const scope = ['read_location', 'read_odometer'];
const path = '/compatibility?vin=fake_vin&'
+ 'scope=read_location%20read_odometer&country=US&'
+ 'mode=simulated';
const n = nock('https://api.smartcar.com/v6.6/')
.get(path)
.matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0')
.reply(200, {
pizza: 'pasta',
});

const response = await smartcar.getCompatibility(vin, scope, 'US', {
clientId: 'clientId',
clientSecret: 'clientSecret',
version: '6.6',
mode: 'simulated',
});

t.is(response.pizza, 'pasta');
t.true(n.isDone());
});

test('getCompatibility - with test_mode true [deprecated]', async function(t) {
const vin = 'fake_vin';
const scope = ['read_location', 'read_odometer'];
const path = '/compatibility?vin=fake_vin&'
+ 'scope=read_location%20read_odometer&country=US&mode=test';
+ 'scope=read_location%20read_odometer&country=US&'
+ 'mode=test';
const n = nock('https://api.smartcar.com/v6.6/')
.get(path)
.matchHeader('Authorization', 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0')
Expand Down
36 changes: 34 additions & 2 deletions test/unit/lib/auth-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test('constructor', function(t) {
t.is(client.clientId, CLIENT_ID);
t.is(client.clientSecret, CLIENT_SECRET);
t.is(client.redirectUri, 'https://insurance.co/callback');
t.is(client.testMode, false);
t.is(client.mode, 'live');
t.true('service' in client);
});

Expand Down Expand Up @@ -51,6 +51,38 @@ test('constructor - client id, secret and redirect url errors', function(t) {
t.is(error.message, message);
});

test('constructor - test_mode true [deprecated]', function(t) {
const client = new AuthClient({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: 'https://insurance.co/callback',
testMode: true,
});

t.is(client.clientId, CLIENT_ID);
t.is(client.clientSecret, CLIENT_SECRET);
t.is(client.redirectUri, 'https://insurance.co/callback');
t.is(client.mode, 'test');
t.true('service' in client);
});

test('constructor - mode invalid input errors', function(t) {
const err = t.throws(
() =>
new AuthClient({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: 'https://insurance.co/callback',
mode: 'pizzapasta',
}),
);
t.is(
err.message,
// eslint-disable-next-line max-len
'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\'',
);
});

test('getAuthUrl - simple', function(t) {
const client = new AuthClient({
clientId: CLIENT_ID,
Expand All @@ -74,7 +106,7 @@ test('getAuthUrl - with optional arguments', function(t) {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: 'https://insurance.co/callback',
testMode: true,
mode: 'test',
});

const actual = client.getAuthUrl(
Expand Down
15 changes: 15 additions & 0 deletions test/unit/lib/vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ test('constructor - non default unit and version', async function(t) {
t.is(res.pizza, 'pasta');
});

test('constructor - with optional flags', async function(t) {
const vehicle = new Vehicle(VID, TOKEN, {
flags: {country: 'DE', flag: 'suboption'},
});
t.context.n = nocks
.base(vehicle.version)
.matchHeader('sc-unit-system', 'metric')
.get('/default?flags=country%3ADE%20flag%3Asuboption')
.reply(200, '{"pizza": "pasta"}');

t.deepEqual(vehicle.query, {flags: 'country:DE flag:suboption'});
const res = await vehicle.service.request('get', 'default');
t.is(res.pizza, 'pasta');
});

test('vehicle webhook subscribe', async(t) => {
const responseBody = {webhookId: 'webhookID', vehicleId: 'vehicleId'};
t.context.n = nocks
Expand Down

0 comments on commit 06ff733

Please sign in to comment.