Repository: auth0/node-auth0
Pull request #370 (2019-06-26 12:59:29)

Add Management API support for Branding and Prompts endpoints.
### Changes

Please describe both what is changing and why this is important. Include:

- Endpoints added: `/prompts` and `/branding`
- Classes added: `BrandingManager` and `PromptsManager`

### References

No external references. Needed by Auth0 SEs for updates to demo environment deploy scripts and auth0-deploy-cli.

### 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] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [ ] All existing and new tests complete without errors

NOTE: Existing tests error, new tests don't



Patch:
diff --git a/src/management/BrandingManager.js b/src/management/BrandingManager.js
new file mode 100644
index 000000000..e743189d9
--- /dev/null
+++ b/src/management/BrandingManager.js
@@ -0,0 +1,104 @@
+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 BrandingManager
+ * Manages settings related to branding.
+ * @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 BrandingManager = 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 = {
+        errorFormatter: { message: 'message', name: 'error' },
+        headers: options.headers,
+        query: { repeatParams: false }
+    };
+
+    /**
+     * Provides an abstraction layer for consuming the
+     * {@link https://auth0.com/docs/api/management/v2#!/Branding Branding endpoint}.
+     *
+     * @type {external:RestClient}
+     */
+    var auth0RestClient = new Auth0RestClient(
+        options.baseUrl + '/branding',
+        clientOptions,
+        options.tokenProvider
+    );
+    this.resource = new RetryRestClient(auth0RestClient, options.retry);
+};
+
+/**
+ * Update the branding settings.
+ *
+ * @method    updateSettings
+ * @memberOf  module:management.BrandingManager.prototype
+ *
+ * @example
+ * management.branding.updateSettings(data, function (err, branding) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ * // Updated branding
+ *    console.log(branding);
+ * });
+ *
+ * @param   {Object}    params            Branding parameters.
+ * @param   {Object}    data              Updated branding data.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return    {Promise|undefined}
+ */
+utils.wrapPropertyMethod(BrandingManager, 'updateSettings', 'resource.patch');
+
+/**
+ * Get the branding settings..
+ *
+ * @method    getSettings
+ * @memberOf  module:management.BrandingManager.prototype
+ *
+ * @example
+ * management.branding.getSettings(data, function (err, branding) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ * // Branding
+ *    console.log(branding);
+ * });
+ *
+ * @param   {Object}    params            Branding parameters.
+ * @param   {Object}    data              Branding data.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return    {Promise|undefined}
+ */
+utils.wrapPropertyMethod(BrandingManager, 'getSettings', 'resource.get');
+
+module.exports = BrandingManager;
diff --git a/src/management/PromptsManager.js b/src/management/PromptsManager.js
new file mode 100644
index 000000000..9d2f7aaba
--- /dev/null
+++ b/src/management/PromptsManager.js
@@ -0,0 +1,104 @@
+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 PromptsManager
+ * Manages settings related to prompts.
+ * @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 PromptsManager = 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 = {
+        errorFormatter: { message: 'message', name: 'error' },
+        headers: options.headers,
+        query: { repeatParams: false }
+    };
+
+    /**
+     * Provides an abstraction layer for consuming the
+     * {@link https://auth0.com/docs/api/management/v2#!/Prompts Prompts endpoint}.
+     *
+     * @type {external:RestClient}
+     */
+    var auth0RestClient = new Auth0RestClient(
+        options.baseUrl + '/prompts',
+        clientOptions,
+        options.tokenProvider
+    );
+    this.resource = new RetryRestClient(auth0RestClient, options.retry);
+};
+
+/**
+ * Update the prompts settings.
+ *
+ * @method    updateSettings
+ * @memberOf  module:management.PromptsManager.prototype
+ *
+ * @example
+ * management.prompts.updateSettings(data, function (err, prompts) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ * // Updated prompts
+ *    console.log(prompts);
+ * });
+ *
+ * @param   {Object}    params            Prompts parameters.
+ * @param   {Object}    data              Updated prompts data.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return    {Promise|undefined}
+ */
+utils.wrapPropertyMethod(PromptsManager, 'updateSettings', 'resource.patch');
+
+/**
+ * Get the prompts settings..
+ *
+ * @method    getSettings
+ * @memberOf  module:management.PromptsManager.prototype
+ *
+ * @example
+ * management.prompts.getSettings(data, function (err, prompts) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ * // Prompts
+ *    console.log(prompts);
+ * });
+ *
+ * @param   {Object}    params            Prompts parameters.
+ * @param   {Object}    data              Prompts data.
+ * @param   {Function}  [cb]              Callback function.
+ *
+ * @return    {Promise|undefined}
+ */
+utils.wrapPropertyMethod(PromptsManager, 'getSettings', 'resource.get');
+
+module.exports = PromptsManager;


