Repository: auth0/node-auth0
Pull request #725 (2022-05-20 20:22:51)

chore: add branding theme routes
### Changes

Please describe both what is changing and why this is important. Include:

Added methods to use the upcoming `/branding/themes` routes:

```
GET /branding/themes/default
GET /branding/themes/:theme_id
POST /branding/themes
PATCH /branding/themes/:theme_id
DELETE /branding/themes/:theme_id
```

### References

Please include relevant links supporting this change such as a:

- support ticket
- community post
- StackOverflow post
- support forum thread

### 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. 

- [ ] This change adds unit test coverage
- [x] 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)
- [x] All existing and new tests complete without errors



Patch:
diff --git a/src/management/BrandingManager.js b/src/management/BrandingManager.js
index 58c6955f1..b4cfb54da 100644
--- a/src/management/BrandingManager.js
+++ b/src/management/BrandingManager.js
@@ -56,6 +56,19 @@ class BrandingManager {
       options.tokenProvider
     );
     this.brandingTemplates = new RetryRestClient(brandingTemplateAuth0RestClient, options.retry);
+
+    /**
+     * Provides an abstraction layer for consuming the
+     * {@link https://auth0.com/docs/api/management/v2#!/Branding/get_branding_theme Branding theme endpoint}.
+     *
+     * @type {external:RestClient}
+     */
+    const brandingThemesAuth0RestClient = new Auth0RestClient(
+      `${options.baseUrl}/branding/themes/:id`,
+      clientOptions,
+      options.tokenProvider
+    );
+    this.brandingThemes = new RetryRestClient(brandingThemesAuth0RestClient, options.retry);
   }
 
   /**
@@ -156,6 +169,115 @@ class BrandingManager {
   deleteUniversalLoginTemplate(...args) {
     return this.brandingTemplates.delete(...args);
   }
+
+  /**
+   * Get the new universal login theme by id.
+   *
+   * @example
+   * var params = { id: THEME_ID };
+   *
+   * management.branding.getTheme(params, function (err, theme) {
+   *   if (err) {
+   *     // Handle error.
+   *   }
+   *
+   *    // Theme
+   *    console.log(theme);
+   * });
+   * @param   {object}    params            Theme params
+   * @param   {object}    params.id         Theme identifier.
+   * @param   {Function}  [cb]              Callback function.
+   * @returns    {Promise|undefined}
+   */
+  getTheme(...args) {
+    return this.brandingThemes.get(...args);
+  }
+
+  /**
+   * Get the default new universal login theme.
+   *
+   * @example
+   * management.branding.getDefaultTheme(function (err, theme) {
+   *   if (err) {
+   *     // Handle error.
+   *   }
+   *
+   *    // Theme
+   *    console.log(theme);
+   * });
+   * @param   {Function}  [cb]              Callback function.
+   * @returns    {Promise|undefined}
+   */
+  getDefaultTheme(...args) {
+    return this.brandingThemes.get(...[{ id: 'default' }].concat(args));
+  }
+
+  /**
+   * Create a new theme.
+   *
+   * @example
+   * management.branding.createTheme(data, function (err, theme) {
+   *   if (err) {
+   *     // Handle error.
+   *   }
+   *
+   *   // Theme created.
+   *   console.log(theme);
+   * });
+   * @param   {object}    data     theme data object.
+   * @param   {Function}  [cb]     Callback function.
+   * @returns  {Promise|undefined}
+   */
+  createTheme(...args) {
+    return this.brandingThemes.create(...args);
+  }
+
+  /**
+   * Update a theme.
+   *
+   * @example
+   * var data = THEME_OBJ;
+   * var params = { id: THEME_ID };
+   *
+   * management.branding.updateTheme(params, data, function (err, theme) {
+   *   if (err) {
+   *     // Handle error.
+   *   }
+   *
+   *   // Theme updated.
+   *   console.log(theme);
+   * });
+   * @param   {object}    params            Theme parameters.
+   * @param   {object}    params.id         Theme identifier.
+   * @param   {object}    data              Updated theme data.
+   * @param   {Function}  [cb]              Callback function.
+   * @returns    {Promise|undefined}
+   */
+  updateTheme(...args) {
+    return this.brandingThemes.patch(...args);
+  }
+
+  /**
+   * Delete a theme.
+   *
+   * @example
+   * var params = { id: THEME_ID };
+   *
+   * management.branding.deleteTheme(params, function (err) {
+   *   if (err) {
+   *     // Handle error.
+   *   }
+   *
+   *   // Theme deleted.
+   * });
+   * @param   {object}    params            Theme parameters.
+   * @param   {object}    params.id         Theme identifier.
+   * @param   {Function}  [cb]              Callback function.
+   * @returns    {Promise|undefined}
+   */
+  deleteTheme(...args) {
+    return this.brandingThemes.delete(...args);
+  }
 }
 
 module.exports = BrandingManager;


