Repository: auth0/node-auth0
Pull request #440 (2019-12-10 19:57:09)

Support for Hooks API
### Changes

Adds support for Hooks API:
- Adds HooksManager class
- Adds support for hook secrets
- Adds wrappers: getHook, getHooks, createHook, updateHook, deleteHook, getHookSecrets, addHookSecrets, updateHookSecrets, removeHookSecrets


### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. 

- [x] This change adds unit test coverage
- [ ] This change adds integration test coverage

### Checklist

- [x] All existing and new tests complete without errors



Patch:
diff --git a/src/management/HooksManager.js b/src/management/HooksManager.js
new file mode 100644
index 000000000..53276ac03
--- /dev/null
+++ b/src/management/HooksManager.js
@@ -0,0 +1,368 @@
+var ArgumentError = require('rest-facade').ArgumentError;
+var utils = require('../utils');
+var Auth0RestClient = require('../Auth0RestClient');
+var RetryRestClient = require('../RetryRestClient');
+
+/**
+ * Simple facade for consuming a REST API endpoint.
+ * @external RestClient
+ * @see https://github.com/ngonzalvez/rest-facade
+ */
+
+/**
+ * @class HooksManager
+ * The hooks manager class provides a simple abstraction for performing CRUD operations
+ * on Auth0 HooksManagers.
+ * @constructor
+ * @memberOf module:management
+ *
+ * @param {Object} options            The client options.
+ * @param {String} options.baseUrl    The URL of the API.
+ * @param {Object} [options.headers]  Headers to be included in all requests.
+ * @param {Object} [options.retry]    Retry Policy Config
+ */
+var HooksManager = 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');
+  }
+
+  /**
+   * Options object for the Rest Client instance.
+   *
+   * @type {Object}
+   */
+  var clientOptions = {
+    headers: options.headers,
+    query: { repeatParams: false }
+  };
+
+  /**
+   * Provides an abstraction layer for performing CRUD operations on
+   * {@link https://auth0.com/docs/api/v2#!/HooksManagers Auth0 HooksManagers}.
+   *
+   * @type {external:RestClient}
+   */
+  var auth0RestClient = new Auth0RestClient(
+    options.baseUrl + '/hooks/:id',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.resource = new RetryRestClient(auth0RestClient, options.retry);
+
+  var hookSecretsClient = new Auth0RestClient(
+    options.baseUrl + '/hooks/:id/secrets',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.secrets = new RetryRestClient(hookSecretsClient, options.retry);
+};
+
+/**
+ * Create a new hook.
+ *
+ * @method    create
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * management.hooks.create(data, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook created.
+ * });
+ *
+ * @param   {Object}    data     Hook data object.
+ * @param   {Function}  [cb]     Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(HooksManager, 'create', 'resource.create');
+
+/**
+ * Get all hooks.
+ *
+ * @method    getAll
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example <caption>
+ *   This method takes an optional object as first argument that may be used to
+ *   specify pagination settings. If pagination options are not present,
+ *   the first page of a limited number of results will be returned.
+ * </caption>
+ *
+ * // Pagination settings.
+ * var params = {
+ *   per_page: 10,
+ *   page: 0
+ * };
+ *
+ * management.hooks.getAll(params, function (err, hooks) {
+ *   console.log(hooks.length);
+ * });
+ *
+ * @param   {Object}    [params]          Hooks parameters.
+ * @param   {Number}    [params.per_page] Number of results per page.
+ * @param   {Number}    [params.page]     Page number, zero indexed.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(HooksManager, 'getAll', 'resource.getAll');
+
+/**
+ * Get an Auth0 hook.
+ *
+ * @method    get
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * management.hooks.get({ id: HOOK_ID }, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(hook);
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(HooksManager, 'get', 'resource.get');
+
+/**
+ * Update an existing hook.
+ *
+ * @method    update
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * var data = { name: 'New name' };
+ * var params = { id: HOOK_ID };
+ *
+ * // Using auth0 instance.
+ * management.updateHook(params, data, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(hook.name);  // 'New name'
+ * });
+ *
+ * // Using the hooks manager directly.
+ * management.hooks.update(params, data, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(hook.name);  // 'New name'
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Object}    data          Updated hook data.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(HooksManager, 'update', 'resource.patch');
+
+/**
+ * Delete an existing hook.
+ *
+ * @method    delete
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * management.hooks.delete({ id: HOOK_ID }, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook deleted.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(HooksManager, 'delete', 'resource.delete');
+
+/**
+ * Get Hook secrets
+ *
+ * @method    getSecrets
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * var params = {id : 'HOOK_ID'}
+ * @example <caption>
+ *   This method takes a first argument as the hookId and returns the secrets for the hook. The secret values will be hidden.
+ * </caption>
+ *
+ * management.hooks.getSecrets( {id : 'HOOK_ID'}, function (err, secrets) {
+ *   console.log(secrets);
+ * });
+ *
+ * @param   {Object}    params            Hook parameters.
+ * @param   {String}    params.id         ID of the Hook.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+HooksManager.prototype.getSecrets = function(params, callback) {
+  return this.secrets.getAll(params, callback);
+};
+
+/**
+ * Add secrets in a hook
+ *
+ * @method    addSecrets
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * var params =  { id :'HOOK_ID'};
+ * var data   = { "DB_USER" :  "jdoe", "DB_PASS": "password123!"};
+ *
+ * management.hooks.addSecrets(params, data, function (err, secrets) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // secrets added.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     ID of the Hook.
+ * @param   {Object}    data          object containing secrets as key/value pairs
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+
+HooksManager.prototype.addSecrets = function(params, data, cb) {
+  params = params || {};
+  data = data || {};
+
+  // Require an ID.
+  if (!params.id) {
+    throw new ArgumentError('The id passed in params cannot be null or undefined');
+  }
+
+  if (typeof params.id !== 'string') {
+    throw new ArgumentError('The hook Id has to be a string');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.secrets.create(params, data, cb);
+  }
+
+  return this.secrets.create(params, data);
+};
+
+/**
+ * Update secrets in a hook
+ *
+ * @method    updateSecrets
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * var params =  { id :'HOOK_ID'};
+ * var data   = { "DB_USER" :  "jdoe", "DB_PASS": "password123!"};
+ *
+ * management.hooks.updateSecrets(params, data, function (err, secrets) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // secrets updated.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     ID of the Hook.
+ * @param   {Object}    data          object containing secrets as key/value pairs
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+
+HooksManager.prototype.updateSecrets = function(params, data, cb) {
+  params = params || {};
+  data = data || {};
+
+  // Require an ID.
+  if (!params.id) {
+    throw new ArgumentError('The id passed in params cannot be null or undefined');
+  }
+
+  if (typeof params.id !== 'string') {
+    throw new ArgumentError('The hook Id has to be a string');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.secrets.patch(params, data, cb);
+  }
+
+  return this.secrets.patch(params, data);
+};
+
+/**
+ * Remove secrets from a hook
+ *
+ * @method    removeSecrets
+ * @memberOf  module:management.HooksManager.prototype
+ *
+ * @example
+ * var params =  { id :'HOOK_ID'};
+ * var data =["DB_PASS"];
+ *
+ * management.hooks.removeSecrets(params, data, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // secrets added.
+ * });
+ *
+ * @param   {Object}    params      Hook parameters.
+ * @param   {String}    params.id   ID of the Hook.
+ * @param   {Object}    data        Array of secret names
+ * @param   {Function}  [cb]        Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+
+HooksManager.prototype.removeSecrets = function(params, data, cb) {
+  params = params || {};
+  data = data || {};
+
+  // Require an ID.
+  if (!params.id) {
+    throw new ArgumentError('The id passed in params cannot be null or undefined');
+  }
+
+  if (typeof params.id !== 'string') {
+    throw new ArgumentError('The hook Id has to be a string');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.secrets.delete(params, data, cb);
+  }
+
+  return this.secrets.delete(params, data);
+};
+
+module.exports = HooksManager;
diff --git a/src/management/index.js b/src/management/index.js
index bbda84755..f57610b77 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -30,6 +30,7 @@ var EmailTemplatesManager = require('./EmailTemplatesManager');
 var GuardianManager = require('./GuardianManager');
 var CustomDomainsManager = require('./CustomDomainsManager');
 var RolesManager = require('./RolesManager');
+var HooksManager = require('./HooksManager');
 
 var BASE_URL_FORMAT = 'https://%s/api/v2';
 var MANAGEMENT_API_AUD_FORMAT = 'https://%s/api/v2/';
@@ -304,6 +305,14 @@ var ManagementClient = function(options) {
    * @type {RolesManager}
    */
   this.roles = new RolesManager(managerOptions);
+
+  /**
+   * Simple abstraction for performing CRUD operations on the
+   * hooks endpoint.
+   *
+   * @type {HooksManager}
+   */
+  this.hooks = new HooksManager(managerOptions);
 };
 
 /**
@@ -2888,6 +2897,233 @@ utils.wrapPropertyMethod(ManagementClient, 'removePermissionsFromRole', 'roles.r
  */
 utils.wrapPropertyMethod(ManagementClient, 'getUsersInRole', 'roles.getUsers');
 
