From 76373770b77bbc00bbb80e6b439c1eac822534d2 Mon Sep 17 00:00:00 2001 From: Michael Kelly Date: Thu, 9 Mar 2017 14:08:15 -0800 Subject: [PATCH] prep for plaid-node@2.0.0 --- .gitignore | 1 + .jscsrc | 1 - CONTRIBUTING.md | 31 ++ LICENSE | 2 +- Makefile | 2 +- PUBLISHING.md | 24 - README.md | 407 ++++++--------- circle.yml | 2 +- index.js | 383 -------------- lib/PlaidClient.js | 258 +++++++++ lib/plaid.js | 9 + lib/plaidEnvironments.js | 9 + lib/plaidRequest.js | 85 +++ package.json | 22 +- test/PlaidClientTest.js | 1009 +++++++++++++++++++++++++++++++++++ test/authenticated.js | 1074 -------------------------------------- test/public.js | 171 ------ test/testConstants.js | 27 + 18 files changed, 1600 insertions(+), 1917 deletions(-) create mode 100755 CONTRIBUTING.md mode change 100644 => 100755 LICENSE mode change 100644 => 100755 Makefile delete mode 100644 PUBLISHING.md mode change 100644 => 100755 README.md mode change 100644 => 100755 circle.yml delete mode 100644 index.js create mode 100755 lib/PlaidClient.js create mode 100755 lib/plaid.js create mode 100755 lib/plaidEnvironments.js create mode 100755 lib/plaidRequest.js mode change 100644 => 100755 package.json create mode 100755 test/PlaidClientTest.js delete mode 100644 test/authenticated.js delete mode 100644 test/public.js create mode 100755 test/testConstants.js diff --git a/.gitignore b/.gitignore index 3f1b4976..8cfadd54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /coverage/ /node_modules/ /npm-debug.log +.env diff --git a/.jscsrc b/.jscsrc index 017315c0..593ee1e5 100644 --- a/.jscsrc +++ b/.jscsrc @@ -29,7 +29,6 @@ "requireSpaceBeforeKeywords": ["else", "catch", "finally"], "requireSpacesInConditionalExpression": true, "requireSpacesInFunction": {"beforeOpeningCurlyBrace": true}, - "requireTrailingComma": {"ignoreSingleLine": true}, "validateLineBreaks": "LF", "validateQuoteMarks": {"escape": true, "mark": "'"} } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 00000000..266076fc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Instructions for contributing to [plaid-node][1]. A node.js client library for the [Plaid API][2]. + +## Setup + +1. From the `plaid-node-apiv2` directory, create the `.env` file, which will be used to configure the Plaid client. + + ``` + cp .env.example .env + ``` + +2. Go to the [Plaid Dashboard](https://dashboard.plaid.com/) and copy and paste your `client_id`, `public_key`, and `secret` + into `.env` using a text editor of your choice. Your account must be enabled for sandbox access. + +3. Install the necessary dependencies. + + ``` + make setup + ``` + +## Running Tests + +```console +$ make test +``` + +Code coverage information is written to `/coverage`. + +[1]: https://github.plaid.com/plaid/plaid-node-apiv2 +[2]: https://plaid.com diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index c3b710e5..288357ae --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2016 Plaid Technologies, Inc. +Copyright (c) 2017 Plaid Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 3e404acf..ce1fd047 --- a/Makefile +++ b/Makefile @@ -24,4 +24,4 @@ setup: .PHONY: test test: - $(ISTANBUL) cover node_modules/.bin/_mocha -- --timeout 10000 + $(ISTANBUL) cover node_modules/.bin/_mocha -- --timeout 20000 diff --git a/PUBLISHING.md b/PUBLISHING.md deleted file mode 100644 index 15cbcc5d..00000000 --- a/PUBLISHING.md +++ /dev/null @@ -1,24 +0,0 @@ -# Publishing - -The module is published to the [npm public registry][1] under the package name [plaid][2]. - -The publishing process is handled by [xyz][3] and is exposed as a [Makefile][4] target. - -To publish: - -```console -$ git checkout master -$ git pull git@github.com:plaid/plaid-node.git master -$ make release-[major|minor|patch] -``` - -See the [semantic versioning][5] guidelines to determine what level of release to publish. - -**Note:** You MUST be a [package owner][6] to publish the module. - -[1]: https://www.npmjs.com -[2]: https://www.npmjs.com/package/plaid -[3]: https://github.com/davidchambers/xyz -[4]: https://github.com/plaid/plaid-node/blob/master/Makefile -[5]: http://semver.org -[6]: https://www.npmjs.com/package/plaid/access diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 2f0f3638..f9191cd5 --- a/README.md +++ b/README.md @@ -3,23 +3,20 @@ plaid-node [![Circle CI](https://circleci.com/gh/plaid/plaid-node.svg?style=svg A node.js client library for the [Plaid API][1]. -This module was recently refactored and released as version `1.0.x`. The previous -version, `0.1.1`, is still available via npm. - +This module was recently refactored and released as version `2.0.x` to support [Plaid's updated API][11]. The previous version, `1.2.0`, is still available via npm (the package is also mirrored as `plaid-legacy`). ## Table of Contents - [plaid-node](#plaid-node) * [Install](#install) * [Getting started](#getting-started) - + [Public Endpoints](#public-endpoints) - + [Authenticated Endpoints](#authenticated-endpoints) + * [Methods](#methods) * [Callbacks](#callbacks) * [Error Handling](#error-handling) * [Examples](#examples) * [Promise Support](#promise-support) * [Support](#support) - * [Tests](#tests) + * [Contributing](#contributing) * [License](#license) ## Install @@ -33,133 +30,86 @@ $ npm install plaid The module supports all Plaid API endpoints. For complete information about the API, head to the [docs][2]. -### Public Endpoints - -Public endpoints (category and institution information) require no authentication and can be accessed as follows: +All endpoints require a valid `client_id`, `secret`, and `public_key` to +access and are accessible from a valid instance of a Plaid `Client`: ```javascript -var plaid = require('plaid'); - -plaid.getCategory(category_id, plaid_env, callback); -plaid.getCategories(plaid_env, callback); - -plaid.getInstitution(institution_id, plaid_env, callback); -plaid.getInstitutions(plaid_env, callback); +const plaid = require('plaid'); -plaid.searchInstitutions({id: institutionId}, env, callback); -plaid.searchInstitutions({product: plaidProduct, query: searchString}, env, callback); - -plaid.searchAllInstitutions({id: institutionId}, env, callback); -plaid.searchAllInstitutions({product: plaidProduct, query: searchString}, env, callback); +const plaidClient = new plaid.Client(client_id, secret, public_key, plaid_env, options); ``` -`plaid_env` dictates which Plaid API environment you will access. Values are: -- `plaid.environments.tartan` - use for integration development and testing, creates users on https://tartan.plaid.com -- `plaid.environments.production` - production use, creates users on https://api.plaid.com +The `plaid_env` parameter dictates which Plaid API environment you will access. Values are: +- `plaid.environments.production` - production use, creates `Item`s on https://production.plaid.com +- `plaid.environments.development` - use for integration development and testing, creates `Item`s on https://development.plaid.com +- `plaid.environments.sandbox` - quickly build out your integration with stateful test data, creates `Item`s on https://sandbox.plaid.com -Environments are exported from the module, i.e.: +The `options` parameter is optional and allows for clients to override the default options used to make requests. e.g. ```javascript -var plaid = require('plaid'); - -console.log(plaid.environments); +const patientClient = new plaid.Client(client_id, secret, public_key, plaid_env, { + timeout: 10 * 60 * 1000, // 30 minutes + agent: 'Patient Agent' +}); ``` -### Authenticated Endpoints - -Authenticated endpoints require a valid `client_id` and `secret` to access. You can use the sandbox -client_id and secret for testing (`test_id` and `test_secret`). - -All authenticated endpoints are accessible from an instance of a Plaid `Client`: - -```javascript -var plaid = require('plaid'); - -var plaidClient = new plaid.Client(client_id, secret, plaid_env); -``` +See [here][12] for a complete list of options. The default timeout for requests is 10 minutes. -The `plaid_env` parameter dictates which Plaid API environment you will access. Values are: -- `plaid.environments.tartan` - use for integration development and testing, creates users on https://tartan.plaid.com -- `plaid.environments.production` - production use, creates users on https://api.plaid.com +## Methods Once an instance of the client has been created you use the following methods: ```javascript -var plaid = require('plaid'); +const plaid = require('plaid'); // Initialize client -var plaidClient = new plaid.Client(client_id, secret, plaid_env); - -// addAuthUser(String, Object, Object?, Function) -plaidClient.addAuthUser(institution_type, credentials, options, callback); -// stepAuthUser(String, String, Object?, Function) -plaidClient.stepAuthUser(access_token, mfaResponse, options, callback); -// getAuthUser(String, Object?, Function) -plaidClient.getAuthUser(access_token, options, callback); -// patchAuthUser(String, Object, Object? Function) -plaidClient.patchAuthUser(access_token, credentials, options, callback); -// deleteAuthUser(String, Object?, Function) -plaidClient.deleteAuthUser(access_token, options, callback); - -// addConnectUser(String, Object, Object?, Function) -plaidClient.addConnectUser(institution_type, credentials, options, callback); -// stepConnectUser(String, String, Object?, Function) -plaidClient.stepConnectUser(access_token, mfaResponse, options, callback); -// getConnectUser(String, Object?, Function) -plaidClient.getConnectUser(access_token, options, callback); -// patchConnectUser(String, Object, Object?, Function) -plaidClient.patchConnectUser(access_token, credentials, options, callback); -// deleteConnectUser(String, Object?, Function) -plaidClient.deleteConnectUser(access_token, options, callback); - -// addIncomeUser(String, Object, Object?, Function) -plaidClient.addIncomeUser(institution_type, credentials, options, callback); -// stepIncomeUser(String, String, Object, Function) -plaidClient.stepIncomeUser(access_token, mfaResponse, options, callback); -// getIncomeUser(String, Object?, Function) -plaidClient.getIncomeUser(access_token, options, callback); -// patchIncomeUser(String, Object, Object?, Function) -plaidClient.patchIncomeUser(access_token, credentials, options, callback); -// deleteIncomeUser(String, Object?, Function) -plaidClient.deleteIncomeUser(access_token, options, callback); - -// addInfoUser(String, Object, Object?, Function) -plaidClient.addInfoUser(institution_type, credentials, options, callback); -// stepInfoUser(String, String, Object, Function) -plaidClient.stepInfoUser(access_token, mfaResponse, options, callback); -// getInfoUser(String, Object?, Function) -plaidClient.getInfoUser(access_token, options, callback); -// patchInfoUser(String, Object, Object?, Function) -plaidClient.patchInfoUser(access_token, credentials, options, callback); -// deleteInfoUser(String, Object?, Function) -plaidClient.deleteInfoUser(access_token, options, callback); - -// addRiskUser(String, Object, Object?, Function) -plaidClient.addRiskUser(institution_type, credentials, options, callback); -// stepRiskUser(String, String, Object, Function) -plaidClient.stepRiskUser(access_token, mfaResponse, options, callback); -// getRiskUser(String, Object?, Function) -plaidClient.getRiskUser(access_token, options, callback); -// patchRiskUser(String, Object, Object?, Function) -plaidClient.patchRiskUser(access_token, credentials, options, callback); -// deleteRiskUser(String, Object?, Function) -plaidClient.deleteRiskUser(access_token, options, callback); - - -// getBalance(String, Function) -plaidClient.getBalance(access_token, callback); - -// upgradeUser(String, String, Object?, Function) -plaidClient.upgradeUser(access_token, upgrade_to, options, callback); - -// exchangeToken(String, Function) -plaidClient.exchangeToken(public_token, callback); - -// getLongtailInstitutions(Object, Function) -plaidClient.getLongtailInstitutions(optionsObject, callback); - -// getAllInstitutions(Object, Function) -plaidClient.getAllInstitutions(optionsObject, callback); +const plaidClient = new plaid.Client(client_id, secret, public_key, plaid_env, options); + +// createPublicToken(String, Function) +plaidClient.createPublicToken(access_token, cb); +// exchangePublicToken(String, Function) +plaidClient.exchangePublicToken(public_token, cb); +// createProcessorToken(String, String, String, Function) +plaidClient.createProcessorToken(access_token, account_id, processor, cb); + +// invalidateAccessToken(String, Function) +plaidClient.invalidateAccessToken(access_token, cb); +// updateAccessTokenVersion(String, Function) +plaidClient.updateAccessTokenVersion(legacy_access_token, cb); +// deleteItem(String, Function) +plaidClient.deleteItem(access_token, cb); +// getItem(String, Function) +plaidClient.getItem(access_token, cb); +// updateItemWebhook(String, String, Function) +plaidClient.updateItemWebhook(access_token, webhook, cb); + +// getAccounts(String, Object?, Function) +plaidClient.getAccounts(access_token, options, cb); +// getBalance(String, Object?, Function) +plaidClient.getBalance(access_token, options, cb); +// getAuth(String, Object?, Function) +plaidClient.getAuth(access_token, options, cb); +// getIdentity(String, Function) +plaidClient.getIdentity(access_token, cb); +// getCreditDetails(String, Function) +plaidClient.getCreditDetails(access_token, cb); + +// getTransactions(String, Date(YYYY-MM-DD), Date(YYYY-MM-DD), Object?, Function) +plaidClient.getTransactions(access_token, start_date, end_date, options, cb); + +// getInstitutions(Number, Number, Function); +plaidClient.getInstitutions(count, offset, cb); +// getInstitutionsById(String, Object?, Function) +plaidClient.getInstitutionById(institution_id, options, cb); +// searchInstitutionsByName(String, [String], Object?, Function) +plaidClient.searchInstitutionsByName(query, products, options, cb); + +// getCategories(Function) +plaidClient.getCategories(cb); + +// resetLogin(String, Function) +// Sandbox-only endpoint to trigger an `ITEM_LOGIN_REQUIRED` error +plaidClient.resetLogin(access_token, cb); ``` **All parameters except `options` are required. If the options parameter is omitted, the last argument to the function @@ -167,17 +117,7 @@ will be interpreted as the callback.** ## Callbacks -For a request that could potentially return a MFA response, callbacks are in the form: - -```javascript -function callback(err, mfaResponse, response) { - // err can be a network error or a Plaid API error (i.e. invalid credentials) - // mfaResponse can be any type of Plaid MFA flow -} -``` -All `add`, `step`, and `patch` related requests can return a MFA response. `upgradeUser` can also return MFA responses. - -For `delete`, `get`, `getBalance`, and `exchangeToken` requests, callbacks are in the form: +All requests have callbacks of the following form: ```javascript function callback(err, response) { @@ -195,9 +135,9 @@ between a Plaid error and a standard Error instance: ```javascript function callback(err, response) { if (err != null) { - if (err.code != null) { + if (err.error_code != null) { // This is a Plaid error - console.log(err.code + ': ' + err.message); + console.log(err.error_code + ': ' + err.error_message); } else { // This is a connection error, an Error object console.log(err.toString()); @@ -208,173 +148,136 @@ function callback(err, response) { ## Examples -Bank of America question-based MFA flow: -```javascript -var plaid = require('plaid'); - -// Initialize a client -var plaidClient = new plaid.Client('test_id', 'test_secret', plaid.environments.tartan); - -// Add a BofA auth user going through question-based MFA -plaidClient.addAuthUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', -}, function(err, mfaResponse, response) { - if (err != null) { - // Bad request - invalid credentials, account locked, etc. - console.error(err); - } else if (mfaResponse != null) { - plaidClient.stepAuthUser(mfaResponse.access_token, 'tomato', {}, - function(err, mfaRes, response) { - console.log(response.accounts); - }); - } else { - // No MFA required - response body has accounts - console.log(response.accounts); - } -}); -``` +Exchange a `public_token` from [Plaid Link][6] for a Plaid `access_token` and then +retrieve account data: -Chase device-based MFA flow, including using the `list:true` option to allow the user select -the device to send their security code to: ```javascript -// Add a Chase user using the list:true option -plaidClient.addConnectUser('chase', { - username: 'plaid_test', - password: 'plaid_good', -}, { - list: true, -}, function(err, mfaRes, response) { - // mfaRes.mfa is a list of send_methods - plaidClient.stepConnectUser(mfaRes.access_token, null, { - send_method: mfaRes.mfa[0], - }, function(err, mfaRes, response) { - // code was sent to the device we specified - plaidClient.stepConnectUser(mfaRes.access_token, '1234', function(err, mfaRes, res) { - // We now have accounts and transactions - console.log('# transactions: ' + res.transactions.length); - console.log('access token: ' + res.access_token); - }); +plaidClient.exchangePublicToken(public_token, function(err, res) { + const access_token = res.access_token; + + plaidClient.getAccounts(access_token, function(err, res) { + console.log(res.accounts); }); }); ``` -Retrieve transactions for a connect user for the last thirty days: +Retrieve transactions for a transactions user for the last thirty days: + ```javascript -plaidClient.getConnectUser(access_token, { - gte: '30 days ago', -}, function(err, response) { - console.log('You have ' + response.transactions.length + - ' transactions from the last thirty days.'); -}); -``` -Associate a new webhook with a connect user (webhook PATCH): +const now = moment(); +const today = now.format('YYYY-MM-DD'); +const thirtyDaysAgo = now.subtract(30, 'days').format('YYYY-MM-DD'); -```javascript -// Credentials are not required in this case -plaidClient.patchConnectUser(access_token, {}, { - webhook: 'http://requestb.in', -}, function(err, mfaResponse, response) { - // The webhook URI should receive a code 4 "webhook acknowledged" webhook +plaidClient.getTransactions(access_token, thirtyDaysAgo, today, (err, res) => { + console.log(`You have ${res.transactions.length} transactions from the last thirty days.`); }); ``` -Exchange a `public_token` from [Plaid Link][8] for a Plaid access token and then -retrieve account data: +Get accounts for a particular `Item`: ```javascript -plaidClient.exchangeToken(public_token, function(err, res) { - var access_token = res.access_token; - - plaidClient.getAuthUser(access_token, function(err, res) { - console.log(res.accounts); - }); +plaidClient.getAccounts(access_token, { + account_ids: ['123456790'] +}, (err, res) => { + console.log(res.accounts); }); -``` - -Exchange a `public_token` and `account_id` from the [Plaid + Stripe][9] ACH -integration for a Plaid access token and a [Stripe bank account token][10]: +// The library also juggles arguments, when options is omitted -```javascript -plaidClient.exchangeToken(public_token, account_id, function(err, res) { - var access_token = res.access_token; - var stripe_token = res.stripe_bank_account_token; - - // Use the access_token to make make Plaid API requests. - // Use the Stripe token to make Stripe ACH API requests. +plaidClient.getAccounts(access_token, (err, res) => { + console.log(res.accounts); }); - ``` ## Promise Support -You can "promisify" this library using a third-party Promise utility library such as [Bluebird][4]. +Every method returns a promise, so you don't have to use the callbacks. -For example, using Bluebird's [`promisifyAll`][5] functionality, we can do: +API methods that return either a success or an error can be used with the +usual `then/else` paradigm, e.g. ```javascript -var bluebird = require('bluebird'); -var plaid = require('plaid'); +plaidPromise.then(successResponse => { + // ... +}).catch(err => { + // ... +}); +``` -// Promisify the plaid module -bluebird.promisifyAll(plaid); +Plaid API methods that may return an MFA response pass an array of responses +to the next promise. The first element of the array is the `mfaResponse`, and +the second element is the `successResponse`. You can use destructuring to +improve readability. -var client = new plaid.Client('test_id', 'test_secret', plaid.environments.tartan); +For example: + +```javascript +const plaid = require('plaid'); + +const plaidClient = new plaid.Client(CLIENT_ID, SECRET, PUBLIC_KEY, plaid.environments.SANDBOX); + +plaidClient.getInstitutions(1, 0).then(successResponse => { + return successResponse.institutions; +}).catch(err => { + throw new Error(`Unreachable code block for example: ${err}`); +}).then(institutions => { + const institution = institutions[0]; + const institutionId = institution.institution_id; + const products = ['transactions', 'info', 'numbers']; + + const credentials = { + username: 'user_good', + password: 'pass_good' + }; + + return plaidClient.createItem(credentials, institutionId, products); +}).then(([mfaResponse, successResponse]) => { + if (mfaResponse) { + throw new Error(`Unreachable code block for example: ${mfaResponse}`); + } -// bluebird.promisifyAll(plaid) creates a promsified version of each method -// in the client library suffixed with "Async" -// i.e. getAuthUser's promsified counterpart is getAuthUserAsync -client.getAuthUserAsync('test_chase').then(function(authResponse) { - console.log('You have ' + authResponse.accounts.length + ' accounts!'); -}).catch(function(err) { - console.error(err); + return plaidClient.getAccounts(successResponse.access_token); +}).catch(err => { + throw new Error(`Unreachable code block for example: ${err}`); +}).then(successResponse => { + console.log(successResponse.accounts); }); ``` -Callbacks that expect more than one argument pose a challenge for Bluebird. For Plaid API functions that may -return an mfa response, use the bluebird `multiarg` option to get an array. +The following is also valid: ```javascript -var bluebird = require('bluebird'); -var plaid = require('plaid'); - -var client = new plaid.Client('test_id', 'test_secret', plaid.environments.tartan); -var addAuthUserAsync = bluebird.promisify(client.addAuthUser, {context: client, multiArgs: true}); - -addAuthUserAsync('bofa', { - username: 'plaid_test', - password: 'plaid_good' -}).then(responses => { - var mfaResponse = responses[0]; - var response = responses[1]; +const promise = plaidClient.createItem(credentials, institutionId, products); + +promise.then(array => { + const mfaResponse = array[0]; + const successResponse = array[1]; + + // do something +}).catch(err => { + console.log(err); }); ``` ## Support -Open an [issue][6]! - -## Tests +Open an [issue][4]! -```console -$ make test -``` +## Contributing -Code coverage information is written to `/coverage`. +Click [here][7]! ## License -[MIT][7] - +[MIT][5] [1]: https://plaid.com [2]: https://plaid.com/docs -[3]: https://plaid.com/docs/#response-codes -[4]: https://github.com/petkaantonov/bluebird -[5]: https://github.com/petkaantonov/bluebird/blob/master/API.md#promisepromisifyallobject-target--object-options---object -[6]: https://github.com/plaid/plaid-node/issues/new -[7]: https://github.com/plaid/plaid-node/blob/master/LICENSE -[8]: https://plaid.com/docs/link +[4]: https://github.com/plaid/plaid-node/issues/new +[5]: https://github.com/plaid/plaid-node/blob/master/LICENSE +[6]: https://plaid.com/docs/api#creating-items-with-plaid-link +[7]: ./CONTRIBUTING.md [9]: https://plaid.com/docs/link/stripe [10]: https://stripe.com/docs/api#create_bank_account_token +[11]: https://blog.plaid.com/improving-our-api/ +[12]: https://github.com/request/request/blob/master/README.md#requestoptions-callback diff --git a/circle.yml b/circle.yml old mode 100644 new mode 100755 index 96ea9c7b..e1ea935f --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ dependencies: machine: node: - version: 0.10.26 + version: 6.2.2 test: override: diff --git a/index.js b/index.js deleted file mode 100644 index 757ceaf1..00000000 --- a/index.js +++ /dev/null @@ -1,383 +0,0 @@ -'use strict'; - -var querystring = require('querystring'); - -var R = require('ramda'); -var request = require('request'); - -var Plaid = module.exports = {}; - -Plaid.environments = { - production: 'https://api.plaid.com', - tartan: 'https://tartan.plaid.com', -}; - -Plaid.Client = function(client_id, secret, env) { - if (R.isNil(client_id)) { - throw new Error('Missing Plaid "client_id"'); - } - - if (R.isNil(secret)) { - throw new Error('Missing Plaid "secret"'); - } - - if (env !== Plaid.environments.tartan && - env !== Plaid.environments.production) { - throw new Error('Invalid Plaid environment'); - } - - this.client_id = client_id; - this.secret = secret; - this.env = env; -}; - -// Private - -Plaid.Client.prototype._addUser = - function(product, type, credentials, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/' + product, - method: 'POST', - body: { - type: type, - credentials: credentials, - options: options, - }, - includeMfaResponse: true, - }, callback); -}; - -Plaid.Client.prototype._getUser = - function(product, access_token, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/' + product + '/get', - method: 'POST', - body: { - access_token: access_token, - options: options, - }, - includeMfaResponse: false, - }, callback); -}; - -Plaid.Client.prototype._stepUser = - function(product, access_token, mfaResponse, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/' + product + '/step', - method: 'POST', - body: { - access_token: access_token, - mfa: mfaResponse, - options: options, - }, - includeMfaResponse: true, - }, callback); -}; - -Plaid.Client.prototype._patchUser = - function(product, access_token, credentials, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/' + product, - method: 'PATCH', - body: { - access_token: access_token, - credentials: credentials, - options: options, - }, - includeMfaResponse: true, - }, callback); -}; - -Plaid.Client.prototype._deleteUser = - function(product, access_token, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/' + product, - method: 'DELETE', - body: { - access_token: access_token, - options: options, - }, - includeMfaResponse: false, - }, callback); -}; - -Plaid._publicRequest = function(options, callback) { - var $requestOptions = { - uri: options.uri, - method: options.method, - json: true, - }; - - if (options.body != null) { - $requestOptions = R.assoc('body', options.body, $requestOptions); - } - - request($requestOptions, function(err, res, $body) { - if (err != null) { - callback(err, null); - } else if (res.statusCode !== 200) { - callback(R.assoc('statusCode', res.statusCode, $body), null); - } else { - callback(null, $body); - } - }); -}; - -Plaid.Client.prototype._authenticatedRequest = function(options, callback) { - if (R.has('options', options.body)) { - options.body.options = JSON.stringify(options.body.options); - } - - request({ - uri: options.uri, - method: options.method, - json: R.merge({ - client_id: this.client_id, - secret: this.secret, - }, options.body), - }, function(err, res, body) { - handleApiResponse(err, res, body, options.includeMfaResponse, callback); - }); -}; - -// Authenticated Routes - -// Auth -Plaid.Client.prototype.addAuthUser = - R.partial(Plaid.Client.prototype._addUser, ['auth']); -Plaid.Client.prototype.getAuthUser = - R.partial(Plaid.Client.prototype._getUser, ['auth']); -Plaid.Client.prototype.stepAuthUser = - R.partial(Plaid.Client.prototype._stepUser, ['auth']); -Plaid.Client.prototype.patchAuthUser = - R.partial(Plaid.Client.prototype._patchUser, ['auth']); -Plaid.Client.prototype.deleteAuthUser = - R.partial(Plaid.Client.prototype._deleteUser, ['auth']); - -// Connect -Plaid.Client.prototype.addConnectUser = - R.partial(Plaid.Client.prototype._addUser, ['connect']); -Plaid.Client.prototype.getConnectUser = - R.partial(Plaid.Client.prototype._getUser, ['connect']); -Plaid.Client.prototype.stepConnectUser = - R.partial(Plaid.Client.prototype._stepUser, ['connect']); -Plaid.Client.prototype.patchConnectUser = - R.partial(Plaid.Client.prototype._patchUser, ['connect']); -Plaid.Client.prototype.deleteConnectUser = - R.partial(Plaid.Client.prototype._deleteUser, ['connect']); - -// Income -Plaid.Client.prototype.addIncomeUser = - R.partial(Plaid.Client.prototype._addUser, ['income']); -Plaid.Client.prototype.getIncomeUser = - R.partial(Plaid.Client.prototype._getUser, ['income']); -Plaid.Client.prototype.stepIncomeUser = - R.partial(Plaid.Client.prototype._stepUser, ['income']); -Plaid.Client.prototype.patchIncomeUser = - R.partial(Plaid.Client.prototype._patchUser, ['income']); -Plaid.Client.prototype.deleteIncomeUser = - R.partial(Plaid.Client.prototype._deleteUser, ['income']); - -// Info -Plaid.Client.prototype.addInfoUser = - R.partial(Plaid.Client.prototype._addUser, ['info']); -Plaid.Client.prototype.getInfoUser = - R.partial(Plaid.Client.prototype._getUser, ['info']); -Plaid.Client.prototype.stepInfoUser = - R.partial(Plaid.Client.prototype._stepUser, ['info']); -Plaid.Client.prototype.patchInfoUser = - R.partial(Plaid.Client.prototype._patchUser, ['info']); -Plaid.Client.prototype.deleteInfoUser = - R.partial(Plaid.Client.prototype._deleteUser, ['info']); - -// Risk -Plaid.Client.prototype.addRiskUser = - R.partial(Plaid.Client.prototype._addUser, ['risk']); -Plaid.Client.prototype.getRiskUser = - R.partial(Plaid.Client.prototype._getUser, ['risk']); -Plaid.Client.prototype.stepRiskUser = - R.partial(Plaid.Client.prototype._stepUser, ['risk']); -Plaid.Client.prototype.patchRiskUser = - R.partial(Plaid.Client.prototype._patchUser, ['risk']); -Plaid.Client.prototype.deleteRiskUser = - R.partial(Plaid.Client.prototype._deleteUser, ['risk']); - - -// exchangeToken -Plaid.Client.prototype.exchangeToken = - function(public_token, account_id, callback) { - if (typeof account_id === 'function') { - callback = account_id; - account_id = null; - } - this._authenticatedRequest({ - uri: this.env + '/exchange_token', - method: 'POST', - body: { - account_id: account_id, - public_token: public_token, - }, - includeMfaResponse: false, - }, callback); -}; - -// Balance -Plaid.Client.prototype.getBalance = function(access_token, callback) { - this._authenticatedRequest({ - uri: this.env + '/balance', - method: 'POST', - body: { - access_token: access_token, - }, - includeMfaResponse: false, - }, callback); -}; - -// Upgrade -Plaid.Client.prototype.upgradeUser = - function(access_token, upgrade_to, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this._authenticatedRequest({ - uri: this.env + '/upgrade', - method: 'POST', - body: { - access_token: access_token, - upgrade_to: upgrade_to, - options: options, - }, - includeMfaResponse: true, - }, callback); -}; - -// Longtail Institutions -Plaid.Client.prototype.getLongtailInstitutions = function(options, callback) { - this._authenticatedRequest({ - uri: this.env + '/institutions/longtail', - method: 'POST', - body: options, - }, callback); -}; - -// simplified `/institutions/all` endpoint -// Replaces the deprecated Longtail Institutions endpoint -Plaid.Client.prototype.getAllInstitutions = function(options, callback) { - this._authenticatedRequest({ - uri: this.env + '/institutions/all', - method: 'POST', - body: options, - }, callback); -}; - -// Public Routes - -Plaid.getCategory = function(category_id, env, callback) { - this._publicRequest({ - uri: env + '/categories/' + category_id, - method: 'GET', - }, callback); -}; - -Plaid.getCategories = function(env, callback) { - this._publicRequest({ - uri: env + '/categories', - method: 'GET', - }, callback); -}; - -Plaid.getInstitution = function(institution_id, env, callback) { - this._publicRequest({ - uri: env + '/institutions/' + institution_id, - method: 'GET', - }, callback); -}; - -Plaid.getInstitutions = function(env, callback) { - this._publicRequest({ - uri: env + '/institutions', - method: 'GET', - }, callback); -}; - -Plaid.searchInstitutions = function(options, env, callback) { - var qs = querystring.stringify(R.reject(R.isNil, { - id: options.id, - p: options.product, - q: options.query, - })); - this._publicRequest({ - uri: env + '/institutions/search?' + qs, - method: 'GET', - }, callback); -}; - -// simplified `/institutions/all` endpoint -// Replaces the deprecated `/institutions/search` endpoint -Plaid.searchAllInstitutions = function(options, env, callback) { - var qs = querystring.stringify(R.reject(R.isNil, { - id: options.id, - p: options.product, - q: options.query, - })); - this._publicRequest({ - uri: env + '/institutions/all/search?' + qs, - method: 'GET', - }, callback); -}; - -function handleApiResponse(err, res, $body, includeMfaResponse, callback) { - if (res != null) { - $body = R.merge({ - statusCode: res.statusCode, - request_id: res.headers['x-request-id'], - }, $body); - } - - if (includeMfaResponse) { - if (err != null) { - callback(err, null, null); - } else if (res.statusCode === 200) { - callback(null, null, $body); - } else if (res.statusCode === 201) { - callback(null, $body, null); - } else { - callback($body, null, null); - } - } else { - if (err != null) { - callback(err, null); - } else if (res.statusCode === 200) { - callback(null, $body); - } else { - callback($body, null); - } - } -} diff --git a/lib/PlaidClient.js b/lib/PlaidClient.js new file mode 100755 index 00000000..8499c51b --- /dev/null +++ b/lib/PlaidClient.js @@ -0,0 +1,258 @@ +'use strict'; + +const R = require('ramda'); + +const plaidEnvironments = require('./plaidEnvironments.js'); +const plaidRequest = require('./plaidRequest.js'); + +// Client(String, String, String, String, Object?) +function Client(client_id, secret, public_key, env, options) { + if (R.isNil(client_id)) { + throw new Error('Missing Plaid "client_id"'); + } + + if (R.isNil(secret)) { + throw new Error('Missing Plaid "secret"'); + } + + if (R.isNil(public_key)) { + throw new Error('Missing Plaid "public_key"'); + } + + if (!R.any(R.equals(env), R.values(plaidEnvironments))) { + throw new Error('Invalid Plaid environment'); + } + + this.client_id = client_id; + this.secret = secret; + this.env = env; + this.public_key = public_key; + this.client_request_opts = R.defaultTo({}, options); +} + +// Private +const requestWithAccessToken = function(path) { + return function(access_token, options, cb) { + return this._authenticatedRequest({ + path: path, + body: { + access_token: access_token, + } + }, options, cb); + }; +}; + +Client.prototype._authenticatedRequest = + function _authenticatedRequest(requestSpec, options, cb, withPublicKey) { + // juggle arguments + if (typeof options === 'function') { + cb = options; + options = {}; + } else { + requestSpec.body.options = options; + } + + const context = R.merge({env: this.env}, withPublicKey ? { + public_key: this.public_key, + } : { + client_id: this.client_id, + secret: this.secret, + } + ); + + return plaidRequest(context, requestSpec, this.client_request_opts, cb); + }; + +// createItem(Object, String, [String], Object?, Function) +Client.prototype.createItem = + function(credentials, institution_id, initial_products, options, cb) { + return this._authenticatedRequest({ + path: '/item/create', + body: { + credentials: credentials, + institution_id: institution_id, + initial_products: initial_products, + }, + includeMfaResponse: true + }, options, cb); + }; + +// answerItemMFA(String, String, [String], Object?, Function) +Client.prototype.answerItemMFA = + function(access_token, mfa_type, responses, options, cb) { + return this._authenticatedRequest({ + path: '/item/mfa', + body: { + access_token: access_token, + mfa_type: mfa_type, + responses: responses, + }, + includeMfaResponse: true + }, options, cb); + }; + +// updateItemCredentials(String, Object, Object?, Function) +Client.prototype.updateItemCredentials = + function(access_token, credentials, options, cb) { + return this._authenticatedRequest({ + path: '/item/credentials/update', + body: { + access_token: access_token, + credentials: credentials, + }, + includeMfaResponse: true + }, options, cb); + }; + +// createPublicToken(String, Function) +Client.prototype.createPublicToken = + requestWithAccessToken('/item/public_token/create', false); + +// exchangePublicToken(String, Function) +Client.prototype.exchangePublicToken = + function(public_token, cb) { + return this._authenticatedRequest({ + path: '/item/public_token/exchange', + body: { + public_token: public_token, + } + }, cb); + }; + +// updateAccessTokenVersion(String, Function) +Client.prototype.updateAccessTokenVersion = + function(legacy_access_token, cb) { + return this._authenticatedRequest({ + path: '/item/access_token/update_version', + body: { + access_token_v1: legacy_access_token, + } + }, cb); + }; + +// updateItemWebhook(String, String, Function) +Client.prototype.updateItemWebhook = + function(access_token, webhook, cb) { + return this._authenticatedRequest({ + path: '/item/webhook/update', + body: { + access_token: access_token, + webhook: webhook, + } + }, cb); + }; + +// createProcessorToken(String, String, String, Function) +Client.prototype.createProcessorToken = + function(access_token, account_id, processor, cb) { + const endpoint = processor === 'stripe' ? + '/processor/stripe/bank_account_token/create' : + '/processor/' + processor + '/processor_token/create'; + return this._authenticatedRequest({ + path: endpoint, + body: { + access_token: access_token, + account_id: account_id, + } + }, cb); + }; + +// invalidateAccessToken(String, Function) +Client.prototype.invalidateAccessToken = + requestWithAccessToken('/item/access_token/invalidate'); + +// deleteItem(String, Function) +Client.prototype.deleteItem = + requestWithAccessToken('/item/delete'); + +// getItem(String, Function) +Client.prototype.getItem = + requestWithAccessToken('/item/get'); + +// getAccounts(String, Object?, Function) +Client.prototype.getAccounts = + requestWithAccessToken('/accounts/get'); + +// getBalance(String, Object?, Function) +Client.prototype.getBalance = + requestWithAccessToken('/accounts/balance/get'); + +// getAuth(String, Object?, Function) +Client.prototype.getAuth = + requestWithAccessToken('/auth/get'); + +// getIdentity(String, Function) +Client.prototype.getIdentity = + requestWithAccessToken('/identity/get'); + +// getTransactions(String, Date, Date, Object?, Function) +Client.prototype.getTransactions = + function(access_token, start_date, end_date, options, cb) { + return this._authenticatedRequest({ + path: '/transactions/get', + body: { + access_token: access_token, + start_date: start_date, + end_date: end_date, + }, + }, options, cb); + }; + +// deactivateTransactions(String, Function) +Client.prototype.deactivateTransactions = + requestWithAccessToken('/transactions/deactivate'); + +// getCreditDetails(String, Function) +Client.prototype.getCreditDetails = + requestWithAccessToken('/credit_details/get'); + +// getInstitutions(Number, Number, Function); +Client.prototype.getInstitutions = + function(count, offset, cb) { + return this._authenticatedRequest({ + path: '/institutions/get', + body: { + count: count, + offset: offset + }, + }, cb); + }; + +// getInstitutionById(String, Object?, Function); +Client.prototype.getInstitutionById = + function(institution_id, options, cb) { + return this._authenticatedRequest({ + path: '/institutions/get_by_id', + body: { + institution_id: institution_id, + } + }, options, cb, true); + }; + +// searchInstitutionsByName(String, [String], Object?, Function) +Client.prototype.searchInstitutionsByName = + function(query, products, options, cb) { + return this._authenticatedRequest({ + path: '/institutions/search', + body: { + query: query, + products: products, + } + }, options, cb, true); +}; + +// getCategories(Function) +Client.prototype.getCategories = + function(cb) { + return plaidRequest({ + env: this.env + }, { + path: '/categories/get' + }, this.client_request_opts, cb); + }; + +// resetLogin(String, Function) - sandbox only +Client.prototype.resetLogin = + requestWithAccessToken('/sandbox/item/reset_login'); + +module.exports = Client; diff --git a/lib/plaid.js b/lib/plaid.js new file mode 100755 index 00000000..8fb4d431 --- /dev/null +++ b/lib/plaid.js @@ -0,0 +1,9 @@ +'use strict'; + +const environments = require('./plaidEnvironments.js'); +const Client = require('./PlaidClient.js'); + +module.exports = { + environments: environments, + Client: Client +}; diff --git a/lib/plaidEnvironments.js b/lib/plaidEnvironments.js new file mode 100755 index 00000000..96e49a61 --- /dev/null +++ b/lib/plaidEnvironments.js @@ -0,0 +1,9 @@ +'use strict'; + +const environments = { + production: 'https://production.plaid.com', + sandbox: 'https://sandbox.plaid.com', + development: 'https://development.plaid.com' +}; + +module.exports = environments; diff --git a/lib/plaidRequest.js b/lib/plaidRequest.js new file mode 100755 index 00000000..0bd1334b --- /dev/null +++ b/lib/plaidRequest.js @@ -0,0 +1,85 @@ +'use strict'; + +const R = require('ramda'); +const request = require('request'); +const pjson = require('../package.json'); + +// Max timeout of ten minutes +const DEFAULT_TIMEOUT_IN_MILLIS = 10 * 60 * 1000; + +const wrapPromise = function(promise, cb) { + if (cb) { + return promise.then(function(args) { + if (R.isArrayLike(args)) { + // call outside of promise stack + setImmediate(function() { + R.apply(R.partial(cb, [null]), args); + }); + } else { + setImmediate(function() { + cb(null, args); + }); + } + }).catch(function(err) { + setImmediate(function() { + cb(err); + }); + }); + } + return promise; +}; + +const handleApiResponse = function(resolve, reject, err, res, $body, isMfa) { + if (res != null) { + $body.status_code = res.statusCode; + } + + // network / usage errors + if (err != null) { + return reject(err); + + // success response (MFA) + } else if (isMfa && res.statusCode === 200) { + return resolve([null, $body]); + + // mfa response (MFA) + } else if (isMfa && res.statusCode === 210) { + return resolve([$body, null]); + + // success response (non mfa) + } else if (res.statusCode === 200) { + return resolve($body); + + // plaid error + } else { + return reject($body); + } +}; + +const plaidRequest = function(context, requestSpec, clientRequestOptions, cb) { + const uri = context.env + requestSpec.path; + const method = 'POST'; + const requestJSON = R.merge(R.dissoc('env', context), requestSpec.body); + const headers = { + 'User-Agent': `Plaid Node v${pjson.version}` + }; + + // merge the default request options with the client specified options, + // this allows for clients to supply extra options to the request function + const requestOptions = R.merge({ + uri: uri, + method: method, + json: requestJSON, + headers: headers, + timeout: DEFAULT_TIMEOUT_IN_MILLIS + }, clientRequestOptions); + + return wrapPromise(new Promise(function(resolve, reject) { + request(requestOptions, function(err, res, body) { + handleApiResponse(resolve, reject, err, res, body, + requestSpec.includeMfaResponse); + }); + }), cb); +}; + +module.exports = plaidRequest; diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 629156d3..acc1064f --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plaid", - "version": "1.2.0", + "version": "1.9.9", "description": "A node.js client for the Plaid API", "keywords": [ "plaid", @@ -8,11 +8,12 @@ ], "repository": { "type": "git", - "url": "git://github.com/plaid/plaid-node.git" + "url": "git@github.com:plaid/plaid-node.git" }, "bugs": { - "url": "http://github.com/plaid/plaid-node/issues" + "url": "https://github.com/plaid/plaid-node/issues" }, + "main": "lib/plaid.js", "licenses": [ { "type": "MIT", @@ -20,15 +21,18 @@ } ], "dependencies": { - "ramda": "0.19.x", + "ramda": "0.23.x", "request": "2.74.x" }, "devDependencies": { + "async": "2.1.x", + "dotenv": "4.0.x", + "expect.js": "0.3.x", "istanbul": "0.4.x", - "jscs": "2.7.x", - "jshint": "2.8.x", - "mocha": "2.3.x", - "proxyquire": "1.7.x", - "xyz": "0.5.x" + "jscs": "3.0.x", + "jshint": "2.9.x", + "mocha": "3.2.x", + "moment": "2.17.x", + "xyz": "2.1.x" } } diff --git a/test/PlaidClientTest.js b/test/PlaidClientTest.js new file mode 100755 index 00000000..f1c0b5bd --- /dev/null +++ b/test/PlaidClientTest.js @@ -0,0 +1,1009 @@ +'use strict'; + +/* global before, beforeEach, describe, it */ + +const crypto = require('crypto'); + +const async = require('async'); +const dotenv = require('dotenv'); +const expect = require('expect.js'); +const moment = require('moment'); +const R = require('ramda'); + +const plaid = require('../'); +const testConstants = require('./testConstants.js'); + +dotenv.config(); +const {SECRET, PUBLIC_KEY, CLIENT_ID} = process.env; + +describe('plaid.Client', () => { + + let pCl; + beforeEach(() => { + pCl = new plaid.Client(CLIENT_ID, SECRET, PUBLIC_KEY, + plaid.environments.sandbox); + }); + + describe('constructor', () => { + it('throws for missing client_id', () => { + expect(() => { + plaid.Client(null, SECRET, PUBLIC_KEY, plaid.environments.sandbox); + }).to.throwException(e => { + expect(e).to.be.ok(); + expect(e.message).to.equal('Missing Plaid "client_id"'); + }); + }); + + it('throws for missing secret', () => { + expect(() => { + plaid.Client(CLIENT_ID, null, PUBLIC_KEY, plaid.environments.sandbox); + }).to.throwException(e => { + expect(e).to.be.ok(); + expect(e.message).to.equal('Missing Plaid "secret"'); + }); + }); + + it('throws for missing public_key', () => { + expect(() => { + plaid.Client(CLIENT_ID, SECRET, null, plaid.environments.sandbox); + }).to.throwException(e => { + expect(e).to.be.ok(); + expect(e.message).to.equal('Missing Plaid "public_key"'); + }); + }); + + it('throws for invalid environment', () => { + expect(() => { + plaid.Client(CLIENT_ID, SECRET, PUBLIC_KEY, 'gingham'); + }).to.throwException(e => { + expect(e).to.be.ok(); + expect(e.message).to.equal('Invalid Plaid environment'); + }); + }); + + it('succeeds with all arguments', () => { + expect(() => { + R.forEachObjIndexed(env => { + plaid.Client(CLIENT_ID, SECRET, PUBLIC_KEY, env); + }, plaid.environments); + }).not.to.throwException(); + }); + }); + + describe('endpoints', () => { + + const now = moment().format('YYYY-MM-DD'); + let testAccessToken; + + before(cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + testAccessToken = successResponse.access_token; + + cb(); + }); + }); + + describe('item', () => { + + describe('createItem', () => { + it('normal flow', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + expect(successResponse.request_id).to.be.ok(); + + cb(); + }); + }); + + it('normal flow (w/o options arg)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + expect(successResponse.request_id).to.be.ok(); + + cb(); + }); + }); + + it('mfa flow', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_SELECTIONS + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.status_code).to.be(210); + expect(mfaResponse.request_id).to.be.ok(); + + cb(); + }); + }); + + it('err flow (invalid credentials)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.INVALID, + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + expect(err.request_id).to.be.ok(); + expect(successResponse).not.to.be.ok(); + expect(mfaResponse).not.to.be.ok(); + + cb(); + }); + }); + }); + + describe('itemManagement', () => { + + it('mfa', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_SELECTIONS + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + + cb(err, mfaResponse); + }); + }, + (mfaResponse, cb) => { + const accessToken = mfaResponse.access_token; + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.SELECTIONS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + } + ], cb); + }); + + it('mfa (w/o options arg)', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_SELECTIONS + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + + cb(err, mfaResponse); + }); + }, + (mfaResponse, cb) => { + const accessToken = mfaResponse.access_token; + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.SELECTIONS, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + } + ], cb); + }); + + it('update credentials, update credentials (w/o options arg)', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + + cb(null, successResponse); + }); + }, + (successResponse, cb) => { + const accessToken = successResponse.access_token; + + pCl.resetLogin(accessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse.reset_login); + + cb(null, accessToken); + }); + }, + (accessToken, cb) => { + pCl.updateItemCredentials(accessToken, { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, {}, (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(null, accessToken); + }); + }, (accessToken, cb) => { + pCl.resetLogin(accessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse.reset_login); + + cb(null, accessToken); + }); + }, + (accessToken, cb) => { + // juggled version + pCl.updateItemCredentials(accessToken, { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(null, accessToken); + }); + } + ], cb); + }); + + it('create and exchange a public token', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + + cb(null, successResponse); + }); + }, + (successResponse, cb) => { + const accessToken = successResponse.access_token; + + pCl.createPublicToken(accessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse.status_code).to.be(200); + expect(successResponse.public_token).to.be.ok(); + + cb(null, successResponse.public_token); + }); + }, + (publicToken, cb) => { + pCl.exchangePublicToken(publicToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse.status_code).to.be(200); + expect(successResponse.access_token).to.be.ok(); + + cb(); + }); + } + ], cb); + }); + + it('update access token version', cb => { + // Generate a dummy legacy access token, a 160 character hex string + // We only test the failure case here since we can't generate a + // valid legacy access_token + const dummyAccessToken = crypto.randomBytes(80).toString('hex'); + pCl.updateAccessTokenVersion(dummyAccessToken, + (err, successResponse) => { + void successResponse; + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + expect(err.error_code).to.be('INVALID_ACCESS_TOKEN'); + + cb(); + }); + }); + + it('invalidate an access_token, then delete the item', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + + cb(null, successResponse); + }); + }, + (successResponse, cb) => { + pCl.invalidateAccessToken(successResponse.access_token, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(null, successResponse.new_access_token); + }); + }, + (newAccessToken, cb) => { + pCl.deleteItem(newAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.deleted).to.be(true); + + cb(); + }); + } + ], cb); + }); + + it('update webhook', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + + cb(null, successResponse); + }); + }, + (successResponse, cb) => { + const accessToken = successResponse.access_token; + pCl.updateItemWebhook(accessToken, + 'https://fooWebhook.com', + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(null, accessToken); + }); + } + ], cb); + }); + }); + }); + + describe('product access', () => { + + it('item', cb => { + pCl.getItem(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + + cb(); + }); + }); + + it('accounts', cb => { + pCl.getAccounts(testAccessToken, {}, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + + cb(); + }); + }); + + it('accounts (w/o options arg)', cb => { + pCl.getAccounts(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + + cb(); + }); + }); + + it('balance', cb => { + pCl.getBalance(testAccessToken, {}, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + + cb(); + }); + }); + + it('balance (w/o options arg)', cb => { + pCl.getBalance(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + + cb(); + }); + }); + + it('auth', cb => { + pCl.getAuth(testAccessToken, {}, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + expect(successResponse.numbers).to.be.ok(); + + cb(); + }); + }); + + it('auth (w/o options arg)', cb => { + pCl.getAuth(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.accounts).to.be.ok(); + expect(successResponse.numbers).to.be.ok(); + + cb(); + }); + }); + + it('identity', cb => { + pCl.getIdentity(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.identity).to.be.ok(); + + cb(); + }); + }); + + it('credit details', cb => { + pCl.getCreditDetails(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.credit_details).to.be.ok(); + + cb(); + }); + }); + + it('transactions', cb => { + async.waterfall([ + cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, { + transactions: { + start_date: now, + end_date: now, + await_results: true + } + }, + (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + + cb(null, successResponse.access_token); + }); + }, (accessToken, cb) => { + pCl.getTransactions(accessToken, now, now, {}, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.transactions).to.be.an(Array); + + cb(); + }); + } + ], cb); + }); + + it('transactions (w/o options arg) (with 400)', cb => { + pCl.getTransactions('invalid token', now, now, + (err, successResponse) => { + expect(err).to.be.ok(); + expect(successResponse).not.to.be.ok(); + expect(err.status_code).to.be(400); + expect(err.request_id).to.be.ok(); + expect(err.error_code).to.be('INVALID_ACCESS_TOKEN'); + + cb(); + }); + }); + + it('deactivate transactions', cb => { + pCl.deactivateTransactions(testAccessToken, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + }); + }); + + describe('institutions', () => { + + it('get', cb => { + pCl.getInstitutions(10, 0, (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.institutions).to.be.an(Array); + + cb(); + }); + }); + + it('getById', cb => { + pCl.getInstitutionById(testConstants.INSTITUTION, {}, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.institution).to.be.ok(); + + cb(); + }); + }); + + it('getById (w/o options arg)', cb => { + pCl.getInstitutionById(testConstants.INSTITUTION, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.institution).to.be.ok(); + + cb(); + }); + }); + + it('search', cb => { + pCl.searchInstitutionsByName(testConstants.INSTITUTION, null, {}, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.institutions).to.be.an(Array); + + cb(); + }); + }); + + it('search (w/o options arg)', cb => { + pCl.searchInstitutionsByName(testConstants.INSTITUTION, null, + (err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.institutions).to.be.an(Array); + + cb(); + }); + }); + }); + + describe('categories', () => { + it('get', cb => { + pCl.getCategories((err, successResponse) => { + expect(err).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.categories).to.be.an(Array); + + cb(); + }); + }); + }); + + describe('errors', () => { + it('MFA bad request (library error)', cb => { + // branch is only reachable by mucking with Client's internal state + pCl.env = null; + + pCl.updateItemCredentials(testAccessToken, { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD, + }, + (err, mfaResponse, successResponse) => { + expect(err).to.be.ok(); + expect(err.status_code).not.to.be.ok(); + expect(mfaResponse).not.to.be.ok(); + expect(successResponse).not.to.be.ok(); + + cb(); + }); + }); + + it('no MFA bad request (library error)', cb => { + pCl.env = null; + + pCl.getItem(null, (err, successResponse) => { + expect(err).to.be.ok(); + expect(err.status_code).not.to.be.ok(); + expect(successResponse).not.to.be.ok(); + + cb(); + }); + }); + }); + }); + + describe('create an item and complete MFA flow', () => { + it('device', cb => { + const credentials = { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_DEVICE + }; + + let accessToken; + + async.waterfall([ + cb => { + pCl.createItem(credentials, testConstants.INSTITUTION, + testConstants.PRODUCTS, cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('device_list'); + expect(mfaResponse.device_list).to.be.ok(); + accessToken = mfaResponse.access_token; + + // arbitrarily choose the first device option + const chosenDevice = R.head(mfaResponse.device_list); + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + [chosenDevice.device_id], cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('device'); + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.DEVICE, cb); + } + ], (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.request_id).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + }); + + it('selections', cb => { + const credentials = { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_SELECTIONS + }; + + async.waterfall([ + cb => { + pCl.createItem(credentials, testConstants.INSTITUTION, + testConstants.PRODUCTS, cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('selections'); + expect(mfaResponse.device_list).to.be(null); + + pCl.answerItemMFA(mfaResponse.access_token, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.SELECTIONS, cb); + } + ], (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.request_id).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + }); + + it('questions_1_1', cb => { + // 2 rounds, 2 questions each + const credentials = { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_QUESTIONS_1_1 + }; + + let accessToken; + + async.waterfall([ + cb => { + pCl.createItem(credentials, testConstants.INSTITUTION, + testConstants.PRODUCTS, cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('questions'); + expect(mfaResponse.device_list).to.be(null); + accessToken = mfaResponse.access_token; + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.QUESTIONS_1_1[0], cb); + }, + ], (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.request_id).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + }); + + it('questions_2_2', cb => { + // 2 rounds, 2 questions each + const credentials = { + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_QUESTIONS_2_2 + }; + + let accessToken; + + async.waterfall([ + cb => { + pCl.createItem(credentials, testConstants.INSTITUTION, + testConstants.PRODUCTS, cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('questions'); + expect(mfaResponse.device_list).to.be(null); + accessToken = mfaResponse.access_token; + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.QUESTIONS_2_2[0], cb); + }, + (mfaResponse, successResponse, cb) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.mfa_type).to.be('questions'); + expect(mfaResponse.device_list).to.be(null); + + pCl.answerItemMFA(accessToken, mfaResponse.mfa_type, + testConstants.MFA_RESPONSES.QUESTIONS_2_2[1], cb); + } + ], (err, mfaResponse, successResponse) => { + expect(err).to.be(null); + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.item).to.be.ok(); + expect(successResponse.request_id).to.be.ok(); + expect(successResponse.status_code).to.be(200); + + cb(); + }); + }); + }); + + describe('promises', () => { + let testAccessToken; + + beforeEach(cb => { + const createItem = pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}); + + createItem.then(([mfaResponse, successResponse]) => { + testAccessToken = successResponse.access_token; + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + describe('success path', () => { + it('normal', cb => { + pCl.searchInstitutionsByName(testConstants.INSTITUTION, null, {}) + .then(successResponse => { + expect(successResponse).to.be.ok(); + expect(successResponse.institutions).to.be.an(Array); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + it('normal (w/o options arg)', cb => { + pCl.searchInstitutionsByName(testConstants.INSTITUTION, null) + .then(successResponse => { + expect(successResponse).to.be.ok(); + expect(successResponse.institutions).to.be.an(Array); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + it('mfa (success)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}) + .then(([mfaResponse, successResponse]) => { + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + expect(successResponse.request_id).to.be.ok(); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + it('mfa (success) (w/o options arg)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.GOOD + }, testConstants.INSTITUTION, testConstants.PRODUCTS) + .then(([mfaResponse, successResponse]) => { + expect(mfaResponse).to.be(null); + expect(successResponse).to.be.ok(); + expect(successResponse.status_code).to.be(200); + expect(successResponse.request_id).to.be.ok(); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + it('mfa (mfa)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_DEVICE + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}) + .then(([mfaResponse, successResponse]) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.status_code).to.be(210); + expect(mfaResponse.request_id).to.be.ok(); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + + it('mfa (mfa) (w/o options arg)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.PASSWORDS.MFA_DEVICE + }, testConstants.INSTITUTION, testConstants.PRODUCTS) + .then(([mfaResponse, successResponse]) => { + expect(successResponse).to.be(null); + expect(mfaResponse).to.be.ok(); + expect(mfaResponse.status_code).to.be(210); + expect(mfaResponse.request_id).to.be.ok(); + }).catch(err => { + void err; + throw new Error('Unreachable code block for test'); + }).then(() => { + cb(); + }); + }); + }); + + describe('error path', () => { + it('normal', cb => { + pCl.getAccounts('promise', {}).then(successResponse => { + void successResponse; + throw new Error('Unreachable code block for test'); + }).catch(err => { + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + }).then(() => { + cb(); + }); + }); + + it('normal (w/o options arg)', cb => { + pCl.getAccounts('promise').then(successResponse => { + void successResponse; + throw new Error('Unreachable code block for test'); + }).catch(err => { + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + }).then(() => { + cb(); + }); + }); + + it('mfa', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.INVALID + }, testConstants.INSTITUTION, testConstants.PRODUCTS, {}) + .then(([mfaResponse, successResponse]) => { + void successResponse; + throw new Error('Unreachable code block for test'); + }).catch(err => { + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + }).then(() => { + cb(); + }); + }); + + it('mfa (w/o options arg)', cb => { + pCl.createItem({ + username: testConstants.USERNAME, + password: testConstants.INVALID + }, testConstants.INSTITUTION, testConstants.PRODUCTS) + .then(([mfaResponse, successResponse]) => { + void successResponse; + throw new Error('Unreachable code block for test'); + }).catch(err => { + expect(err).to.be.ok(); + expect(err.status_code).to.be(400); + }).then(() => { + cb(); + }); + }); + }); + }); +}); diff --git a/test/authenticated.js b/test/authenticated.js deleted file mode 100644 index 5df591b4..00000000 --- a/test/authenticated.js +++ /dev/null @@ -1,1074 +0,0 @@ -'use strict'; - -/* global describe, it */ - -var assert = require('assert'); - -var R = require('ramda'); -var proxyquire = require('proxyquire'); - -var eq = assert.strictEqual; - -var Plaid = require('../'); - - -describe('Plaid.Client', function() { - - it('throws for missing client_id', function(done) { - assert.throws(function() { - Plaid.Client(null, 'secret', Plaid.environments.tartan); - }, Error); - done(); - }); - - it('throws for missing secret', function(done) { - assert.throws(function() { - Plaid.Client('client_id', null, Plaid.environments.tartan); - }, Error); - done(); - }); - - it('throws for invalid environment', function(done) { - assert.throws(function() { - Plaid.Client('client_id', 'secret', 'foo'); - }, Error); - done(); - }); - -}); - -describe('Plaid Client - Balance', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('returns account balances', function(done) { - client.getBalance('test_chase', function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); -}); - -describe('Plaid Client - Exchange Token', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('returns an access_token', function(done) { - client.exchangeToken('test,chase,connected', null, function(err, res) { - eq(err, null); - - eq(res.access_token, 'test_chase'); - - done(); - }); - }); - - it('does not require an account_id parameter', function(done) { - client.exchangeToken('test,chase,connected', function(err, res) { - eq(err, null); - - eq(res.access_token, 'test_chase'); - - done(); - }); - }); -}); - -describe('Plaid Client - Upgrade', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('does not require an options parameter', - function(done) { - client.upgradeUser('test_chase', 'info', function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('info', res)); - done(); - }); - }); - - it('upgrades a user successfully', function(done) { - client.upgradeUser('test_chase', 'connect', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); -}); - -describe('Plaid.Client - Auth', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - // Mocked client simulates connection failures - var mocked_plaid = proxyquire('../', { - request: function(body, callback) { - callback(new Error('foobar')); - }, - }); - var mocked_client = new mocked_plaid.Client( - 'test_id', 'test_secret', Plaid.environments.tartan); - - it('Plaid.Client.addAuthUser returns accounts for a non-MFA user', - function(done) { - client.addAuthUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - done(); - }); - - }); - - it('Plaid.Client.addAuthUser returns plaid errors as the "err" ' + - 'arg to callback', function(done) { - client.addAuthUser('wells', { - username: 'plaid_test', - password: 'plaid_locked', - }, {}, function(err, mfa, res) { - eq(mfa, null); - eq(res, null); - - eq(err.code, 1205); - eq(err.message, 'account locked'); - done(); - }); - - }); - - it('Plaid.Client.addAuthUser does not require an options parameter', - function(done) { - client.addAuthUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - done(); - }); - - }); - - it('Plaid.Client.addAuthUser returns questions for a MFA question user', - function(done) { - client.addAuthUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'questions'); - - done(); - }); - - }); - - it('Plaid.Client.addAuthUser returns selections for a MFA selections user', - function(done) { - client.addAuthUser('citi', { - username: 'plaid_selections', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'selections'); - - done(); - }); - - }); - - it('Plaid.Client.addAuthUser returns list of send_methods for a MFA ' + - 'device user when options.list === true', function(done) { - client.addAuthUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, { - list: true, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'list'); - - done(); - }); - - }); - - it('Plaid.Client.addAuthUser returns a code sent message for MFA device ' + - 'user when options.list !== true', function(done) { - client.addAuthUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.addAuthUser gracefully handles connection errors', - function(done) { - mocked_client.addAuthUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfaRes, res) { - eq(mfaRes, null); - eq(res, null); - - eq(err.toString(), 'Error: foobar'); - - done(); - }); - }); - - it('Plaid.Client.stepAuthUser accepts a send_method in options', - function(done) { - client.stepAuthUser('test_chase', '', { - send_method: {type: 'phone'}, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepAuthUser does not require an options parameter', - function(done) { - client.stepAuthUser('test_bofa', 'tomato', function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - - it('Plaid.Client.stepAuthUser returns accounts for valid ' + - 'MFA question answer', function(done) { - client.stepAuthUser('test_bofa', 'tomato', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - it('Plaid.Client.stepAuthUser returns accounts for valid MFA code answer', - function(done) { - client.stepAuthUser('test_chase', '1234', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - it('Plaid.Client.stepAuthUser returns accounts for valid ' + - 'MFA selections answers', function(done) { - client.stepAuthUser('test_citi', JSON.stringify([ - 'tomato', - 'ketchup', - ]), {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - it('Plaid.Client.getAuthUser gracefully handles connection errors', - function(done) { - mocked_client.getAuthUser('test_bofa', {}, function(err, res) { - eq(res, null); - - eq(err.toString(), 'Error: foobar'); - - done(); - }); - }); - - it('Plaid.Client.getAuthUser returns accounts', - function(done) { - client.getAuthUser('test_chase', {}, function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - it('Plaid.Client.getAuthUser returns a plaid error as the ' + - '"err" arg to callback', function(done) { - client.getAuthUser('foo', {}, function(err, res) { - eq(res, null); - - eq(err.code, 1105); - eq(err.message, 'bad access_token'); - - done(); - }); - }); - - it('Plaid.Client.getAuthUser does not require an options paramter', - function(done) { - client.getAuthUser('test_chase', function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - - done(); - }); - }); - - it('Plaid.Client.patchAuthUser patches a user', function(done) { - client.patchAuthUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.patchAuthUser does not require an options parameter', - function(done) { - client.patchAuthUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.deleteAuthUser deletes a user', function(done) { - client.deleteAuthUser('test_chase', {}, function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - - it('Plaid.Client.deleteAuthUser does not require an options parameter', - function(done) { - client.deleteAuthUser('test_chase', function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - Connect', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('Plaid.Client.addConnectUser returns accounts and transactions ' + - 'for a non-MFA user', function(done) { - client.addConnectUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('transactions', res)); - - done(); - }); - - }); - - it('Plaid.Client.addConnectUser returns questions for a MFA question user', - function(done) { - client.addConnectUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'questions'); - - done(); - }); - - }); - - it('Plaid.Client.addConnectUser returns selections for a MFA ' + - 'selections user', function(done) { - client.addConnectUser('citi', { - username: 'plaid_selections', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'selections'); - - done(); - }); - - }); - - it('Plaid.Client.addConnectUser returns list of send_methods for a ' + - 'MFA device user when options.list === true', function(done) { - client.addConnectUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, { - list: true, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'list'); - - done(); - }); - - }); - - it('Plaid.Client.addConnectUser returns a code sent message for ' + - 'MFA device user when options.list !== true', function(done) { - client.addConnectUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepConnectUser accepts a send_method in options', - function(done) { - client.stepConnectUser('test_chase', '', { - send_method: {type: 'phone'}, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepConnectUser returns accounts and transactions for' + - 'valid MFA question answer', function(done) { - client.stepConnectUser('test_bofa', 'tomato', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('transactions', res)); - - done(); - }); - }); - - it('Plaid.Client.stepConnectUser returns accounts and transactions for ' + - 'valid MFA code answer', function(done) { - client.stepConnectUser('test_chase', '1234', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('transactions', res)); - - done(); - }); - }); - - it('Plaid.Client.stepConnectUser returns accounts and transactions for ' + - 'valid MFA selections answers', function(done) { - client.stepConnectUser('test_citi', JSON.stringify([ - 'tomato', - 'ketchup', - ]), {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('transactions', res)); - - done(); - }); - }); - - it('Plaid.Client.getConnectUser returns accounts and transactions', - function(done) { - client.getConnectUser('test_chase', {}, function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - assert(R.has('transactions', res)); - - done(); - }); - }); - - it('Plaid.Client.patchConnectUser patches a user', function(done) { - client.patchConnectUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.deleteConnectUser deletes a user', function(done) { - client.deleteConnectUser('test_chase', {}, function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - Income', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('Plaid.Client.addIncomeUser returns accounts and income for a ' + - 'non-MFA user', function(done) { - client.addIncomeUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('income', res)); - - done(); - }); - - }); - - it('Plaid.Client.addIncomeUser returns questions for a MFA question user', - function(done) { - client.addIncomeUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'questions'); - - done(); - }); - - }); - - it('Plaid.Client.addIncomeUser returns list of send_methods for a ' + - 'MFA device user when options.list === true', function(done) { - client.addIncomeUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, { - list: true, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'list'); - - done(); - }); - - }); - - it('Plaid.Client.addIncomeUser returns a code sent message for MFA device ' + - 'user when options.list !== true', function(done) { - client.addIncomeUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepIncomeUser accepts a send_method in options', - function(done) { - client.stepIncomeUser('test_chase', '', { - send_method: {type: 'phone'}, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepIncomeUser returns accounts and income for ' + - 'valid MFA question answer', function(done) { - client.stepIncomeUser('test_bofa', 'tomato', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('income', res)); - - done(); - }); - }); - - it('Plaid.Client.stepIncomeUser returns accounts and income for ' + - 'valid MFA code answer', function(done) { - client.stepIncomeUser('test_chase', '1234', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('income', res)); - - done(); - }); - }); - - it('Plaid.Client.getIncomeUser returns accounts and income', - function(done) { - client.getIncomeUser('test_chase', {}, function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - assert(R.has('income', res)); - - done(); - }); - }); - - it('Plaid.Client.patchIncomeUser patches a user', function(done) { - client.patchIncomeUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.deleteIncomeUser deletes a user', function(done) { - client.deleteIncomeUser('test_chase', {}, function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - Info', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('Plaid.Client.addInfoUser returns accounts and info for a non-MFA user', - function(done) { - client.addInfoUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('info', res)); - - done(); - }); - - }); - - it('Plaid.Client.addInfoUser returns questions for a MFA question user', - function(done) { - client.addInfoUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'questions'); - - done(); - }); - - }); - - it('Plaid.Client.addInfoUser returns list of send_methods for a ' + - 'MFA device user when options.list === true', function(done) { - client.addInfoUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, { - list: true, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'list'); - - done(); - }); - - }); - - it('Plaid.Client.addInfoUser returns a code sent message for MFA device ' + - 'user when options.list !== true', function(done) { - client.addInfoUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepInfoUser accepts a send_method in options', - function(done) { - client.stepInfoUser('test_chase', '', { - send_method: {type: 'phone'}, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepInfoUser returns accounts and info for ' + - 'valid MFA question answer', function(done) { - client.stepInfoUser('test_bofa', 'tomato', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('info', res)); - - done(); - }); - }); - - it('Plaid.Client.stepInfoUser returns accounts and info for ' + - 'valid MFA code answer', function(done) { - client.stepInfoUser('test_chase', '1234', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - assert(R.has('info', res)); - - done(); - }); - }); - - it('Plaid.Client.getInfoUser returns accounts and info', - function(done) { - client.getInfoUser('test_chase', {}, function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - assert(R.has('info', res)); - - done(); - }); - }); - - it('Plaid.Client.patchInfoUser patches a user', function(done) { - client.patchInfoUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.deleteInfoUser deletes a user', function(done) { - client.deleteInfoUser('test_chase', {}, function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - Risk', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - it('Plaid.Client.addRiskUser returns accounts and risk for a ' + - 'non-MFA user', function(done) { - client.addRiskUser('wells', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - R.forEach(R.pipe(R.has('risk'), assert), res.accounts); - - done(); - }); - - }); - - it('Plaid.Client.addRiskUser returns questions for a MFA question user', - function(done) { - client.addRiskUser('bofa', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'questions'); - - done(); - }); - - }); - - it('Plaid.Client.addRiskUser returns list of send_methods for a ' + - 'MFA device user when options.list === true', function(done) { - client.addRiskUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, { - list: true, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'list'); - - done(); - }); - - }); - - it('Plaid.Client.addRiskUser returns a code sent message for MFA device ' + - 'user when options.list !== true', function(done) { - client.addRiskUser('chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepRiskUser accepts a send_method in options', - function(done) { - client.stepRiskUser('test_chase', '', { - send_method: {type: 'phone'}, - }, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.stepRiskUser returns accounts and risk for ' + - 'valid MFA question answer', function(done) { - client.stepRiskUser('test_bofa', 'tomato', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - R.forEach(R.pipe(R.has('risk'), assert), res.accounts); - - done(); - }); - }); - - it('Plaid.Client.stepRiskUser returns accounts and risk for ' + - 'valid MFA code answer', function(done) { - client.stepRiskUser('test_chase', '1234', {}, function(err, mfa, res) { - eq(err, null); - eq(mfa, null); - - assert(R.has('accounts', res)); - R.forEach(R.pipe(R.has('risk'), assert), res.accounts); - - done(); - }); - }); - - it('Plaid.Client.getRiskUser returns accounts and risk', - function(done) { - client.getRiskUser('test_chase', {}, function(err, res) { - eq(err, null); - - assert(R.has('accounts', res)); - R.forEach(R.pipe(R.has('risk'), assert), res.accounts); - - done(); - }); - }); - - it('Plaid.Client.patchRiskUser patches a user', function(done) { - client.patchRiskUser('test_chase', { - username: 'plaid_test', - password: 'plaid_good', - }, {}, function(err, mfa, res) { - eq(err, null); - eq(res, null); - - eq(mfa.type, 'device'); - - done(); - }); - }); - - it('Plaid.Client.deleteRiskUser deletes a user', function(done) { - client.deleteRiskUser('test_chase', {}, function(err, res) { - eq(err, null); - - eq(res.message, 'Successfully removed from your account'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - Longtail Institutions', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - // The /institutions/longtail endpoint requires additional permissions - // to access. Accessing it with the sandbox client ID and secret returns - // an error, which we test here. - it('Plaid.Client.getLongtailInstitutions returns institutions', - function(done) { - client.getLongtailInstitutions({}, function(err, res) { - eq(err, null); - - eq(R.type(res.results), 'Array'); - eq(R.type(res.total_count), 'Number'); - - done(); - }); - }); - -}); - -describe('Plaid.Client - All Institutions', function() { - var client = - new Plaid.Client('test_id', 'test_secret', Plaid.environments.tartan); - - // The /institutions/all endpoint requires additional permissions - // to access. Accessing it with the sandbox client ID and secret returns - // an error, which we test here. - it('Plaid.Client.getAllInstitutions returns institutions', - function(done) { - client.getAllInstitutions({}, function(err, res) { - eq(err, null); - - eq(R.type(res.results), 'Array'); - eq(R.type(res.total_count), 'Number'); - - done(); - }); - }); - -}); diff --git a/test/public.js b/test/public.js deleted file mode 100644 index 16866dfb..00000000 --- a/test/public.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -/* global describe, it */ - -var assert = require('assert'); - -var R = require('ramda'); -var proxyquire = require('proxyquire'); - -var eq = assert.strictEqual; - -var Plaid = require('../'); - -describe('Plaid.getCategory', function() { - - it('returns a code 1501 error if the category does not exist', - function(done) { - Plaid.getCategory('xxx', Plaid.environments.tartan, function(err, res) { - eq(err.code, 1501); - eq(err.statusCode, 404); - eq(res, null); - - done(); - }); - }); - - it('returns a category for a valid category id', function(done) { - Plaid.getCategory('10000000', Plaid.environments.tartan, - function(err, res) { - eq(err, null); - - eq(res.type, 'special'); - eq(res.hierarchy.length, 1); - done(); - }); - }); - - it('handles connection errors gracefully', function(done) { - var mocked_plaid = proxyquire('../', { - request: function(body, callback) { - callback(new Error('foobar')); - }, - }); - - mocked_plaid.getCategory('10000000', Plaid.environments.tartan, - function(err, res) { - eq(res, null); - - eq(err.toString(), 'Error: foobar'); - - done(); - }); - }); - -}); - -describe('Plaid.getCategories', function() { - - it('returns a list of Plaid categories', function(done) { - Plaid.getCategories(Plaid.environments.tartan, function(err, res) { - eq(err, null); - - assert(R.is(Array, res)); - - done(); - }); - }); - -}); - -describe('Plaid.getInstitution', function() { - - it('returns a code 1301 error if the institution does not exist', - function(done) { - Plaid.getInstitution('xxx', Plaid.environments.tartan, function(err, res) { - eq(err.code, 1301); - eq(err.statusCode, 404); - eq(res, null); - - done(); - }); - }); - - it('returns an institution for a valid institution id', function(done) { - Plaid.getInstitution('5301a93ac140de84910000e0', Plaid.environments.tartan, - function(err, res) { - eq(err, null); - - eq(res.type, 'bofa'); - - done(); - }); - }); - -}); - -describe('Plaid.getInstitutions', function() { - - it('returns a list of Plaid institutions', function(done) { - Plaid.getInstitutions(Plaid.environments.tartan, function(err, res) { - eq(err, null); - - assert(R.is(Array, res)); - - done(); - }); - }); - -}); - -describe('Plaid.searchInstitutions', function() { - - it('returns a single institution given an "id"', function(done) { - Plaid.searchInstitutions({ - id: 'bofa', - }, Plaid.environments.tartan, function(err, res) { - eq(err, null); - - eq(res.id, 'bofa'); - eq(R.type(res), 'Object'); - - done(); - }); - }); - - it('returns a list of institutions given a "product" and "query"', - function(done) { - Plaid.searchInstitutions({ - product: 'connect', - query: 'suntrust', - }, Plaid.environments.tartan, function(err, res) { - eq(err, null); - - eq(R.type(res), 'Array'); - - done(); - }); - }); - -}); - -describe('Plaid.searchAllInstitutions', function() { - - it('returns a single institution given an "id"', function(done) { - Plaid.searchAllInstitutions({ - id: 'bofa', - }, Plaid.environments.tartan, function(err, res) { - eq(err, null); - - eq(res.id, 'bofa'); - eq(R.type(res), 'Object'); - - done(); - }); - }); - - it('returns a list of institutions given a "product" and "query"', - function(done) { - Plaid.searchAllInstitutions({ - product: 'connect', - query: 'suntrust', - }, Plaid.environments.tartan, function(err, res) { - eq(err, null); - - eq(R.type(res), 'Array'); - - done(); - }); - }); - -}); diff --git a/test/testConstants.js b/test/testConstants.js new file mode 100755 index 00000000..3d8eb88f --- /dev/null +++ b/test/testConstants.js @@ -0,0 +1,27 @@ +'use strict'; + +module.exports = { + USERNAME: 'user_good', + PASSWORDS: { + GOOD: 'pass_good', + MFA_SELECTIONS: 'mfa_selections', + MFA_DEVICE: 'mfa_device', + MFA_QUESTIONS_1_1: 'mfa_questions_1_1', + MFA_QUESTIONS_2_2: 'mfa_questions_2_2', + INVALID: 'error_INVALID_CREDENTIALS', + LOGIN_REQUIRED: 'error_ITEM_LOGIN_REQUIRED', + }, + INSTITUTION: 'ins_109508', + PRODUCTS: ['transactions', 'auth'], + MFA_RESPONSES: { + DEVICE: ['1234'], + QUESTIONS_1_1: [ + ['answer_0_0'] + ], + QUESTIONS_2_2: [ + ['answer_0_0', 'answer_0_1'], + ['answer_1_0', 'answer_1_1'] + ], + SELECTIONS: ['tomato', 'ketchup'], + } +};