Test patch:
diff --git a/test/data/theme.json b/test/data/theme.json
new file mode 100644
index 000000000..38d23708d
--- /dev/null
+++ b/test/data/theme.json
@@ -0,0 +1,74 @@
+{
+  "borders": {
+    "button_border_radius": 3,
+    "button_border_weight": 1,
+    "buttons_style": "rounded",
+    "input_border_radius": 3,
+    "input_border_weight": 1,
+    "inputs_style": "rounded",
+    "show_widget_shadow": true,
+    "widget_border_weight": 0,
+    "widget_corner_radius": 5
+  },
+  "colors": {
+    "body_text": "#1e212a",
+    "error": "#d03c38",
+    "header": "#1e212a",
+    "icons": "#65676e",
+    "input_background": "#ffffff",
+    "input_border": "#c9cace",
+    "input_filled_text": "#000000",
+    "input_labels_placeholders": "#65676e",
+    "links_focused_components": "#635dff",
+    "primary_button": "#de7878",
+    "primary_button_label": "#ffffff",
+    "secondary_button_border": "#c9cace",
+    "secondary_button_label": "#1e212a",
+    "success": "#13a688",
+    "widget_background": "#ffffff",
+    "widget_border": "#c9cace"
+  },
+  "fonts": {
+    "body_text": {
+      "bold": false,
+      "size": 87.5
+    },
+    "buttons_text": {
+      "bold": false,
+      "size": 100
+    },
+    "font_url": "",
+    "input_labels": {
+      "bold": false,
+      "size": 100
+    },
+    "links": {
+      "bold": true,
+      "size": 87.5
+    },
+    "links_style": "normal",
+    "reference_text_size": 16,
+    "subtitle": {
+      "bold": false,
+      "size": 87.5
+    },
+    "title": {
+      "bold": false,
+      "size": 150
+    }
+  },
+  "page_background": {
+    "background_color": "#000000",
+    "background_image_url": "",
+    "page_layout": "center"
+  },
+  "widget": {
+    "header_text_alignment": "center",
+    "logo_height": 52,
+    "logo_position": "center",
+    "logo_url": "",
+    "social_buttons_layout": "bottom"
+  },
+  "themeId": "themeid1",
+  "displayName": "default theme"
+}
diff --git a/test/management/branding.tests.js b/test/management/branding.tests.js
index 398ac8d25..813d84d61 100644
--- a/test/management/branding.tests.js
+++ b/test/management/branding.tests.js
@@ -1,5 +1,8 @@
 const { expect } = require('chai');
+const fs = require('fs');
+const path = require('path');
 const nock = require('nock');
+const util = require('util');
 
 const API_URL = 'https://tenant.auth0.com';
 
