Repository: auth0/node-auth0
Pull request #51 (2015-11-26 19:28:31)

Add unit tests, improve backward compatibility and some bug fixes
Changes:
- Fixed several bugs in some managers.
- Add unit tests for JobsManager
- Add unit tests for DeviceCredentialsManager
- Add unit tests for EmailProviderManager
- Add unit tests for the ConnectionsManager
- Add unit tests for the TenantsManager
- Add unit tests for the TicketsManager
- Add unit tests for the StatsManager
- Add unit tests for the RulesManager
- Improve validation of options in several managers
- Fix bug in importUsers and verifyEmail functions
- Update examples in the repo
- Add missing dependency to JobsManager
- Add updateUserMetadata and updateAppMetadata methods
- Add documentation build process
- Improve documentation of Auth0 class

ngonzalvez: There was an issue with the commits history. I already fixed it but it needs to be forced to merge the branches. It is safe since it only affects my own commits.
ntotten: @ngonzalvez Looks like there are some conflicts

Patch:
diff --git a/.gitignore b/.gitignore
index 263f819ef..a6b600aef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,5 @@ node_modules
 # InelliJ IDEA
 .idea
 
+# Documentation
+docs/*
diff --git a/examples/updateAppMetadata.js b/examples/updateAppMetadata.js
index f6928bce3..e3b31b350 100644
--- a/examples/updateAppMetadata.js
+++ b/examples/updateAppMetadata.js
@@ -1,11 +1,17 @@
 var token = '{YOUR_API_JSON_WEB_TOKEN}';
 var auth0 = require('..')({ token: token });
 
-auth0.users.updateAppMetadata('google-oauth2|1234', {
+var metadata = {
   rules: ['writer', 'reader'],
   plan: null
-}).then(function(body){
-  console.log('Body', body);
-}).catch(function(err){
-  console.error(err);
-});
\ No newline at end of file
+};
+
+auth0
+  .users
+  .updateAppMetadata({ id: 'google-oauth2|1234' }, metadata)
+  .then(function(body){
+    console.log('Body', body);
+  })
+  .catch(function(err){
+    console.error(err);
+  });
diff --git a/examples/updateUserMetadata.js b/examples/updateUserMetadata.js
index 4476af3c9..470e3e1df 100644
--- a/examples/updateUserMetadata.js
+++ b/examples/updateUserMetadata.js
@@ -1,11 +1,17 @@
 var token = '{YOUR_API_JSON_WEB_TOKEN}';
 var auth0 = require('..')({ token: token });
 
-auth0.users.updateUserMetadata('google-oauth2|1234', {
+var metadata = {
   hobby: 'surf',
   theme: 'green'
-}).then(function(body){
-  console.log('Body', body);
-}).catch(function(err){
-  console.error(err);
-});
\ No newline at end of file
+};
+
+auth0
+  .users
+  .updateUserMetadata({ id: 'google-oauth2|1234' }, metadata)
+  .then(function(body){
+    console.log('Body', body);
+  })
+  .catch(function(err){
+    console.error(err);
+  });
diff --git a/package.json b/package.json
index e4c38bb2b..e2cf03e65 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
   "main": "index.js",
   "scripts": {
     "test": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec $(find ./test -name *.tests.js) && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage",
-    "test-watch": "NODE_ENV=test mocha --timeout 5000 $(find ./test -name *.tests.js) --watch"
+    "test-watch": "NODE_ENV=test mocha --timeout 5000 $(find ./test -name *.tests.js) --watch",
+    "docs-build": "jsdoc src/*.js -d docs -t node_modules/latodoc"
   },
   "repository": {
     "type": "git",
@@ -31,6 +32,8 @@
     "chai": "^2.2.0",
     "coveralls": "^2.11.4",
     "istanbul": "^0.4.0",
+    "jsdoc": "^3.4.0",
+    "latodoc": "smeijer/latodoc",
     "mocha": "^2.2.4",
     "nock": "^3.1.1",
     "sinon": "^1.17.1"
diff --git a/src/Auth0.js b/src/Auth0.js
index 81c5da4da..bdadd4f9f 100644
--- a/src/Auth0.js
+++ b/src/Auth0.js
@@ -17,8 +17,6 @@ var JobsManager = require('./JobsManager');
 var TicketsManager = require('./TicketsManager');
 
 var BASE_URL_FORMAT = 'https://%s/api/v2';
-var clientInfo = null;
-var b65ClientInfo = null;
 
 
 /**
@@ -61,7 +59,7 @@ var Auth0 = function (options) {
    * Simple abstraction for performing CRUD operations on the
    * clients endpoint.
    *
-   * @type {ClientsManager}.
+   * @type {ClientsManager}
    */
   this.clients = new ClientsManager(managerOptions);
 
