Repository: auth0/node-auth0 (ts)

Issue:
importUsers as first API command doesn't authenticate
To recreate:

* Create an API connection in the standard way with `create:users` permission
* Call `auth0.jobs.importUsers`

The call will fail with a 401, as importUsers does not request an auth token prior to running its own query. If you call another API method first (such as `createUser` or `findByEmail`) it works fine.

I think this is because this method (and possibly `UsersManager.impersonate`) roll their own POST request, rather than going through Auth0RestClient as every other method does.

Pull request description:
Get access token before importing users
Get an access token before running `importUsers`. Resolves #266.

The changes are more minimal than they look, because of the pre-commit formatter. Description of change added in comments on the relevant lines.

Patch:
diff --git a/src/management/JobsManager.js b/src/management/JobsManager.js
index 9611b1b3d..fb26918f5 100644
--- a/src/management/JobsManager.js
+++ b/src/management/JobsManager.js
@@ -100,7 +100,7 @@ JobsManager.prototype.get = function(params, cb) {
 
 /**
  * Given a path to a file and a connection id, create a new job that imports the
- * users contained in the file or JSON string and associate them with the given 
+ * users contained in the file or JSON string and associate them with the given
  * connection.
  *
  * @method   importUsers
@@ -135,42 +135,45 @@ JobsManager.prototype.importUsers = function(data, cb) {
   var url = options.baseUrl + '/jobs/users-imports';
   var method = 'POST';
 
-  var promise = new Promise(function(resolve, reject) {
-    request(
-      {
-        url: url,
-        method: method,
-        headers: headers,
-        formData: {
-          users: {
-            value: data.users_json ? Buffer.from(data.users_json) : fs.createReadStream(data.users),
-            options: {
-              filename: data.users_json ? 'users.json' : data.users,
-            }
-          },
-          connection_id: data.connection_id
+  var promise = options.tokenProvider.getAccessToken().then(function(access_token) {
+    return new Promise(function(resolve, reject) {
+      request(
+        {
+          url: url,
+          method: method,
+          headers: extend({ Authorization: `Bearer ${access_token}` }, headers),
+          formData: {
+            users: {
+              value: data.users_json
+                ? Buffer.from(data.users_json)
+                : fs.createReadStream(data.users),
+              options: {
+                filename: data.users_json ? 'users.json' : data.users
+              }
+            },
+            connection_id: data.connection_id
+          }
+        },
+        function(err, res) {
+          if (err) {
+            reject(err);
+            return;
+          }
+          // `superagent` uses the error parameter in callback on http errors.
+          // the following code is intended to keep that behaviour (https://github.com/visionmedia/superagent/blob/master/lib/node/response.js#L170)
+          var type = (res.statusCode / 100) | 0;
+          var isErrorResponse = 4 === type || 5 === type;
+          if (isErrorResponse) {
+            var error = new Error('cannot ' + method + ' ' + url + ' (' + res.statusCode + ')');
+            error.status = res.statusCode;
+            error.method = method;
+            error.text = res.text;
+            reject(error);
+          }
+          resolve(res);
         }
-      },
-      function(err, res) {
-        // `superagent` uses the error parameter in callback on http errors.
-        // the following code is intended to keep that behaviour (https://github.com/visionmedia/superagent/blob/master/lib/node/response.js#L170)
-        var type = (res.statusCode / 100) | 0;
-        var isErrorResponse = 4 === type || 5 === type;
-        if (isErrorResponse) {
-          var error = new Error('cannot ' + method + url + ' (' + res.statusCode + ')');
-          error.status = res.statusCode;
-          error.method = method;
-          error.text = res.text;
-          reject(error);
-        }
-
-        if (err) {
-          reject(err);
-        }
-
-        resolve(res);
-      }
-    );
+      );
+    });
   });
 
   // Don't return a promise if a callback was given.


Test patch:
diff --git a/test/management/jobs.tests.js b/test/management/jobs.tests.js
index f93fef635..c273fbfb2 100644
--- a/test/management/jobs.tests.js
+++ b/test/management/jobs.tests.js
@@ -10,12 +10,18 @@ var API_URL = 'https://tenant.auth0.com';
 var JobsManager = require(SRC_DIR + '/management/JobsManager');
 var ArgumentError = require('rest-facade').ArgumentError;
 
+var token = 'TOKEN';
+
 describe('JobsManager', function() {
   before(function() {
-    this.token = 'TOKEN';
     this.id = 'testJob';
     this.jobs = new JobsManager({
-      headers: { authorization: 'Bearer ' + this.token },
+      tokenProvider: {
+        getAccessToken: function() {
+          return Promise.resolve(token);
+        }
+      },
+      headers: {},
       baseUrl: API_URL
     });
   });
@@ -115,7 +121,7 @@ describe('JobsManager', function() {
 
       var request = nock(API_URL)
         .get('/jobs/' + this.id)
-        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .matchHeader('Authorization', 'Bearer ' + token)
         .reply(200);
 
       this.jobs.get({ id: this.id }).then(function() {
@@ -169,7 +175,20 @@ describe('JobsManager', function() {
         .catch(done.bind(null, null));
     });
 
-    it('should pass any errors to the promise catch handler', function(done) {
+    it('should pass request errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/jobs/users-imports')
+        .replyWithError('printer on fire');
+
+      this.jobs.importUsers(data).catch(function(err) {
+        expect(err.message).to.equal('printer on fire');
+        done();
+      });
+    });
+
+    it('should pass HTTP errors to the promise catch handler', function(done) {
       nock.cleanAll();
 
       var request = nock(API_URL)
@@ -177,7 +196,9 @@ describe('JobsManager', function() {
         .reply(500);
 
       this.jobs.importUsers(data).catch(function(err) {
-        expect(err).to.exist;
+        expect(err.message).to.equal(
+          'cannot POST https://tenant.auth0.com/jobs/users-imports (500)'
+        );
         done();
       });
     });
@@ -253,7 +274,7 @@ describe('JobsManager', function() {
 
       var request = nock(API_URL)
         .post('/jobs/users-imports')
-        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .matchHeader('Authorization', 'Bearer ' + token)
         .reply(200);
 
       this.jobs.importUsers(data).then(function() {
@@ -378,7 +399,7 @@ describe('JobsManager', function() {
 
       var request = nock(API_URL)
         .post('/jobs/verification-email')
-        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .matchHeader('Authorization', 'Bearer ' + token)
         .reply(200);
 
       this.jobs.verifyEmail(data).then(function() {