diff --git a/lib/index.js b/lib/index.js index d4dc8b0..316e126 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,6 +9,13 @@ var auth0Ticket = require('./utils/auth0_ticket'); var httpClient = require('./utils/http_client'); var transactionFactory = require('./transaction/factory'); var clientFactory = require('./utils/client_factory'); + +var apiTransport = { + polling: 'polling', + manual: 'polling', + socket: 'socket' +}; + /** * @public * @@ -81,7 +88,9 @@ auth0GuardianJS.prototype.start = function start(callback) { self.httpClient.post('/api/start-flow', self.credentials, - { state_transport: self.transport }, + // TODO: polling is not a good name for api state checking since + // it could be polling or manual checking + { state_transport: apiTransport[self.transport] }, function startTransaction(err, txLegacyData) { if (err) { callback(err); diff --git a/lib/utils/client_factory.js b/lib/utils/client_factory.js index ab1c0f0..b164074 100644 --- a/lib/utils/client_factory.js +++ b/lib/utils/client_factory.js @@ -1,5 +1,6 @@ var polling = require('./polling_client'); var socketio = require('./socket_client'); +var nullClient = require('./null_client'); exports.create = function create(options) { var serviceUrl = options.serviceUrl; @@ -13,6 +14,8 @@ exports.create = function create(options) { if (transport === 'polling') { return polling(serviceUrl, { httpClient: httpClient }); + } else if (transport === 'manual') { + return nullClient(); } // default socket diff --git a/lib/utils/null_client.js b/lib/utils/null_client.js new file mode 100644 index 0000000..b13652d --- /dev/null +++ b/lib/utils/null_client.js @@ -0,0 +1,23 @@ +'use strict'; + +// Null client used when no event client listener is needed (transport=manual) + +var EventEmitter = require('events').EventEmitter; +var object = require('./object'); +var asyncHelpers = require('./async'); + +function nullClient() { + var self = object.create(nullClient.prototype); + + EventEmitter.call(self); + + return self; +} + +nullClient.prototype = new EventEmitter(); + +nullClient.prototype.connect = function connect(token, callback) { + asyncHelpers.setImmediate(callback); +}; + +module.exports = nullClient; diff --git a/test/index.test.js b/test/index.test.js index 45fe659..c7d60ab 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -3,7 +3,7 @@ const expect = require('chai').expect; const guardianjsb = require('../lib'); const sinon = require('sinon'); -// const EventEmitter = require('events').EventEmitter; +const nullClient = require('../lib/utils/null_client'); describe('guardian.js', function () { let httpClient; @@ -308,6 +308,193 @@ describe('guardian.js', function () { }); }); }); + + describe('when transport is manual', function () { + describe('when everything works ok', function () { + let response; + + beforeEach(function () { + response = { + deviceAccount: { + methods: ['otp'], + availableMethods: ['otp'], + name: 'test', + phoneNumber: '+1234' + }, + availableEnrollmentMethods: ['otp'], + availableAuthenticationMethods: ['push'], + transactionToken + }; + + socketClient.connect.yields(); + httpClient.post.yields(null, response); + + guardianjs = guardianjsb({ + serviceUrl: 'https://tenant.guardian.auth0.com', + requestToken, + issuer: { + label: 'label', + name: 'name' + }, + accountLabel: 'accountLabel', + globalTrackingId: 'globalTrackingId', + dependencies: { + httpClient + }, + transport: 'manual' + }); + }); + + it('calls start flow as expected', function (done) { + guardianjs.start((err) => { + expect(err).not.to.exist; + expect(httpClient.post.calledOnce).to.be.true; + + const call = httpClient.post.getCall(0); + expect(call.args[0]).to.equal('/api/start-flow'); + expect(call.args[1].getAuthHeader()).to.equal(`Bearer ${requestToken}`); + expect(call.args[2]).to.eql({ state_transport: 'polling' }); + + done(); + }); + }); + + it('uses nullClient', function () { + // This is an implementation detail, but worth checking IMO + expect(guardianjs.socketClient).to.be.an.instanceOf(nullClient); + }); + + describe('for an user already enrolled', function () { + beforeEach(function () { + response = { + deviceAccount: { + methods: ['otp'], + availableMethods: ['otp'], + name: 'test', + phoneNumber: '+1234' + }, + availableEnrollmentMethods: ['otp'], + availableAuthenticationMethods: ['push'], + transactionToken, + transport: 'manual' + }; + + socketClient.connect.yields(); + httpClient.post.yields(null, response); + + guardianjs = guardianjsb({ + serviceUrl: 'https://tenant.guardian.auth0.com', + requestToken, + issuer: { + label: 'label', + name: 'name' + }, + accountLabel: 'accountLabel', + globalTrackingId: 'globalTrackingId', + dependencies: { + httpClient, + socketClient + }, + transport: 'manual' + }); + }); + + it('callbacks with an enrolled-transaction', function (done) { + guardianjs.start((err, tx) => { + expect(err).not.to.exist; + + const enrollment = tx.getEnrollments()[0]; + expect(enrollment).to.exist; + + expect(tx.isEnrolled()).to.be.true; + + expect(enrollment.getAvailableMethods()) + .to.eql(response.deviceAccount.availableMethods); + expect(enrollment.getMethods()) + .to.eql(response.deviceAccount.methods); + expect(enrollment.getName()) + .to.eql(response.deviceAccount.name); + expect(enrollment.getPhoneNumber()) + .to.eql(response.deviceAccount.phoneNumber); + + expect(tx.transactionToken.getAuthHeader()) + .to.equal(`Bearer ${transactionToken}`); + + done(); + }); + }); + }); + + describe('for an user not enrolled', function () { + beforeEach(function () { + response = { + deviceAccount: { + id: '1234', + otpSecret: 'abcd1234', + recoveryCode: '12asddasdasdasd' + }, + availableEnrollmentMethods: ['otp'], + availableAuthenticationMethods: ['push'], + enrollmentTxId: '1234678', + transactionToken, + transport: 'manual' + }; + + socketClient.connect.yields(); + httpClient.post.yields(null, response); + + guardianjs = guardianjsb({ + serviceUrl: 'https://tenant.guardian.auth0.com', + requestToken, + issuer: { + label: 'label', + name: 'name' + }, + accountLabel: 'accountLabel', + globalTrackingId: 'globalTrackingId', + dependencies: { + httpClient, + socketClient + } + }); + }); + + it('callbacks with a non enrolled transaction', function (done) { + guardianjs.start((err, tx) => { + expect(err).not.to.exist; + + const enrollment = tx.getEnrollments()[0]; + expect(enrollment).not.to.exist; + + expect(tx.getAvailableEnrollmentMethods()).to.eql(['otp']); + expect(tx.getAvailableAuthenticationMethods()).to.eql(['push']); + + expect(tx.enrollmentAttempt.getEnrollmentTransactionId()) + .to.eql(response.enrollmentTxId); + expect(tx.enrollmentAttempt.getOtpSecret()) + .to.eql(response.deviceAccount.otpSecret); + expect(tx.enrollmentAttempt.getIssuerName()) + .to.eql('name'); + expect(tx.enrollmentAttempt.getIssuerLabel()) + .to.eql('label'); + expect(tx.enrollmentAttempt.getAccountLabel()) + .to.eql('accountLabel'); + expect(tx.enrollmentAttempt.getRecoveryCode()) + .to.eql(response.deviceAccount.recoveryCode); + expect(tx.enrollmentAttempt.getEnrollmentId()) + .to.eql(response.deviceAccount.id); + expect(tx.enrollmentAttempt.getBaseUri()) + .to.equal('https://tenant.guardian.auth0.com'); + + expect(tx.transactionToken.getAuthHeader()) + .to.equal(`Bearer ${transactionToken}`); + + done(); + }); + }); + }); + }); + }); }); describe('#resume', function () { @@ -359,3 +546,4 @@ describe('guardian.js', function () { }); }); }); + diff --git a/test/utils/client_factory.test.js b/test/utils/client_factory.test.js new file mode 100644 index 0000000..ba0d99d --- /dev/null +++ b/test/utils/client_factory.test.js @@ -0,0 +1,37 @@ +'use strict'; + +const expect = require('chai').expect; +const clientFactory = require('../../lib/utils/client_factory'); +const pollingClient = require('../../lib/utils/polling_client'); +const socketClient = require('../../lib/utils/socket_client'); +const nullClient = require('../../lib/utils/null_client'); + +describe('client factory', function () { + describe('#create', function () { + describe('if options.dependency is defined', function () { + it('returns the dependency', function () { + const dependency = { hola: 'hello' }; + + expect(clientFactory.create({ dependency })).to.equal(dependency); + }); + }); + + describe('if options.transport is polling', function () { + it('returns an instance of polling client', function () { + expect(clientFactory.create({ transport: 'polling', serviceUrl: 'http://localhost', httpClient: {} })).to.be.an.instanceOf(pollingClient); + }); + }); + + describe('if options.transport is manual', function () { + it('returns an instance of null client', function () { + expect(clientFactory.create({ transport: 'manual' })).to.be.an.instanceOf(nullClient); + }); + }); + + describe('if options.transport is not defined', function () { + it('returns an instance of socket client', function () { + expect(clientFactory.create({ serviceUrl: 'http://localhost' })).to.be.an.instanceOf(socketClient); + }); + }); + }); +}); diff --git a/test/utils/null_client.test.js b/test/utils/null_client.test.js new file mode 100644 index 0000000..e049289 --- /dev/null +++ b/test/utils/null_client.test.js @@ -0,0 +1,30 @@ +'use strict'; + +const expect = require('chai').expect; +const nullClientBuilder = require('../../lib/utils/null_client'); + +describe('utils/null_client', function () { + let nullClient; + + beforeEach(function () { + nullClient = nullClientBuilder(); + }); + + it('has method #on', function () { + expect(nullClient).to.have.respondsTo('on'); + }); + + it('has method #once', function () { + expect(nullClient).to.have.respondsTo('once'); + }); + + it('has method #removeAllListeners', function () { + expect(nullClient).to.have.respondsTo('removeAllListeners'); + }); + + describe('#connect', function () { + it('callbacks immediatelly', function (done) { + nullClient.connect('abc', done); + }); + }); +});