@@ -179,7 +177,7 @@ Auth0.prototype.getAuth0ClientInfo = function () {
 /**
  * Wrapper for auth0.connections.getAll()
  *
- * @method
+ * @method getConnections
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getConnections', 'connections.getAll');
@@ -188,7 +186,7 @@ utils.wrapPropertyMethod(Auth0, 'getConnections', 'connections.getAll');
 /**
  * Wrapper for auth0.connections.create()
  *
- * @method
+ * @method createConnection
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createConnection', 'connections.create');
@@ -197,7 +195,7 @@ utils.wrapPropertyMethod(Auth0, 'createConnection', 'connections.create');
 /**
  * Wrapper for auth0.connections.get()
  *
- * @method
+ * @method getConnection
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getConnection', 'connections.get');
@@ -206,7 +204,7 @@ utils.wrapPropertyMethod(Auth0, 'getConnection', 'connections.get');
 /**
  * Wrapper for auth0.connections.delete()
  *
- * @method
+ * @method deleteConnection
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteConnection', 'connections.delete');
@@ -215,7 +213,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteConnection', 'connections.delete');
 /**
  * Wrapper for auth0.connections.update()
  *
- * @method
+ * @method updateConnection
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateConnection', 'connections.update');
@@ -224,7 +222,7 @@ utils.wrapPropertyMethod(Auth0, 'updateConnection', 'connections.update');
 /**
  * Wrapper for auth0.clients.getAll()
  *
- * @method
+ * @method getClients
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getClients', 'clients.getAll');
@@ -233,7 +231,7 @@ utils.wrapPropertyMethod(Auth0, 'getClients', 'clients.getAll');
 /**
  * Wrapper for auth0.clients.get()
  *
- * @method
+ * @method getClient
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getClient', 'clients.get');
@@ -242,7 +240,7 @@ utils.wrapPropertyMethod(Auth0, 'getClient', 'clients.get');
 /**
  * Wrapper for auth0.clients.create()
  *
- * @method
+ * @method createClient
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createClient', 'clients.create');
@@ -251,7 +249,7 @@ utils.wrapPropertyMethod(Auth0, 'createClient', 'clients.create');
 /**
  * Wrapper for auth0.clients.update()
  *
- * @method
+ * @method updateClient
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateClient', 'clients.update');
@@ -260,7 +258,7 @@ utils.wrapPropertyMethod(Auth0, 'updateClient', 'clients.update');
 /**
  * Wrapper for auth0.clients.delete()
  *
- * @method
+ * @method deleteClient
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteClient', 'clients.delete');
@@ -269,7 +267,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteClient', 'clients.delete');
 /**
  * Wrapper for auth0.deviceCredentials.createPubicKey()
  *
- * @method
+ * @method createDevicePublicKey
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createDevicePublicKey', 'deviceCredentials.createPublicKey');
@@ -278,7 +276,7 @@ utils.wrapPropertyMethod(Auth0, 'createDevicePublicKey', 'deviceCredentials.crea
 /**
  * Wrapper for auth0.deviceCredentials.getAll()
  *
- * @method
+ * @method getDeviceCredentials
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getDeviceCredentials', 'deviceCredentials.getAll');
@@ -287,7 +285,7 @@ utils.wrapPropertyMethod(Auth0, 'getDeviceCredentials', 'deviceCredentials.getAl
 /**
  * Wrapper for auth0.deviceCredentials.delete()
  *
- * @method
+ * @method deleteDeviceCredential
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteDeviceCredential', 'deviceCredentials.delete');
@@ -296,7 +294,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteDeviceCredential', 'deviceCredentials.del
 /**
  * Wrapper for auth0.rules.getAll()
  *
- * @method
+ * @method getRules
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getRules', 'rules.getAll');
@@ -305,7 +303,7 @@ utils.wrapPropertyMethod(Auth0, 'getRules', 'rules.getAll');
 /**
  * Wrapper for auth0.rules.create()
  *
- * @method
+ * @method createRules
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createRule', 'rules.create');
@@ -314,7 +312,7 @@ utils.wrapPropertyMethod(Auth0, 'createRule', 'rules.create');
 /**
  * Wrapper for auth0.rules.get()
  *
- * @method
+ * @method getRule
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getRule', 'rules.get');
@@ -323,7 +321,7 @@ utils.wrapPropertyMethod(Auth0, 'getRule', 'rules.get');
 /**
  * Wrapper for auth0.rules.delete()
  *
- * @method
+ * @method deleteRule
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteRule', 'rules.delete');
@@ -332,7 +330,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteRule', 'rules.delete');
 /**
  * Wrapper for auth0.rules.update()
  *
- * @method
+ * @method updateRule
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateRule', 'rules.update');
@@ -341,7 +339,7 @@ utils.wrapPropertyMethod(Auth0, 'updateRule', 'rules.update');
 /**
  * Wrapper for auth0.users.getAll()
  *
- * @method
+ * @method getUsers
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getUsers', 'users.getAll');
@@ -350,7 +348,7 @@ utils.wrapPropertyMethod(Auth0, 'getUsers', 'users.getAll');
 /**
  * Wrapper for auth0.users.get()
  *
- * @method
+ * @method getUser
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getUser', 'users.get');
@@ -359,7 +357,7 @@ utils.wrapPropertyMethod(Auth0, 'getUser', 'users.get');
 /**
  * Wrapper for auth0.users.deleteAll()
  *
- * @method
+ * @method deleteAllUsers
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteAllUsers', 'users.deleteAll');
@@ -368,7 +366,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteAllUsers', 'users.deleteAll');
 /**
  * Wrapper for auth0.users.delete()
  *
- * @method
+ * @method deleteUser
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteUser', 'users.delete');
@@ -377,7 +375,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteUser', 'users.delete');
 /**
  * Wrapper for auth0.users.create()
  *
- * @method
+ * @method createUser
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createUser', 'users.create');
@@ -386,16 +384,61 @@ utils.wrapPropertyMethod(Auth0, 'createUser', 'users.create');
 /**
  * Wrapper for auth0.users.update()
  *
- * @method
+ * @method updateUser
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateUser', 'users.update');
 
 
+/**
+ * Wrapper for auth0.users.updateUserMetadata()
+ *
+ * @method updateUserMetadata
+ * @memberOf Auth0
+ */
+utils.wrapPropertyMethod(Auth0, 'updateUserMetadata', 'users.updateUserMetadata');
+
+
+/**
+ * Wrapper for auth0.users.updateAppMetadata()
+ *
+ * @method updateAppMetadata
+ * @memberOf Auth0
+ */
+utils.wrapPropertyMethod(Auth0, 'updateAppMetadata', 'users.updateAppMetadata');
+
+
+/**
+ * Wrapper for auth0.users.deleteMultifactorProvider()
+ *
+ * @method deleteUserMultifactor
+ * @memberOf Auth0
+ */
+utils.wrapPropertyMethod(Auth0, 'deleteUserMultifcator', 'users.deleteMultifactorProvider');
+
+
+/**
+ * Wrapper for auth0.users.unlink()
+ *
+ * @method unlinkUsers
+ * @memberOf Auth0
+ */
+utils.wrapPropertyMethod(Auth0, 'unlinkUsers', 'users.unlink');
+
+
+/**
+ * Wrapper for auth0.users.link()
+ *
+ * @method linkUsers
+ * @memberOf Auth0
+ */
+utils.wrapPropertyMethod(Auth0, 'linkUsers', 'users.link');
+
+
 /**
  * Wrapper for auth0.blacklistedTokens.getAll()
  *
- * @method
+ * @method getBlacklistedTokens
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getBlacklistedTokens', 'blacklistedTokens.getAll');
@@ -404,7 +447,7 @@ utils.wrapPropertyMethod(Auth0, 'getBlacklistedTokens', 'blacklistedTokens.getAl
 /**
  * Wrapper for auth0.blacklistedTokens.add()
  *
- * @method
+ * @method blacklistToken
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'blacklistToken', 'blacklistedTokens.add');
@@ -413,7 +456,7 @@ utils.wrapPropertyMethod(Auth0, 'blacklistToken', 'blacklistedTokens.add');
 /**
  * Wrapper for auth0.emailProvider.get()
  *
- * @method
+ * @method getEmailProvider
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getEmailProvider', 'emailProvider.get');
@@ -422,7 +465,7 @@ utils.wrapPropertyMethod(Auth0, 'getEmailProvider', 'emailProvider.get');
 /**
  * Wrapper for auth0.emailProvider.configure()
  *
- * @method
+ * @method configureEmailProvider
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'configureEmailProvider', 'emailProvider.configure');
@@ -431,7 +474,7 @@ utils.wrapPropertyMethod(Auth0, 'configureEmailProvider', 'emailProvider.configu
 /**
  * Wrapper for auth0.emailProvider.delete()
  *
- * @method
+ * @method deleteEmailProvider
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'deleteEmailProvider', 'emailProvider.delete');
@@ -440,7 +483,7 @@ utils.wrapPropertyMethod(Auth0, 'deleteEmailProvider', 'emailProvider.delete');
 /**
  * Wrapper for auth0.emailProvider.update()
  *
- * @method
+ * @method updateEmailProvider
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateEmailProvider', 'emailProvider.update');
@@ -449,7 +492,7 @@ utils.wrapPropertyMethod(Auth0, 'updateEmailProvider', 'emailProvider.update');
 /**
  * Wrapper for auth0.stats.getActiveUsersCount()
  *
- * @method
+ * @method getActiveUsersCount
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getActiveUsersCount', 'stats.getActiveUsersCount');
@@ -458,7 +501,7 @@ utils.wrapPropertyMethod(Auth0, 'getActiveUsersCount', 'stats.getActiveUsersCoun
 /**
  * Wrapper for auth0.stats.getDaily()
  *
- * @method
+ * @method getDailyStats
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getDailyStats', 'stats.getDaily');
@@ -467,7 +510,7 @@ utils.wrapPropertyMethod(Auth0, 'getDailyStats', 'stats.getDaily');
 /**
  * Wrapper for auth0.tenatn.getSettings()
  *
- * @method
+ * @method getTenantSettings
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getTenantSettings', 'tenant.getSettings');
@@ -476,7 +519,7 @@ utils.wrapPropertyMethod(Auth0, 'getTenantSettings', 'tenant.getSettings');
 /**
  * Wrapper for auth0.tenant.updateSettings()
  *
- * @method
+ * @method updateTenantSettings
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'updateTenantSettings', 'tenant.updateSettings');
@@ -485,7 +528,7 @@ utils.wrapPropertyMethod(Auth0, 'updateTenantSettings', 'tenant.updateSettings')
 /**
  * Wrapper for auth0.jobs.get()
  *
- * @method
+ * @method getJob
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'getJob', 'jobs.get');
@@ -494,7 +537,7 @@ utils.wrapPropertyMethod(Auth0, 'getJob', 'jobs.get');
 /**
  * Wrapper for auth0.jobs.importUsers()
  *
- * @method
+ * @method importUsers
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'importUsers', 'jobs.importUsers');
@@ -503,7 +546,7 @@ utils.wrapPropertyMethod(Auth0, 'importUsers', 'jobs.importUsers');
 /**
  * Wrapper for auth0.jobs.verifyEmail()
  *
- * @method
+ * @method sendEmailVerification
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'sendEmailVerification', 'jobs.verifyEmail');
@@ -512,7 +555,7 @@ utils.wrapPropertyMethod(Auth0, 'sendEmailVerification', 'jobs.verifyEmail');
 /**
  * Wrapper for auth0.tickets.changePassword()
  *
- * @method
+ * @method createPasswordChangeTicket
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createPasswordChangeTicket', 'tickets.changePassword');
@@ -521,7 +564,7 @@ utils.wrapPropertyMethod(Auth0, 'createPasswordChangeTicket', 'tickets.changePas
 /**
  * Wrapper for auth0.tickets.verifyEmail()
  *
- * @method
+ * @method createEmailVerificationTicket
  * @memberOf Auth0
  */
 utils.wrapPropertyMethod(Auth0, 'createEmailVerificationTicket', 'tickets.verifyEmail');
diff --git a/src/ConnectionsManager.js b/src/ConnectionsManager.js
index 9302a587a..adc088709 100644
--- a/src/ConnectionsManager.js
+++ b/src/ConnectionsManager.js
@@ -20,6 +20,14 @@ var ConnectionsManager = function (options) {
     throw new ArgumentError('Must provide client options');
   }
 
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   /**
    * Options object for the Rest Client instance.
    *
diff --git a/src/DeviceCredentialsManager.js b/src/DeviceCredentialsManager.js
index 8a98fa0f5..6a452f295 100644
--- a/src/DeviceCredentialsManager.js
+++ b/src/DeviceCredentialsManager.js
@@ -4,7 +4,7 @@ var utils = require('./utils');
 
 /**
  * Simple facade for consuming a REST API endpoint.
- * @external Rest
+ * @external RestClient
  * @see https://github.com/ngonzalvez/rest-facade
  */
 
@@ -20,7 +20,15 @@ var utils = require('./utils');
  */
 var DeviceCredentialsManager = function (options) {
   if (options === null || typeof options !== 'object') {
-    throw new ArgumentError('Must provide credential options');
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
   }
 
   /**
diff --git a/src/EmailProviderManager.js b/src/EmailProviderManager.js
index 486b3482b..865b66185 100644
--- a/src/EmailProviderManager.js
+++ b/src/EmailProviderManager.js
@@ -23,6 +23,14 @@ var EmailProviderManager = function (options) {
     throw new ArgumentError('Must provide client options');h
   }
 
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   /**
    * Options object for the Rest Client instance.
    *
@@ -58,7 +66,7 @@ utils.wrapPropertyMethod(EmailProviderManager, 'configure', 'resource.create');
  * @memberOf  EmailProviderManager
  * @return  {Promise}               Returns a promise if no callback is received.
  */
-utils.wrapPropertyMethod(EmailProviderManager, 'get', 'resource.get');
+utils.wrapPropertyMethod(EmailProviderManager, 'get', 'resource.getAll');
 
 
 /**
diff --git a/src/JobsManager.js b/src/JobsManager.js
index 9e9c3bdd8..88fc0f4d1 100644
--- a/src/JobsManager.js
+++ b/src/JobsManager.js
@@ -1,5 +1,6 @@
 var request = require('superagent');
 var extend = require('util')._extend;
+var Promise = require('bluebird');
 
 var RestClient = require('rest-facade').Client;
 var ArgumentError = require('./exceptions').ArgumentError;
@@ -13,6 +14,18 @@ var ArgumentError = require('./exceptions').ArgumentError;
  * @param {Object}    options       Manager options.
  */
 var JobsManager = function (options){
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide client options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   var clientOptions = {
     headers: options.headers,
     query: { repeatParams: false }
@@ -72,7 +85,7 @@ JobsManager.prototype.importUsers = function (data, cb) {
   var promise = new Promise(function (resolve, reject) {
     var req = request
       .post(options.baseUrl + '/jobs/users-imports')
-      .send('connection_id', data.connectionId)
+      .field('connection_id', data.connection_id)
       .attach('users', data.users)
 
     for (var name in headers) {
@@ -80,10 +93,10 @@ JobsManager.prototype.importUsers = function (data, cb) {
     }
 
     req.end(function (err, res) {
-        if (err) reject(err);
+      if (err) reject(err);
 
-        resolve(res);
-      });
+      resolve(res);
+    });
   });
 
   // Don't return a promise if a callback was given.
@@ -107,7 +120,7 @@ JobsManager.prototype.importUsers = function (data, cb) {
  * @return  {Promise}
  */
 JobsManager.prototype.verifyEmail = function (data, cb) {
-  if (!data.user_id || typeof data.user_id === 'string') {
+  if (!data.user_id || typeof data.user_id !== 'string') {
     throw new ArgumentError('Must specify a user ID');
   }
 
diff --git a/src/RulesManager.js b/src/RulesManager.js
index d97041125..351fe210c 100644
--- a/src/RulesManager.js
+++ b/src/RulesManager.js
@@ -18,7 +18,15 @@ var utils = require('./utils');
  */
 var RulesManager = function (options) {
   if (options === null || typeof options !== 'object') {
-    throw new ArgumentError('Must provide client options');
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
   }
 
   /**
diff --git a/src/StatsManager.js b/src/StatsManager.js
index df102cbdb..500e93998 100644
--- a/src/StatsManager.js
+++ b/src/StatsManager.js
@@ -1,4 +1,5 @@
 var RestClient = require('rest-facade').Client;
+var ArgumentError = require('./exceptions').ArgumentError;
 
 
 /**
@@ -7,6 +8,18 @@ var RestClient = require('rest-facade').Client;
  * @constructor
  */
 var StatsManager = function (options){
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   var clientOptions = {
     headers: options.headers,
     query: { repeatParams: false }
diff --git a/src/TenantManager.js b/src/TenantManager.js
index 4dcba65e0..e2f91659e 100644
--- a/src/TenantManager.js
+++ b/src/TenantManager.js
@@ -1,4 +1,5 @@
 var RestClient = require('rest-facade').Client;
+var ArgumentError = require('./exceptions').ArgumentError;
 
 
 /**
@@ -7,6 +8,18 @@ var RestClient = require('rest-facade').Client;
  * @constructor
  */
 var TenantManager = function (options){
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   var clientOptions = {
     headers: options.headers,
     query: { repeatParams: false }
diff --git a/src/TicketsManager.js b/src/TicketsManager.js
index 6d9d2a7f2..96bbeeb7d 100644
--- a/src/TicketsManager.js
+++ b/src/TicketsManager.js
@@ -1,4 +1,5 @@
 var RestClient = require('rest-facade').Client;
+var ArgumentError = require('./exceptions').ArgumentError;
 
 
 /**
@@ -7,6 +8,18 @@ var RestClient = require('rest-facade').Client;
  * @constructor
  */
 var TicketsManager = function (options){
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   var clientOptions = {
     headers: options.headers,
     query: { repeatParams: false }
diff --git a/src/UsersManager.js b/src/UsersManager.js
index f1c6ff085..7e344f74f 100644
--- a/src/UsersManager.js
+++ b/src/UsersManager.js
@@ -8,6 +8,18 @@ var ArgumentError = require('./exceptions').ArgumentError;
  * @constructor
  */
 var UsersManager = function (options){
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
   var clientOptions = {
     headers: options.headers,
     query: { repeatParams: false }
@@ -28,7 +40,7 @@ var UsersManager = function (options){
    *
    * @type {external:RestClient}
    */
-  this.identities = new RestClient(options.baseUrl + '/users/:id/identities/:provider/:userId', clientOptions);
+  this.identities = new RestClient(options.baseUrl + '/users/:id/identities/:provider/:user_id', clientOptions);
 };
 
 
@@ -88,6 +100,50 @@ UsersManager.prototype.update = function () {
 };
 
 
+/**
+ * Update the user metadata.
+ *
+ * @method
+ * @param   {any}       params  The user id.
+ * @param   {Object}    data    New user metadata.
+ * @param   {Function}  [cb]    Callback function
+ * @return  {Promise}
+ */
+UsersManager.prototype.updateUserMetadata = function (params, metadata, cb) {
+  var data = {
+    user_metadata: metadata
+  };
+
+  if (cb && cb instanceof Function) {
+    return this.users.patch(params, metadata, cb);
+  }
+
+  return this.users.patch(params, metadata);
+};
+
+
+/**
+ * Update the user metadata.
+ *
+ * @method
+ * @param   {any}       params  The user id.
+ * @param   {Object}    data    New user metadata.
+ * @param   {Function}  [cb]    Callback function
+ * @return  {Promise}
+ */
+UsersManager.prototype.updateAppMetadata = function (params, metadata, cb) {
+  var data = {
+    app_metadata: metadata
+  };
+
+  if (cb && cb instanceof Function) {
+    return this.users.patch(params, data, cb);
+  }
+
+  return this.users.patch(params, data);
+};
+
+
 /**
  * Delete a user by its id.
  *
@@ -178,15 +234,15 @@ UsersManager.prototype.unlink = function (params, cb) {
   params = params || {};
 
   if (!params.id || typeof params.id !== 'string') {
-    throw new ArgumentError('The id cannot be null or undefined');
+    throw new ArgumentError('id field is required');
   }
 
-  if (!params.userId || typeof params.userId !== 'string') {
-    throw new ArgumentError('A userId is required');
+  if (!params.user_id || typeof params.user_id !== 'string') {
+    throw new ArgumentError('user_id field is required');
   }
 
   if (!params.provider || typeof params.provider !== 'string') {
-    throw new ArgumentError('You must specify a provider');
+    throw new ArgumentError('provider field is required');
   }
 
   if (cb && cb instanceof Function) {
diff --git a/src/utils.js b/src/utils.js
index 780824a21..09fe2cffc 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -31,7 +31,6 @@ utils.wrapPropertyMethod = function (Parent, name, propertyMethod) {
   Object.defineProperty(Parent.prototype, name, {
     enumerable: false,
     get: function () {
-
       return this[property][method].bind(this[property]);
     }
   });


Test patch:
diff --git a/test/client.tests.js b/test/client.tests.js
index 8c479bf80..b4f5d9c2a 100644
--- a/test/client.tests.js
+++ b/test/client.tests.js
@@ -68,7 +68,7 @@ describe('ClientsManager', function () {
       this.request = nock(API_URL)
         .get('/clients')
         .reply(200);
-    })
+    });
 
     it('should accept a callback', function (done) {
       this.clients.getAll(function () {
diff --git a/test/connections.tests.js b/test/connections.tests.js
new file mode 100644
index 000000000..0696f2da0
--- /dev/null
+++ b/test/connections.tests.js
@@ -0,0 +1,607 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var ConnectionsManager = require('../src/ConnectionsManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('ConnectionsManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.connections = new ConnectionsManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['getAll', 'get', 'create', 'update', 'delete'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.connections[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(ConnectionsManager)
+        .to.throw(ArgumentError, 'Must provide client options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = ConnectionsManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = ConnectionsManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getAll', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/connections')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .connections
+        .getAll(function () {
+        done();
+      });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .connections
+        .getAll()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections')
+        .reply(500);
+
+      this
+        .connections
+        .getAll()
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/connections')
+        .reply(200, data);
+
+      this
+        .connections
+        .getAll()
+        .then(function (connections) {
+          expect(connections)
+            .to.be.an.instanceOf(Array);
+
+          expect(connections.length)
+            .to.equal(data.length);
+
+          expect(connections[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/connections', function (done) {
+      var request = this.request;
+
+      this
+        .connections
+        .getAll()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .connections
+        .getAll()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections')
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .connections
+        .getAll({ includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#get', function () {
+    var params = { id: 5 };
+    var data = {
+      id: params.id,
+      name: 'Test connection'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/connections/' + data.id)
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .connections
+        .get(params, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .connections
+        .get(params)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections/' + params.id)
+        .reply(500);
+
+      this
+        .connections
+        .get()
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections/' + params.id)
+        .reply(200, data);
+
+      this
+        .connections
+        .get(params)
+        .then(function (connection) {
+          expect(connection.id)
+            .to.equal(data.id);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/connections/:id', function (done) {
+      var request = this.request;
+
+      this
+        .connections
+        .get(params)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .connections
+        .getAll()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/connections')
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .connections
+        .getAll({ includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#create', function () {
+    var data = {
+      name: 'Test connection',
+      options: {}
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/connections')
+        .reply(200, data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .connections
+        .create(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .connections
+        .create(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/connections')
+        .reply(500);
+
+      this
+        .connections
+        .create(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/connections', function (done) {
+      var request = this.request;
+
+      this
+        .connections
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/connections', data)
+        .reply(200);
+
+      this
+        .connections
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/connections')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .connections
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#update', function () {
+    var params = { id: 5 };
+    var data = {
+      id: 5,
+      name: 'Test connection',
+      options: {}
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .patch('/connections/' + data.id)
+        .reply(200, data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .connections
+        .update(params, data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .connections
+        .update(params, data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/connections/' + data.id)
+        .reply(500);
+
+      this
+        .connections
+        .update(params, data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist
+            .to.be.an.instanceOf(Error);
+
+          done();
+        });
+    });
+
+
+    it('should perform a PATCH request to /api/v2/connections/:id', function (done) {
+      var request = this.request;
+
+      this
+        .connections
+        .update(params, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/connections/' + data.id, data)
+        .reply(200);
+
+      this
+        .connections
+        .update(params, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/connections/' + data.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .connections
+        .update(params, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#delete', function () {
+    var id = 5;
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete('/connections/' + id)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .connections
+        .delete({ id: id }, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .connections
+        .delete({ id: id })
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a DELETE request to /connections/' + id, function (done) {
+      var request = this.request;
+
+      this
+        .connections
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/connections/' + id)
+        .reply(500);
+
+      this
+        .connections
+        .delete({ id: id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/connections/' + id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .connections
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/data/users.json b/test/data/users.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/test/data/users.json
@@ -0,0 +1,1 @@
+[]
diff --git a/test/device-credentials.tests.js b/test/device-credentials.tests.js
new file mode 100644
index 000000000..92a5690ba
--- /dev/null
+++ b/test/device-credentials.tests.js
@@ -0,0 +1,367 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var DeviceCredentialsManager = require('../src/DeviceCredentialsManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('DeviceCredentialsManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.credentials = new DeviceCredentialsManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['createPublicKey', 'getAll', 'delete'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.credentials[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(DeviceCredentialsManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = DeviceCredentialsManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = DeviceCredentialsManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getAll', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/device-credentials')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .credentials
+        .getAll(function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .credentials
+        .getAll()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/device-credentials')
+        .reply(500);
+
+      this
+        .credentials
+        .getAll()
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/device-credentials')
+        .reply(200, data);
+
+      this
+        .credentials
+        .getAll()
+        .then(function (credentials) {
+          expect(credentials)
+            .to.be.an.instanceOf(Array);
+
+          expect(credentials.length)
+            .to.equal(data.length);
+
+          expect(credentials[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/device-credentials', function (done) {
+      var request = this.request;
+
+      this
+        .credentials
+        .getAll()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/device-credentials')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .credentials
+        .getAll()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var params = {
+        includeFields: true,
+        fields: 'test'
+      };
+      var request = nock(API_URL)
+        .get('/device-credentials')
+        .query(params)
+        .reply(200)
+
+      this
+        .credentials
+        .getAll(params)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#createPublicKey', function () {
+    var data = {
+      'device_name': 'Sample device',
+      type: 'public_key',
+      'user_id': 'github|1234'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/device-credentials')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .credentials
+        .createPublicKey(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .credentials
+        .createPublicKey(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/device-credentials')
+        .reply(500);
+
+      this
+        .credentials
+        .createPublicKey(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/device-credentials', function (done) {
+      var request = this.request;
+
+      this
+        .credentials
+        .createPublicKey(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/device-credentials', data)
+        .reply(200);
+
+      this
+        .credentials
+        .createPublicKey(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/device-credentials')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .credentials
+        .createPublicKey(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#delete', function () {
+    var id = 5;
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete('/device-credentials/' + id)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .credentials
+        .delete({ id: id }, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .credentials
+        .delete({ id: id })
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a delete request to /device-credentials/' + id, function (done) {
+      var request = this.request;
+
+      this
+        .credentials
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/device-credentials/' + id)
+        .reply(500);
+
+      this
+        .credentials
+        .delete({ id: id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/device-credentials/' + id)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .credentials
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/email-provider.tests.js b/test/email-provider.tests.js
new file mode 100644
index 000000000..2071d99e0
--- /dev/null
+++ b/test/email-provider.tests.js
@@ -0,0 +1,475 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var EmailProviderManager = require('../src/EmailProviderManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('EmailProviderManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.emailProvider = new EmailProviderManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['configure', 'get', 'update', 'delete'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.emailProvider[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(EmailProviderManager)
+        .to.throw(ArgumentError, 'Must provide client options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = EmailProviderManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = EmailProviderManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#get', function () {
+    var data = {
+      name: 'Test provider',
+      options: {}
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/emails/provider')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .emailProvider
+        .get(function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .emailProvider
+        .get()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/emails/provider')
+        .reply(500);
+
+      this
+        .emailProvider
+        .get()
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/emails/provider')
+        .reply(200, data);
+
+      this
+        .emailProvider
+        .get()
+        .then(function (provider) {
+          expect(provider.id)
+            .to.equal(data.id);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/emails/provider', function (done) {
+      var request = this.request;
+
+      this
+        .emailProvider
+        .get()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/emails/provider')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .emailProvider
+        .get()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var params = {
+        include_fields: true,
+        fields: 'test'
+      };
+
+      var request = nock(API_URL)
+        .get('/emails/provider')
+        .query(params)
+        .reply(200)
+
+      this
+        .emailProvider
+        .get(params)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#configure', function () {
+    var data = {
+      name: 'Test provider',
+      credentials: {}
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/emails/provider')
+        .reply(200, data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .emailProvider
+        .configure(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .emailProvider
+        .configure(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/emails/provider')
+        .reply(500);
+
+      this
+        .emailProvider
+        .configure(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/emails/provider', function (done) {
+      var request = this.request;
+
+      this
+        .emailProvider
+        .configure(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/emails/provider', data)
+        .reply(200);
+
+      this
+        .emailProvider
+        .configure(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/emails/provider')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .emailProvider
+        .configure(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#update', function () {
+    var data = {
+      name: 'Test provider',
+      credentials: {}
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .patch('/emails/provider')
+        .reply(200, data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .emailProvider
+        .update({}, data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .emailProvider
+        .update({}, data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/emails/provider/' + data.id)
+        .reply(500);
+
+      this
+        .emailProvider
+        .update({}, data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist
+            .to.be.an.instanceOf(Error);
+
+          done();
+        });
+    });
+
+
+    it('should perform a PATCH request to /api/v2/emails/provider', function (done) {
+      var request = this.request;
+
+      this
+        .emailProvider
+        .update({}, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/emails/provider', data)
+        .reply(200);
+
+      this
+        .emailProvider
+        .update({}, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/emails/provider')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .emailProvider
+        .update({}, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#delete', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete('/emails/provider')
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .emailProvider
+        .delete({}, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .emailProvider
+        .delete()
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a DELETE request to /emails/provider', function (done) {
+      var request = this.request;
+
+      this
+        .emailProvider
+        .delete()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/emails/provider')
+        .reply(500);
+
+      this
+        .emailProvider
+        .delete()
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/emails/provider')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .emailProvider
+        .delete()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/jobs.tests.js b/test/jobs.tests.js
new file mode 100644
index 000000000..6b8b12912
--- /dev/null
+++ b/test/jobs.tests.js
@@ -0,0 +1,438 @@
+var path = require('path');
+var expect = require('chai').expect;
+var nock = require('nock');
+var extractParts = require('./utils').extractParts;
+
+var JobsManager = require('../src/JobsManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('JobsManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.id = 'testJob';
+    this.jobs = new JobsManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['verifyEmail', 'importUsers', 'get'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.jobs[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(JobsManager)
+        .to.throw(ArgumentError, 'Must provide client options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = JobsManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = JobsManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#get', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/jobs/' + this.id)
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .jobs
+        .get({ id: this.id }, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .jobs
+        .get({ id: this.id })
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/jobs/' + this.id)
+        .reply(500);
+
+      this
+        .jobs
+        .get({ id: this.id })
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/jobs/' + this.id)
+        .reply(200, data);
+
+      this
+        .jobs
+        .get({ id: this.id })
+        .then(function (blacklistedTokens) {
+          expect(blacklistedTokens)
+            .to.be.an.instanceOf(Array);
+
+          expect(blacklistedTokens.length)
+            .to.equal(data.length);
+
+          expect(blacklistedTokens[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/jobs', function (done) {
+      var request = this.request;
+
+      this
+        .jobs
+        .get({ id: this.id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/jobs/' + this.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .jobs
+        .get({ id: this.id })
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/jobs/' + this.id)
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .jobs
+        .get({ id: this.id, includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+
+  describe('#importUsers', function () {
+    var data = {
+      users: path.join(__dirname, './data/users.json'),
+      connection_id: 'con_test'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .jobs
+        .importUsers(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .jobs
+        .importUsers(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .reply(500);
+
+      this
+        .jobs
+        .importUsers(data)
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/jobs/users-imports', function (done) {
+      var request = this.request;
+
+      this
+        .jobs
+        .importUsers(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should be a multipart request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .matchHeader('Content-Type', function (header) {
+          return header.indexOf('multipart/form-data') === 0;
+        })
+        .reply(200);
+
+      this
+        .jobs
+        .importUsers(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should have two parts: connection_id and users file', function (done) {
+      nock.cleanAll();
+      var boundary = null;
+
+      var request = nock(API_URL)
+        .matchHeader('Content-Type', function (header) {
+          boundary = '--' + header.match(/boundary=([^\n]*)/)[1];
+
+          return true;
+        })
+        .post('/jobs/users-imports', function (body) {
+          var parts = extractParts(body, boundary);
+
+          // Validate the connection id.
+          expect(parts.connection_id)
+            .to.exist
+            .to.be.a('string')
+            .to.equal(data.connection_id);
+
+          // Validate the content type of the users JSON.
+          expect(parts.users)
+            .to.exist
+            .to.be.a('string')
+            .to.contain('Content-Type: application/json');
+
+          // Validate the content of the users JSON.
+          expect(parts.users.slice(-2))
+            .to.equal('[]');
+
+          return true;
+        })
+        .reply(200);
+
+      this
+        .jobs
+        .importUsers(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .jobs
+        .importUsers(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#verifyEmail', function () {
+    var data = {
+      user_id: 'github|12345',
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/jobs/verification-email')
+        .reply(200, data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .jobs
+        .verifyEmail(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .jobs
+        .verifyEmail(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/verification-email')
+        .reply(500);
+
+      this
+        .jobs
+        .verifyEmail(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/jobs/verification-email', function (done) {
+      var request = this.request;
+
+      this
+        .jobs
+        .verifyEmail(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/verification-email', data)
+        .reply(200);
+
+      this
+        .jobs
+        .verifyEmail(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/verification-email')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .jobs
+        .verifyEmail(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/rules.tests.js b/test/rules.tests.js
new file mode 100644
index 000000000..2db0a9814
--- /dev/null
+++ b/test/rules.tests.js
@@ -0,0 +1,538 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var RulesManager = require('../src/RulesManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('RulesManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.rules = new RulesManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['get', 'getAll', 'create', 'update', 'delete'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.rules[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(RulesManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = RulesManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = RulesManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getAll', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/rules')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .rules
+        .getAll(function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .rules
+        .getAll()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/rules')
+        .reply(500);
+
+      this
+        .rules
+        .getAll()
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/rules')
+        .reply(200, data);
+
+      this
+        .rules
+        .getAll()
+        .then(function (credentials) {
+          expect(credentials)
+            .to.be.an.instanceOf(Array);
+
+          expect(credentials.length)
+            .to.equal(data.length);
+
+          expect(credentials[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/rules', function (done) {
+      var request = this.request;
+
+      this
+        .rules
+        .getAll()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/rules')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .rules
+        .getAll()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var params = {
+        includeFields: true,
+        fields: 'test'
+      };
+      var request = nock(API_URL)
+        .get('/rules')
+        .query(params)
+        .reply(200)
+
+      this
+        .rules
+        .getAll(params)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#get', function () {
+    beforeEach(function () {
+      this.data = {
+        id: 5,
+        name: 'Test rule',
+        enabled: true,
+        script: 'function (user, contest, callback) { console.log(\'Test\'); }',
+        stage: 'login_success'
+      };
+
+      this.request = nock(API_URL)
+        .get('/rules/' + this.data.id)
+        .reply(200, this.data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      var params = { id: this.data.id };
+
+      this
+        .rules
+        .get(params, done.bind(null, null));
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .rules
+        .get({ id: this.data.id })
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should perform a POST request to /api/v2/rules/5', function (done) {
+      var request = this.request;
+
+      this
+        .rules
+        .get({ id: this.data.id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/rules/' + this.data.id)
+        .reply(500);
+
+      this
+        .rules
+        .get({ id: this.data.id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/rules/' + this.data.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .rules
+        .get({ id: this.data.id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#create', function () {
+    var data = {
+      id: 5,
+      name: 'Test rule',
+      enabled: true,
+      script: 'function (user, contest, callback) { console.log(\'Test\'); }',
+      stage: 'login_success'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/rules')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .rules
+        .create(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .rules
+        .create(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/rules')
+        .reply(500);
+
+      this
+        .rules
+        .create(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/rules', function (done) {
+      var request = this.request;
+
+      this
+        .rules
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/rules', data)
+        .reply(200);
+
+      this
+        .rules
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/rules')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .rules
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#update', function () {
+    beforeEach(function () {
+      this.data = { id: 5 };
+
+      this.request = nock(API_URL)
+        .patch('/rules/' + this.data.id)
+        .reply(200, this.data);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .rules
+        .update({ id: 5 }, {}, done.bind(null, null));
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .rules
+        .update({ id: 5 }, {})
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should perform a PATCH request to /api/v2/rules/5', function (done) {
+      var request = this.request;
+
+      this
+        .rules
+        .update({ id: 5 }, {})
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the new data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/rules/' + this.data.id, this.data)
+        .reply(200);
+
+      this
+        .rules
+        .update({ id: 5 }, this.data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/rules/' + this.data.id)
+        .reply(500);
+
+      this
+        .rules
+        .update({ id: this.data.id }, this.data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#delete', function () {
+    var id = 5;
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete('/rules/' + id)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .rules
+        .delete({ id: id }, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .rules
+        .delete({ id: id })
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a delete request to /rules/' + id, function (done) {
+      var request = this.request;
+
+      this
+        .rules
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/rules/' + id)
+        .reply(500);
+
+      this
+        .rules
+        .delete({ id: id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/rules/' + id)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .rules
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/stats.tests.js b/test/stats.tests.js
new file mode 100644
index 000000000..c0848b103
--- /dev/null
+++ b/test/stats.tests.js
@@ -0,0 +1,272 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var StatsManager = require('../src/StatsManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com';
+
+
+describe('StatsManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.stats = new StatsManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['getActiveUsersCount', 'getDaily'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.stats[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(StatsManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var client = StatsManager.bind(null, {});
+
+      expect(client)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var client = StatsManager.bind(null, { baseUrl: '' });
+
+      expect(client)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getDaily', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/stats/daily')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .stats
+        .getDaily({}, function () {
+        done();
+      });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .stats
+        .getDaily()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/daily')
+        .reply(500);
+
+      this
+        .stats
+        .getDaily()
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/stats/daily')
+        .reply(200, data);
+
+      this
+        .stats
+        .getDaily()
+        .then(function (blacklistedTokens) {
+          expect(blacklistedTokens)
+            .to.be.an.instanceOf(Array);
+
+          expect(blacklistedTokens.length)
+            .to.equal(data.length);
+
+          expect(blacklistedTokens[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/stats/daily', function (done) {
+      var request = this.request;
+
+      this
+        .stats
+        .getDaily()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/daily')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .stats
+        .getDaily()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/daily')
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .stats
+        .getDaily({ includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+
+  describe('#getActiveUsersCount', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/stats/active-users')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .stats
+        .getActiveUsersCount(function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .stats
+        .getActiveUsersCount()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/active-users')
+        .reply(500);
+
+      this
+        .stats
+        .getActiveUsersCount()
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/stats/active-users', function (done) {
+      var request = this.request;
+
+      this
+        .stats
+        .getActiveUsersCount()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the token data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/active-users')
+        .reply(200);
+
+      this
+        .stats
+        .getActiveUsersCount()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/stats/active-users')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .stats
+        .getActiveUsersCount()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/tenant.tests.js b/test/tenant.tests.js
new file mode 100644
index 000000000..fd366c8db
--- /dev/null
+++ b/test/tenant.tests.js
@@ -0,0 +1,276 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var TenantManager = require('../src/TenantManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenants.auth0.com';
+
+
+describe('TenantManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.tenant = new TenantManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['updateSettings', 'getSettings'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.tenant[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(TenantManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var manager = TenantManager.bind(null, {});
+
+      expect(manager)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var manager = TenantManager.bind(null, { baseUrl: '' });
+
+      expect(manager)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getSettings', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/tenants/settings')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .tenant
+        .getSettings(function () {
+        done();
+      });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .tenant
+        .getSettings()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/tenants/settings')
+        .reply(500);
+
+      this
+        .tenant
+        .getSettings()
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/tenants/settings')
+        .reply(200, data);
+
+      this
+        .tenant
+        .getSettings()
+        .then(function (blacklistedTokens) {
+          expect(blacklistedTokens)
+            .to.be.an.instanceOf(Array);
+
+          expect(blacklistedTokens.length)
+            .to.equal(data.length);
+
+          expect(blacklistedTokens[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/tenants/settings', function (done) {
+      var request = this.request;
+
+      this
+        .tenant
+        .getSettings()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/tenants/settings')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .tenant
+        .getSettings()
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/tenants/settings')
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .tenant
+        .getSettings({ includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+
+  describe('#updateSettings', function () {
+    var data = {
+      friendly_name: 'Test name'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .patch('/tenants/settings')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .tenant
+        .updateSettings(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .tenant
+        .updateSettings(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/tenants/settings')
+        .reply(500);
+
+      this
+        .tenant
+        .updateSettings(data)
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should perform a PATCH request to /api/v2/tenants/settings', function (done) {
+      var request = this.request;
+
+      this
+        .tenant
+        .updateSettings(data)
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/tenants/settings', data)
+        .reply(200);
+
+      this
+        .tenant
+        .updateSettings(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/tenants/settings')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .tenant
+        .updateSettings(data)
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/tickets.tests.js b/test/tickets.tests.js
new file mode 100644
index 000000000..0936ce7b7
--- /dev/null
+++ b/test/tickets.tests.js
@@ -0,0 +1,262 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var TicketsManager = require('../src/TicketsManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenants.auth0.com';
+
+
+describe('TicketsManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.tickets = new TicketsManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = ['changePassword', 'verifyEmail'];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.tickets[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(TicketsManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var manager = TicketsManager.bind(null, {});
+
+      expect(manager)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var manager = TicketsManager.bind(null, { baseUrl: '' });
+
+      expect(manager)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#verifyEmail', function () {
+    var data = {
+      result_url: "http://myapp.com/callback",
+      user_id: ""
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/tickets/email-verification')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .tickets
+        .verifyEmail(data, function () {
+        done();
+      });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .tickets
+        .verifyEmail(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('tickets/email-verification')
+        .reply(500);
+
+      this
+        .tickets
+        .verifyEmail(data)
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2tickets/email-verification', function (done) {
+      var request = this.request;
+
+      this
+        .tickets
+        .verifyEmail(data)
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/tickets/email-verification')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .tickets
+        .verifyEmail({})
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/tickets/email-verification')
+        .query({
+          include_fields: true,
+          fields: 'test'
+        })
+        .reply(200)
+
+      this
+        .tickets
+        .verifyEmail({ includeFields: true, fields: 'test' })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#changePassword', function () {
+    var data = {
+      result_url: "http://myapp.com/callback",
+      user_id: "",
+      new_password: "secret",
+      connection_id: "con_0000000000000001",
+      email: ""
+    };
+
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/tickets/password-change')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .tickets
+        .changePassword(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .tickets
+        .changePassword(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/tickets/email-verification')
+        .reply(500);
+
+      this
+        .tickets
+        .changePassword(data)
+        .catch(function (err) {
+          expect(err).to.exist;
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2tickets/email-verification', function (done) {
+      var request = this.request;
+
+      this
+        .tickets
+        .changePassword(data)
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/tickets/password-change', data)
+        .reply(200);
+
+      this
+        .tickets
+        .changePassword(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/tickets/password-change')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200)
+
+      this
+        .tickets
+        .changePassword(data)
+        .then(function () {
+          expect(request.isDone()).to.be.true;
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/users.tests.js b/test/users.tests.js
new file mode 100644
index 000000000..8ca382c2e
--- /dev/null
+++ b/test/users.tests.js
@@ -0,0 +1,833 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var UsersManager = require('../src/UsersManager');
+var ArgumentError = require('../src/exceptions').ArgumentError;
+
+var API_URL = 'https://tenant.auth0.com/api/v2';
+
+
+describe('UsersManager', function () {
+  before(function () {
+    this.token = 'TOKEN';
+    this.users = new UsersManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+
+  describe('instance', function () {
+    var methods = [
+      'get',
+      'getAll',
+      'create',
+      'update',
+      'delete',
+      'deleteAll',
+      'unlink',
+      'link',
+      'deleteMultifactorProvider'
+    ];
+
+    methods.forEach(function (method) {
+      it('should have a ' + method + ' method', function () {
+        expect(this.users[method])
+          .to.exist
+          .to.be.an.instanceOf(Function);
+      })
+    });
+  });
+
+
+  describe('#constructor', function () {
+    it('should error when no options are provided', function () {
+      expect(UsersManager)
+        .to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+
+    it('should throw an error when no base URL is provided', function () {
+      var manager = UsersManager.bind(null, {});
+
+      expect(manager)
+        .to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+
+    it('should throw an error when the base URL is invalid', function () {
+      var manager = UsersManager.bind(null, { baseUrl: '' });
+
+      expect(manager)
+        .to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+
+  describe('#getAll', function () {
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .get('/users')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .getAll(function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .users
+        .getAll()
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/users')
+        .reply(500);
+
+      this
+        .users
+        .getAll()
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should pass the body of the response to the "then" handler', function (done) {
+      nock.cleanAll();
+
+      var data = [{ test: true }];
+      var request = nock(API_URL)
+        .get('/users')
+        .reply(200, data);
+
+      this
+        .users
+        .getAll()
+        .then(function (users) {
+          expect(users)
+            .to.be.an.instanceOf(Array);
+
+          expect(users.length)
+            .to.equal(data.length);
+
+          expect(users[0].test)
+            .to.equal(data[0].test);
+
+          done();
+        });
+    });
+
+
+    it('should perform a GET request to /api/v2/users', function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .getAll()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/users')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .getAll()
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the parameters in the query-string', function (done) {
+      nock.cleanAll();
+
+      var params = {
+        includeFields: true,
+        fields: 'test'
+      };
+      var request = nock(API_URL)
+        .get('/users')
+        .query(params)
+        .reply(200);
+
+      this
+        .users
+        .getAll(params)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#get', function () {
+    beforeEach(function () {
+      this.data = {
+        id: 5,
+        name: 'Test rule',
+        enabled: true,
+        script: 'function (user, contest, callback) { console.log(\'Test\'); }',
+        stage: 'login_success'
+      };
+
+      this.request = nock(API_URL)
+        .get('/users/' + this.data.id)
+        .reply(200, this.data);
+    })
+
+
+    it('should accept a callback', function (done) {
+      var params = { id: this.data.id };
+
+      this
+        .users
+        .get(params, done.bind(null, null));
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .users
+        .get({ id: this.data.id })
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should perform a POST request to /api/v2/users/5', function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .get({ id: this.data.id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/users/' + this.data.id)
+        .reply(500);
+
+      this
+        .users
+        .get({ id: this.data.id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/users/' + this.data.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .get({ id: this.data.id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#create', function () {
+    var data = {
+      id: 5,
+      name: 'Test rule',
+      enabled: true,
+      script: 'function (user, contest, callback) { console.log(\'Test\'); }',
+      stage: 'login_success'
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/users')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .create(data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .users
+        .create(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users')
+        .reply(500);
+
+      this
+        .users
+        .create(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/users', function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users', data)
+        .reply(200);
+
+      this
+        .users
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .create(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#update', function () {
+    beforeEach(function () {
+      this.data = { id: 5 };
+
+      this.request = nock(API_URL)
+        .patch('/users/' + this.data.id)
+        .reply(200, this.data);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .update({ id: 5 }, {}, done.bind(null, null));
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .users
+        .update({ id: 5 }, {})
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should perform a PATCH request to /api/v2/users/5', function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .update({ id: 5 }, {})
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the new data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/users/' + this.data.id, this.data)
+        .reply(200);
+
+      this
+        .users
+        .update({ id: 5 }, this.data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .patch('/users/' + this.data.id)
+        .reply(500);
+
+      this
+        .users
+        .update({ id: this.data.id }, this.data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#delete', function () {
+    var id = 5;
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete('/users/' + id)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .delete({ id: id }, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .users
+        .delete({ id: id })
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a delete request to /users/' + id, function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/users/' + id)
+        .reply(500);
+
+      this
+        .users
+        .delete({ id: id })
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/users/' + id)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .delete({ id: id })
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#link', function () {
+    var userId = 'USER_ID';
+    var data = {
+      provider: "twitter",
+      user_id: "191919191919191",
+    };
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .post('/users/' + userId + '/identities')
+        .reply(200);
+    })
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .link(userId, data, function () {
+          done();
+        });
+    });
+
+
+    it('should return a promise if no callback is given', function (done) {
+      this
+        .users
+        .link(userId, data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + userId + '/identities')
+        .reply(500);
+
+      this
+        .users
+        .link(userId, data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should perform a POST request to /api/v2/users', function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .link(userId, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass the data in the body of the request', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + userId + '/identities', data)
+        .reply(200);
+
+      this
+        .users
+        .link(userId, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the Authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + userId + '/identities')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .link(userId, data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#unlink', function () {
+    var data = {
+      id: 'u1',
+      user_id: 'u2',
+      provider: 'auth0'
+    };
+    var url = (
+      '/users/' + data.id + '/identities/' + data.provider + '/' + data.user_id
+    );
+
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete(url)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .unlink(data, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .users
+        .unlink(data)
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a DELETE request to ' + url, function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .unlink(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete(url)
+        .reply(500);
+
+      this
+        .users
+        .unlink(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete(url)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .unlink(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+
+  describe('#deleteMultifactorProvider', function () {
+    var data = {
+      id: 'u1',
+      provider: 'auth0'
+    };
+    var url = '/users/' + data.id + '/multifactor/' + data.provider;
+
+
+    beforeEach(function () {
+      this.request = nock(API_URL)
+        .delete(url)
+        .reply(200);
+    });
+
+
+    it('should accept a callback', function (done) {
+      this
+        .users
+        .deleteMultifactorProvider(data, done.bind(null, null));
+    });
+
+
+    it('should return a promise when no callback is given', function (done) {
+      this
+        .users
+        .deleteMultifactorProvider(data)
+        .then(done.bind(null, null));
+    });
+
+
+    it('should perform a DELETE request to ' + url, function (done) {
+      var request = this.request;
+
+      this
+        .users
+        .deleteMultifactorProvider(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+
+
+    it('should pass any errors to the promise catch handler', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete(url)
+        .reply(500);
+
+      this
+        .users
+        .deleteMultifactorProvider(data)
+        .catch(function (err) {
+          expect(err)
+            .to.exist;
+
+          done();
+        });
+    });
+
+
+    it('should include the token in the authorization header', function (done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete(url)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this
+        .users
+        .deleteMultifactorProvider(data)
+        .then(function () {
+          expect(request.isDone())
+            .to.be.true;
+
+          done();
+        });
+    });
+  });
+
+});
diff --git a/test/utils/extractParts.js b/test/utils/extractParts.js
new file mode 100644
index 000000000..5d0ba524d
--- /dev/null
+++ b/test/utils/extractParts.js
@@ -0,0 +1,20 @@
+module.exports = function (body, boundary) {
+  var partRegexp = new RegExp(boundary + '[-]{2}|' + boundary, 'g');
+  var parts = {};
+  var name;
+  var value;
+
+  body
+    .split(partRegexp)
+    .forEach(function (part) {
+      // Ignore empty strings in the array.
+      if (part.length === 0) return;
+
+      name = part.match(/name="([^"]*)"/)[1];
+      value = part.split('"' + name + '"')[1];
+
+      parts[name] = value;
+    });
+
+  return parts;
+};
diff --git a/test/utils/index.js b/test/utils/index.js
new file mode 100644
index 000000000..6c96a1e77
--- /dev/null
+++ b/test/utils/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+  extractParts: require('./extractParts')
+};