Repository: auth0/node-auth0
Pull request #407 (2019-08-06 18:03:44)

Improve JobManager (get errors + parse error message when importing users)
### Changes

Please describe both what is changing and why this is important. Include:

- Add support for retrieving errors from import jobs
- Add support for parsing error message from the json body of JobsManager.importUsers when present and the existing error.text is empty.

### References

Please include relevant links supporting this change such as a:

- Just some things I discovered while using this library

### 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)
- [x] All existing and new tests complete without errors


jbauth0: Sorry for the PR spam - I messed up something when forking and couldn't update my old  PR so I deleted it and replaced it with this one. 

Patch:
diff --git a/src/management/JobsManager.js b/src/management/JobsManager.js
index 3c244f290..d717ed9d6 100644
--- a/src/management/JobsManager.js
+++ b/src/management/JobsManager.js
@@ -58,6 +58,19 @@ var JobsManager = function(options) {
   );
   this.jobs = new RetryRestClient(auth0RestClient, options.retry);
 
+  /**
+   * Provides an abstraction layer for consuming the
+   * {@link https://auth0.com/docs/api/v2#!/Jobs/:id/errors Errors endpoint}.
+   *
+   * @type {external:RestClient}
+   */
+  var jobErrorsRestClient = new Auth0RestClient(
+    options.baseUrl + '/jobs/:id/errors',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.jobErrors = new RetryRestClient(jobErrorsRestClient, options.retry);
+
   /**
    * Provides an abstraction layer for consuming the
    * {@link https://auth0.com/docs/api/v2#!/Jobs/post_users_exports Create job to export users endpoint}
@@ -189,6 +202,13 @@ JobsManager.prototype.importUsers = function(data, cb) {
             error.status = res.statusCode;
             error.method = method;
             error.text = res.text;
+            try {
+              if (!error.text && res.body) {
+                error.text = JSON.parse(res.body).message;
+              }
+            } catch (ex) {
+              // Ignore the error.
+            }
             reject(error);
           }
           resolve(res);
@@ -264,6 +284,45 @@ JobsManager.prototype.exportUsers = function(data, cb) {
   return this.usersExports.create(data);
 };
 
+/**
+ * Given a job ID, retrieve the failed/errored items
+ *
+ * @method   get
+ * @memberOf module:management.JobsManager.prototype
+ *
+ * @example
+ * var params = {
+ *   id: '{JOB_ID}'
+ * };
+ *
+ * management.jobs.errors(params, function (err, job) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Retrieved job.
+ *   console.log(job);
+ * });
+ *
+ * @param   {Object}    params        Job parameters.
+ * @param   {String}    params.id     Job ID.
+ * @param   {Function}  [cb]          Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+JobsManager.prototype.errors = function(params, cb) {
+  if (!params.id || typeof params.id !== 'string') {
+    throw new ArgumentError('The id parameter must be a valid job id');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.jobErrors.get(params, cb);
+  }
+
+  // Return a promise.
+  return this.jobErrors.get(params);
+};
+
 /**
  * Send a verification email to a user.
  *


Test patch:
diff --git a/test/management/jobs.tests.js b/test/management/jobs.tests.js
index b8b7cb98c..5bccf6c51 100644
--- a/test/management/jobs.tests.js
+++ b/test/management/jobs.tests.js
@@ -87,6 +87,15 @@ describe('JobsManager', function() {
       });
     });
 
+    it('should throw an ArgumentError if an invalid id is passed', function(done) {
+      try {
+        this.jobs.errors({ id: 12345 }, function() {});
+      } catch (err) {
+        expect(err).to.exist;
+        done();
+      }
+    });
+
     it('should pass the body of the response to the "then" handler', function(done) {
       nock.cleanAll();
 
@@ -148,6 +157,110 @@ describe('JobsManager', function() {
     });
   });
 
+  // Error retrieval tests
+  describe('#errors', function() {
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .get('/jobs/' + this.id + '/errors')
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.jobs.errors({ id: this.id }, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.jobs
+        .errors({ 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 + '/errors')
+        .reply(500);
+
+      this.jobs.errors({ id: this.id }).catch(function(err) {
+        expect(err).to.exist;
+        done();
+      });
+    });
+
+    it('should throw an ArgumentError if an invalid id is passed', function(done) {
+      try {
+        this.jobs.errors({ id: null }, function() {});
+      } catch (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 + '/errors')
+        .reply(200, data);
+
+      this.jobs.errors({ 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/:id/errors', function(done) {
+      var request = this.request;
+
+      this.jobs.errors({ 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 + '/errors')
+        .matchHeader('Authorization', 'Bearer ' + token)
+        .reply(200);
+
+      this.jobs.errors({ 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, include_fields: true, fields: 'test' }).then(function() {
+        expect(request.isDone()).to.be.true;
+        done();
+      });
+    });
+  });
+
   const usersFilePath = path.join(__dirname, '../data/users.json');
 
   describe('#importUsers', function() {
@@ -203,6 +316,31 @@ describe('JobsManager', function() {
       });
     });
 
+    it('should pass rest-api json error messages to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .reply((uri, requestBody) => {
+          return [
+            429,
+            {
+              statusCode: 429,
+              error: 'Too Many Requests',
+              message: 'There are 4 active import users jobs'
+            }
+          ];
+        });
+
+      this.jobs.importUsers(data).catch(function(err) {
+        expect(err.message).to.equal(
+          'cannot POST https://tenant.auth0.com/jobs/users-imports (429)'
+        );
+        expect(err.text).to.equal('There are 4 active import users jobs');
+        done();
+      });
+    });
+
     it('should perform a POST request to /api/v2/jobs/users-imports', function(done) {
       var request = this.request;