+/**
+ * Get all hooks.
+ *
+ * @method    getHooks
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example <caption>
+ *   This method takes an optional object as first argument that may be used to
+ *   specify pagination settings. If pagination options are not present,
+ *   the first page of a limited number of results will be returned.
+ * </caption>
+ *
+ * // Pagination settings.
+ * var params = {
+ *   per_page: 10,
+ *   page: 0
+ * };
+ *
+ * management.getHooks(params, function (err, hooks) {
+ *   console.log(hooks.length);
+ * });
+ *
+ * @param   {Object}    [params]          Hooks parameters.
+ * @param   {Number}    [params.per_page] Number of results per page.
+ * @param   {Number}    [params.page]     Page number, zero indexed.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'getHooks', 'hooks.getAll');
+
+/**
+ * Get an Auth0 hook.
+ *
+ * @method    getHook
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.getHook({ id: HOOK_ID }, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(hook);
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'getHook', 'hooks.get');
+
+/**
+ * Create a new hook.
+ *
+ * @method    createHook
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.createHook(data, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook created.
+ * });
+ *
+ * @param   {Object}    data     Hook data object.
+ * @param   {Function}  [cb]     Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'createHook', 'hooks.create');
+
+/**
+ * Update an existing hook.
+ *
+ * @method    updateHook
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * var params = { id: HOOK_ID };
+ * var data = { name: 'my-hook'};
+ * management.updateHook(params, data, function (err, hook) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(hook.name); // 'my-hook'.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Object}    data          Updated hook data.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'updateHook', 'hooks.update');
+
+/**
+ * Delete an existing hook.
+ *
+ * @method    deleteHook
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * auth0.deleteHook({ id: HOOK_ID }, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook deleted.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'deleteHook', 'hooks.delete');
+
+/**
+ * Get an Auth0 hook's secrets.
+ *
+ * @method    getHookSecrets
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * var params = { id: HOOK_ID }
+ * management.getHookSecrets(params, function (err, secrets) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(secrets);
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'getHookSecrets', 'hooks.getSecrets');
+
+/**
+ * Add hook screts.
+ *
+ * @method    addHookScrets
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * var params = { id: 'HOOK_ID' }
+ * var data = { DB_PASSWORD: 'password1', API_TOKEN: 'secret' }
+ * management.addHookScrets(params, data, function (err, secrets) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook secrets created.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Object}    data          Secrets key/value pairs
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'addHookSecrets', 'hooks.addSecrets');
+
+/**
+ * Update an existing hook.
+ *
+ * @method    updateHookSecrets
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * var params = { id: HOOK_ID };
+ * var data = { API_TOKEN: 'updated-secret'};
+ * management.updateHookSecrets(params, data, function (err, secrets) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   console.log(secrets)
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Object}    data          Secrets key/value pairs
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'updateHookSecrets', 'hooks.updateSecrets');
+
+/**
+ * Delete an existing hook.
+ *
+ * @method    removeHookSecrets
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * var params = { id: HOOK_ID }
+ * var data = ['API_TOKEN', 'DB_PASSWORD']
+ * auth0.removeHookSecrets(params, data, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Hook deleted.
+ * });
+ *
+ * @param   {Object}    params        Hook parameters.
+ * @param   {String}    params.id     Hook ID.
+ * @param   {Object}    data          Secrets key/value pairs
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(ManagementClient, 'removeHookSecrets', 'hooks.removeSecrets');
+
 /**
  * Returns the access_token.
  *


Test patch:
diff --git a/test/management/hooks.tests.js b/test/management/hooks.tests.js
new file mode 100644
index 000000000..7afc756b7
--- /dev/null
+++ b/test/management/hooks.tests.js
@@ -0,0 +1,727 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var SRC_DIR = '../../src';
+var API_URL = 'https://tenant.auth0.com';
+
+var HookManager = require(SRC_DIR + '/management/HooksManager');
+var ArgumentError = require('rest-facade').ArgumentError;
+
+describe('HookManager', function() {
+  before(function() {
+    this.token = 'TOKEN';
+    this.hooks = new HookManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+  describe('instance', function() {
+    var methods = [
+      'get',
+      'getAll',
+      'create',
+      'update',
+      'delete',
+      'getSecrets',
+      'addSecrets',
+      'updateSecrets',
+      'removeSecrets'
+    ];
+
+    methods.forEach(function(method) {
+      it('should have a ' + method + ' method', function() {
+        expect(this.hooks[method]).to.exist.to.be.an.instanceOf(Function);
+      });
+    });
+  });
+
+  describe('#constructor', function() {
+    it('should error when no options are provided', function() {
+      expect(HookManager).to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+    it('should throw an error when no base URL is provided', function() {
+      var client = HookManager.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 = HookManager.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('/hooks')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.getAll(function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .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('/hooks')
+        .reply(500);
+
+      this.hooks.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('/hooks')
+        .reply(200, data);
+
+      this.hooks.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/hooks', function(done) {
+      var request = this.request;
+
+      this.hooks.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('/hooks')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.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 = {
+        include_fields: true,
+        fields: 'test'
+      };
+      var request = nock(API_URL)
+        .get('/hooks')
+        .query(params)
+        .reply(200);
+
+      this.hooks.getAll(params).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#get', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 5,
+        name: 'Test hook',
+        enabled: true,
+        script: "function (user, contest, callback) { console.log('Test'); }",
+        stage: 'login_success'
+      };
+
+      this.request = nock(API_URL)
+        .get('/hooks/' + this.data.id)
+        .reply(200, this.data);
+    });
+
+    it('should accept a callback', function(done) {
+      var params = { id: this.data.id };
+
+      this.hooks.get(params, done.bind(null, null));
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .get({ id: this.data.id })
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should perform a POST request to /api/v2/hooks/5', function(done) {
+      var request = this.request;
+
+      this.hooks.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('/hooks/' + this.data.id)
+        .reply(500);
+
+      this.hooks.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('/hooks/' + this.data.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.get({ id: this.data.id }).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#create', function() {
+    var data = {
+      id: 5,
+      name: 'Test hook',
+      enabled: true,
+      script: "function (user, contest, callback) { console.log('Test'); }",
+      stage: 'login_success'
+    };
+
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .post('/hooks')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.create(data, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .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('/hooks')
+        .reply(500);
+
+      this.hooks.create(data).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/hooks', function(done) {
+      var request = this.request;
+
+      this.hooks.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('/hooks', data)
+        .reply(200);
+
+      this.hooks.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('/hooks')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.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('/hooks/' + this.data.id)
+        .reply(200, this.data);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.update({ id: 5 }, {}, done.bind(null, null));
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .update({ id: 5 }, {})
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should perform a PATCH request to /api/v2/hooks/5', function(done) {
+      var request = this.request;
+
+      this.hooks.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('/hooks/' + this.data.id, this.data)
+        .reply(200);
+
+      this.hooks.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('/hooks/' + this.data.id)
+        .reply(500);
+
+      this.hooks.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('/hooks/' + id)
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.delete({ id: id }, done.bind(null, null));
+    });
+
+    it('should return a promise when no callback is given', function(done) {
+      this.hooks.delete({ id: id }).then(done.bind(null, null));
+    });
+
+    it('should perform a delete request to /hooks/' + id, function(done) {
+      var request = this.request;
+
+      this.hooks.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('/hooks/' + id)
+        .reply(500);
+
+      this.hooks.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('/hooks/' + id)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.delete({ id: id }).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#getSecrets', function() {
+    var data = {
+      id: 'hook_id'
+    };
+
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .get('/hooks/' + data.id + '/secrets')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.getSecrets(data, done.bind(null, null));
+    });
+
+    it('should return a promise when no callback is given', function(done) {
+      this.hooks.getSecrets(data).then(done.bind(null, null));
+    });
+
+    it('should perform a GET request to /api/v2/hooks/hook_id/secrets', function(done) {
+      var request = this.request;
+
+      this.hooks.getSecrets(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)
+        .get('/hooks/' + data.id + '/secrets')
+        .reply(500);
+
+      this.hooks.getSecrets(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)
+        .get('/hooks/' + data.id + '/secrets')
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.getSecrets(data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#addSecrets', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 'hook_id'
+      };
+      this.body = { permission_name: 'My Permission', resource_server_identifier: 'test123' };
+
+      this.request = nock(API_URL)
+        .post('/hooks/' + this.data.id + '/secrets')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.addSecrets(this.data, {}, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .addSecrets(this.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('/hooks/' + this.data.id + '/secrets')
+        .reply(500);
+
+      this.hooks.addSecrets(this.data, {}).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/hooks/hook_id/secrets', function(done) {
+      var request = this.request;
+
+      this.hooks.addSecrets(this.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('/hooks/' + this.data.id + '/secrets', this.body)
+        .reply(200);
+
+      this.hooks.addSecrets(this.data, this.body).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('/hooks/' + this.data.id + '/secrets')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.addSecrets(this.data, {}).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#updateSecrets', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 'hook_id'
+      };
+      this.body = { DB_PASSWORD: 'abcd1234', APITOKEN: 'foosecret' };
+
+      this.request = nock(API_URL)
+        .patch('/hooks/' + this.data.id + '/secrets')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.updateSecrets(this.data, {}, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .updateSecrets(this.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('/hooks/' + this.data.id + '/secrets')
+        .reply(500);
+
+      this.hooks.updateSecrets(this.data, {}).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/hooks/hook_id/secrets', function(done) {
+      var request = this.request;
+
+      this.hooks.updateSecrets(this.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('/hooks/' + this.data.id + '/secrets', this.body)
+        .reply(200);
+
+      this.hooks.updateSecrets(this.data, this.body).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('/hooks/' + this.data.id + '/secrets')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.updateSecrets(this.data, {}).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+
+  describe('#removeSecrets', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 'hook_id'
+      };
+      this.body = { DB_PASSWORD: 'abcd1234', APITOKEN: 'foosecret' };
+
+      this.request = nock(API_URL)
+        .delete('/hooks/' + this.data.id + '/secrets', {})
+        .reply(200);
+    });
+
+    it('should validate empty hookId', function() {
+      var _this = this;
+      expect(function() {
+        _this.hooks.removeSecrets({ id: null }, _this.body, function() {});
+      }).to.throw('The id passed in params cannot be null or undefined');
+    });
+
+    it('should validate non-string hookId', function() {
+      var _this = this;
+      expect(function() {
+        _this.hooks.removeSecrets({ id: 123 }, _this.body, function() {});
+      }).to.throw('The hook Id has to be a string');
+    });
+
+    it('should accept a callback', function(done) {
+      this.hooks.removeSecrets(this.data, {}, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.hooks
+        .removeSecrets(this.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('/hooks/' + this.data.id + '/secrets')
+        .reply(500);
+
+      this.hooks.removeSecrets(this.data, {}).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a DELETE request to /api/v2/hooks/hook_id/secrets', function(done) {
+      var request = this.request;
+
+      this.hooks.removeSecrets(this.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)
+        .delete('/hooks/' + this.data.id + '/secrets', this.body)
+        .reply(200);
+
+      this.hooks.removeSecrets(this.data, this.body).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)
+        .delete('/hooks/' + this.data.id + '/secrets')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.hooks.removeSecrets(this.data, {}).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+});