@@ -7,16 +10,27 @@ const BrandingManager = require(`../../src/management/BrandingManager`);
 const { ArgumentError } = require('rest-facade');
 
 describe('BrandingManager', () => {
+  let branding, token;
+
   before(function () {
     this.token = 'TOKEN';
     this.branding = new BrandingManager({
       headers: { authorization: `Bearer ${this.token}` },
       baseUrl: API_URL,
     });
+    ({ branding, token } = this);
   });
 
   describe('instance', () => {
-    const methods = ['getSettings', 'updateSettings'];
+    const methods = [
+      'getSettings',
+      'updateSettings',
+      'getTheme',
+      'getDefaultTheme',
+      'createTheme',
+      'updateTheme',
+      'deleteTheme',
+    ];
 
     methods.forEach((method) => {
       it(`should have a ${method} method`, function () {
@@ -438,4 +452,298 @@ describe('BrandingManager', () => {
       });
     });
   });
+
+  describe('#getTheme', () => {
+    beforeEach(() => {});
+
+    it('should accept a callback', (done) => {
+      nock(API_URL).get('/branding/themes/themeid1').reply(200);
+
+      branding.getTheme({ id: 'themeid1' }, () => {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', async () => {
+      nock(API_URL).get('/branding/themes/themeid1').reply(200);
+
+      const promise = branding.getTheme({ id: 'themeid1' });
+      expect(promise.then).to.exist;
+      expect(promise.catch).to.exist;
+      await promise;
+    });
+
+    it('should pass any errors to the promise catch handler', async () => {
+      nock(API_URL).get('/branding/themes/themeid1').reply(404);
+
+      try {
+        await branding.getTheme({ id: 'themeid1' });
+      } catch (err) {
+        expect(err.statusCode).to.eq(404);
+        expect(err).to.exist;
+      }
+    });
+
+    it('should pass the body of the response to the "then" handler', async () => {
+      const data = JSON.parse(
+        await util.promisify(fs.readFile)(path.join(__dirname, '../data/theme.json'))
+      );
+      nock(API_URL).get('/branding/themes/themeid1').reply(200, data);
+
+      const theme = await branding.getTheme({ id: 'themeid1' });
+      expect(theme.themeId).to.equal(data.themeId);
+    });
+
+    it('should perform a GET request to /api/v2/branding/themes/:theme_id', async () => {
+      const request = nock(API_URL).get('/branding/themes/themeid1').reply(200);
+
+      await branding.getTheme({ id: 'themeid1' });
+      expect(request.isDone()).to.be.true;
+    });
+
+    it('should include the token in the Authorization header', async () => {
+      const request = nock(API_URL)
+        .get('/branding/themes/themeid1')
+        .matchHeader('Authorization', `Bearer ${token}`)
+        .reply(200, { id: 1 });
+
+      await branding.getTheme({ id: 'themeid1' });
+      expect(request.isDone()).to.be.true;
+    });
+  });
+
+  describe('#getDefaultTheme', () => {
+    beforeEach(() => {});
+
+    it('should accept a callback', (done) => {
+      nock(API_URL).get('/branding/themes/default').reply(200);
+
+      branding.getDefaultTheme(() => {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', async () => {
+      nock(API_URL).get('/branding/themes/default').reply(200);
+
+      const promise = branding.getDefaultTheme();
+      expect(promise.then).to.exist;
+      expect(promise.catch).to.exist;
+      await promise;
+    });
+
+    it('should pass any errors to the promise catch handler', async () => {
+      nock(API_URL).get('/branding/themes/default').reply(404);
+
+      try {
+        await branding.getDefaultTheme();
+      } catch (err) {
+        expect(err.statusCode).to.eq(404);
+        expect(err).to.exist;
+      }
+    });
+
+    it('should pass the body of the response to the "then" handler', async () => {
+      const data = JSON.parse(
+        await util.promisify(fs.readFile)(path.join(__dirname, '../data/theme.json'))
+      );
+      nock(API_URL).get('/branding/themes/default').reply(200, data);
+
+      const theme = await branding.getDefaultTheme();
+      expect(theme.themeId).to.equal(data.themeId);
+    });
+
+    it('should perform a GET request to /api/v2/branding/themes/default', async () => {
+      const request = nock(API_URL).get('/branding/themes/default').reply(200);
+
+      await branding.getDefaultTheme();
+      expect(request.isDone()).to.be.true;
+    });
+
+    it('should include the token in the Authorization header', async () => {
+      const request = nock(API_URL)
+        .get('/branding/themes/default')
+        .matchHeader('Authorization', `Bearer ${token}`)
+        .reply(200, { id: 1 });
+
+      await branding.getDefaultTheme();
+      expect(request.isDone()).to.be.true;
+    });
+  });
+
+  describe('#createTheme', () => {
+    let data;
+    beforeEach(async () => {
+      data = JSON.parse(
+        await util.promisify(fs.readFile)(path.join(__dirname, '../data/theme.json'))
+      );
+    });
+
+    it('should accept a callback', (done) => {
+      nock(API_URL).post(`/branding/themes`, data).reply(201);
+
+      branding.createTheme(data, () => {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', async () => {
+      nock(API_URL).post(`/branding/themes`, data).reply(201);
+
+      const promise = branding.createTheme(data);
+      expect(promise.then).to.exist;
+      expect(promise.catch).to.exist;
+      await promise;
+    });
+
+    it('should pass any errors to the promise catch handler', async () => {
+      nock(API_URL).post(`/branding/themes`, data).reply(409);
+
+      try {
+        await branding.createTheme(data);
+      } catch (err) {
+        expect(err.statusCode).to.eq(409);
+        expect(err).to.exist;
+      }
+    });
+
+    it('should pass the body of the response to the "then" handler', async () => {
+      nock(API_URL).post(`/branding/themes`, data).reply(201, data);
+
+      const theme = await branding.createTheme(data);
+      expect(theme.themeId).to.equal(data.themeId);
+    });
+
+    it('should perform a POST request to /branding/themes', async () => {
+      const request = nock(API_URL).post(`/branding/themes`, data).reply(201, data);
+
+      await branding.createTheme(data);
+      expect(request.isDone()).to.be.true;
+    });
+
+    it('should include the token in the Authorization header', async () => {
+      const request = nock(API_URL)
+        .post('/branding/themes', data)
+        .matchHeader('Authorization', `Bearer ${token}`)
+        .reply(201, data);
+
+      await branding.createTheme(data);
+      expect(request.isDone()).to.be.true;
+    });
+  });
+
+  describe('#updateTheme', () => {
+    let data, themeId, params;
+    beforeEach(async () => {
+      ({ themeId, ...data } = JSON.parse(
+        await util.promisify(fs.readFile)(path.join(__dirname, '../data/theme.json'))
+      ));
+      params = { id: themeId };
+    });
+
+    it('should accept a callback', (done) => {
+      nock(API_URL).patch(`/branding/themes/${themeId}`, data).reply(200);
+
+      branding.updateTheme(params, data, () => {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', async () => {
+      nock(API_URL).patch(`/branding/themes/${themeId}`, data).reply(200);
+
+      const promise = branding.updateTheme(params, data);
+      expect(promise.then).to.exist;
+      expect(promise.catch).to.exist;
+      await promise;
+    });
+
+    it('should pass any errors to the promise catch handler', async () => {
+      nock(API_URL).patch(`/branding/themes/${themeId}`, data).reply(404);
+
+      try {
+        await branding.updateTheme(params, data);
+      } catch (err) {
+        expect(err.statusCode).to.eq(404);
+        expect(err).to.exist;
+      }
+    });
+
+    it('should pass the body of the response to the "then" handler', async () => {
+      nock(API_URL).patch(`/branding/themes/${themeId}`, data).reply(200, data);
+
+      const theme = await branding.updateTheme(params, data);
+      expect(theme.themeId).to.equal(data.themeId);
+    });
+
+    it('should perform a PATCH request to /api/v2/branding/themes/:theme_id', async () => {
+      const request = nock(API_URL).patch(`/branding/themes/${themeId}`, data).reply(200, data);
+
+      await branding.updateTheme(params, data);
+      expect(request.isDone()).to.be.true;
+    });
+
+    it('should include the token in the Authorization header', async () => {
+      const request = nock(API_URL)
+        .patch(`/branding/themes/${themeId}`, data)
+        .matchHeader('Authorization', `Bearer ${token}`)
+        .reply(200, data);
+
+      await branding.updateTheme(params, data);
+      expect(request.isDone()).to.be.true;
+    });
+  });
+
+  describe('#deleteTheme', () => {
+    let themeId, params;
+    beforeEach(async () => {
+      themeId = 'themeid1';
+      params = { id: themeId };
+    });
+
+    it('should accept a callback', (done) => {
+      nock(API_URL).delete(`/branding/themes/${themeId}`).reply(204);
+
+      branding.deleteTheme(params, () => {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', async () => {
+      nock(API_URL).delete(`/branding/themes/${themeId}`).reply(204);
+
+      const promise = branding.deleteTheme(params);
+      expect(promise.then).to.exist;
+      expect(promise.catch).to.exist;
+      await promise;
+    });
+
+    it('should pass any errors to the promise catch handler', async () => {
+      nock(API_URL).delete(`/branding/themes/${themeId}`).reply(404);
+
+      try {
+        await branding.deleteTheme(params);
+      } catch (err) {
+        expect(err.statusCode).to.eq(404);
+        expect(err).to.exist;
+      }
+    });
+
+    it('should perform a PATCH request to /api/v2/branding/themes/:theme_id', async () => {
+      const request = nock(API_URL).delete(`/branding/themes/${themeId}`).reply(204);
+
+      await branding.deleteTheme(params);
+      expect(request.isDone()).to.be.true;
+    });
+
+    it('should include the token in the Authorization header', async () => {
+      const request = nock(API_URL)
+        .delete(`/branding/themes/${themeId}`)
+        .matchHeader('Authorization', `Bearer ${token}`)
+        .reply(204);
+
+      await branding.deleteTheme(params);
+      expect(request.isDone()).to.be.true;
+    });
+  });
 });