Test patch:
diff --git a/test/management/branding.tests.js b/test/management/branding.tests.js
new file mode 100644
index 000000000..10f4f9a16
--- /dev/null
+++ b/test/management/branding.tests.js
@@ -0,0 +1,236 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var SRC_DIR = '../../src';
+var API_URL = 'https://tenant.auth0.com';
+
+var BrandingManager = require(SRC_DIR + '/management/BrandingManager');
+var ArgumentError = require('rest-facade').ArgumentError;
+
+describe('BrandingManager', function() {
+    before(function() {
+        this.token = 'TOKEN';
+        this.branding = new BrandingManager({
+            headers: { authorization: 'Bearer ' + this.token },
+            baseUrl: API_URL
+        });
+    });
+
+    describe('instance', function() {
+        var methods = ['getSettings', 'updateSettings'];
+
+        methods.forEach(function(method) {
+            it('should have a ' + method + ' method', function() {
+                expect(this.branding[method]).to.exist.to.be.an.instanceOf(Function);
+            });
+        });
+    });
+
+    describe('#constructor', function() {
+        it('should error when no options are provided', function() {
+            expect(BrandingManager).to.throw(ArgumentError, 'Must provide manager options');
+        });
+
+        it('should throw an error when no base URL is provided', function() {
+            var client = BrandingManager.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 = BrandingManager.bind(null, { baseUrl: '' });
+
+            expect(client).to.throw(ArgumentError, 'The provided base URL is invalid');
+        });
+    });
+
+    describe('#getSettings', function() {
+        var data = {
+            colors: {
+                primary: '#FFF'
+            },
+            favicon_url: 'https://example.com/favicon.ico',
+            logo_url: 'https://example.com/logo.png',
+            font: {
+                url: 'https://example.com/font.ttf'
+            }
+        };
+
+        beforeEach(function() {
+            this.request = nock(API_URL)
+                .get('/branding')
+                .reply(200);
+        });
+
+        it('should accept a callback', function(done) {
+            this.branding.getSettings(function() {
+                done();
+            });
+        });
+
+        it('should return a promise if no callback is given', function(done) {
+            this.branding
+                .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('/branding')
+                .reply(500);
+
+            this.branding.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 request = nock(API_URL)
+                .get('/branding')
+                .reply(200, data);
+
+            this.branding.getSettings().then(function(provider) {
+                expect(provider.id).to.equal(data.id);
+
+                done();
+            });
+        });
+
+        it('should perform a GET request to /api/v2/branding', function(done) {
+            var request = this.request;
+
+            this.branding.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('/branding')
+                .matchHeader('Authorization', 'Bearer ' + this.token)
+                .reply(200);
+
+            this.branding.getSettings().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('/branding')
+                .query(params)
+                .reply(200);
+
+            this.branding.getSettings(params).then(function() {
+                expect(request.isDone()).to.be.true;
+
+                done();
+            });
+        });
+    });
+
+    describe('#updateSettings', function() {
+        var data = {
+            colors: {
+                primary: '#FFF'
+            },
+            favicon_url: 'https://example.com/favicon.ico',
+            logo_url: 'https://example.com/logo.png',
+            font: {
+                url: 'https://example.com/font.ttf'
+            }
+        };
+
+        beforeEach(function() {
+            this.request = nock(API_URL)
+                .patch('/branding')
+                .reply(200, data);
+        });
+
+        it('should accept a callback', function(done) {
+            this.branding.updateSettings({}, data, function() {
+                done();
+            });
+        });
+
+        it('should return a promise if no callback is given', function(done) {
+            this.branding
+                .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('/branding/' + data.id)
+                .reply(500);
+
+            this.branding.updateSettings({}, data).catch(function(err) {
+                expect(err).to.exist.to.be.an.instanceOf(Error);
+
+                done();
+            });
+        });
+
+        it('should perform a PATCH request to /api/v2/branding', function(done) {
+            var request = this.request;
+
+            this.branding.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('/branding', data)
+                .reply(200);
+
+            this.branding.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('/branding')
+                .matchHeader('Authorization', 'Bearer ' + this.token)
+                .reply(200);
+
+            this.branding.updateSettings({}, data).then(function() {
+                expect(request.isDone()).to.be.true;
+
+                done();
+            });
+        });
+    });
+});
diff --git a/test/management/prompts.tests.js b/test/management/prompts.tests.js
new file mode 100644
index 000000000..4df16a881
--- /dev/null
+++ b/test/management/prompts.tests.js
@@ -0,0 +1,222 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var SRC_DIR = '../../src';
+var API_URL = 'https://tenant.auth0.com';
+
+var PromptsManager = require(SRC_DIR + '/management/PromptsManager');
+var ArgumentError = require('rest-facade').ArgumentError;
+
+describe('PromptsManager', function() {
+    before(function() {
+        this.token = 'TOKEN';
+        this.prompts = new PromptsManager({
+            headers: { authorization: 'Bearer ' + this.token },
+            baseUrl: API_URL
+        });
+    });
+
+    describe('instance', function() {
+        var methods = ['getSettings', 'updateSettings'];
+
+        methods.forEach(function(method) {
+            it('should have a ' + method + ' method', function() {
+                expect(this.prompts[method]).to.exist.to.be.an.instanceOf(Function);
+            });
+        });
+    });
+
+    describe('#constructor', function() {
+        it('should error when no options are provided', function() {
+            expect(PromptsManager).to.throw(ArgumentError, 'Must provide manager options');
+        });
+
+        it('should throw an error when no base URL is provided', function() {
+            var client = PromptsManager.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 = PromptsManager.bind(null, { baseUrl: '' });
+
+            expect(client).to.throw(ArgumentError, 'The provided base URL is invalid');
+        });
+    });
+
+    describe('#getSettings', function() {
+        var data = {
+            universal_login_experience: ''
+        };
+
+        beforeEach(function() {
+            this.request = nock(API_URL)
+                .get('/prompts')
+                .reply(200);
+        });
+
+        it('should accept a callback', function(done) {
+            this.prompts.getSettings(function() {
+                done();
+            });
+        });
+
+        it('should return a promise if no callback is given', function(done) {
+            this.prompts
+                .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('/prompts')
+                .reply(500);
+
+            this.prompts.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 request = nock(API_URL)
+                .get('/prompts')
+                .reply(200, data);
+
+            this.prompts.getSettings().then(function(provider) {
+                expect(provider.id).to.equal(data.id);
+
+                done();
+            });
+        });
+
+        it('should perform a GET request to /api/v2/prompts', function(done) {
+            var request = this.request;
+
+            this.prompts.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('/prompts')
+                .matchHeader('Authorization', 'Bearer ' + this.token)
+                .reply(200);
+
+            this.prompts.getSettings().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('/prompts')
+                .query(params)
+                .reply(200);
+
+            this.prompts.getSettings(params).then(function() {
+                expect(request.isDone()).to.be.true;
+
+                done();
+            });
+        });
+    });
+
+    describe('#updateSettings', function() {
+        var data = {
+            universal_login_experience: 'new'
+        };
+
+        beforeEach(function() {
+            this.request = nock(API_URL)
+                .patch('/prompts')
+                .reply(200, data);
+        });
+
+        it('should accept a callback', function(done) {
+            this.prompts.updateSettings({}, data, function() {
+                done();
+            });
+        });
+
+        it('should return a promise if no callback is given', function(done) {
+            this.prompts
+                .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('/prompts/' + data.id)
+                .reply(500);
+
+            this.prompts.updateSettings({}, data).catch(function(err) {
+                expect(err).to.exist.to.be.an.instanceOf(Error);
+
+                done();
+            });
+        });
+
+        it('should perform a PATCH request to /api/v2/prompts', function(done) {
+            var request = this.request;
+
+            this.prompts.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('/prompts', data)
+                .reply(200);
+
+            this.prompts.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('/prompts')
+                .matchHeader('Authorization', 'Bearer ' + this.token)
+                .reply(200);
+
+            this.prompts.updateSettings({}, data).then(function() {
+                expect(request.isDone()).to.be.true;
+
+                done();
+            });
+        });
+    });
+});